From 417159aad92258792b9c0b9c01572cf6a0821753 Mon Sep 17 00:00:00 2001
From: austinkelsay <austinkelsay@yahoo.com>
Date: Fri, 10 Jan 2025 14:39:45 -0600
Subject: [PATCH] Recovery option, fix profile card menu placement

---
 src/components/profile/UserProfileCard.js |  4 +-
 src/pages/api/auth/[...nextauth].js       | 50 +++++++++++++++++++
 src/pages/auth/signin.js                  | 60 +++++++++++++++++++++++
 3 files changed, 112 insertions(+), 2 deletions(-)

diff --git a/src/components/profile/UserProfileCard.js b/src/components/profile/UserProfileCard.js
index 0c93869..4301cc9 100644
--- a/src/components/profile/UserProfileCard.js
+++ b/src/components/profile/UserProfileCard.js
@@ -48,7 +48,7 @@ const UserProfileCard = ({ user }) => {
     const MobileProfileCard = () => (
         <div className="w-full bg-gray-800 rounded-lg p-2 py-1 border border-gray-700 shadow-md h-[420px] flex flex-col justify-center items-start">
             <div className="flex flex-col gap-2 pt-4 w-full relative">
-                <div className="absolute top-8 right-[10px]">
+                <div className="absolute top-8 right-[14px]">
                     <i
                         className="pi pi-ellipsis-h text-2xl cursor-pointer"
                         onClick={(e) => menu.current.toggle(e)}
@@ -145,7 +145,7 @@ const UserProfileCard = ({ user }) => {
                     className="rounded-full my-4"
                 />
                 <div className="flex flex-col gap-2 pt-4 w-fit relative">
-                    <div className="absolute top-[-4px] right-[-30px]">
+                    <div className="absolute top-[-1px] right-[-24px]">
                         <i
                             className="pi pi-ellipsis-h text-2xl cursor-pointer"
                             onClick={(e) => menu.current.toggle(e)}
diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js
index 2ed0013..32b93f1 100644
--- a/src/pages/api/auth/[...nextauth].js
+++ b/src/pages/api/auth/[...nextauth].js
@@ -12,6 +12,7 @@ import { updateUser, getUserByPubkey, createUser, getUserById, getUserByEmail }
 import { createRole } from "@/db/models/roleModels";
 import appConfig from "@/config/appConfig";
 import NDK from "@nostr-dev-kit/ndk";
+import { nip19 } from 'nostr-tools';
 
 // Initialize NDK for Nostr interactions
 const ndk = new NDK({
@@ -162,6 +163,55 @@ export const authOptions = {
                     avatar: profile.avatar_url
                 };
             }
+        }),
+        // Recovery provider
+        CredentialsProvider({
+            id: "recovery",
+            name: "Recovery",
+            credentials: {
+                nsec: { label: "Recovery Key (nsec or hex)", type: "text" }
+            },
+            authorize: async (credentials) => {
+                if (!credentials?.nsec) return null;
+                
+                try {
+                    // Convert nsec to hex if needed
+                    let privkeyHex = credentials.nsec;
+                    if (credentials.nsec.startsWith('nsec')) {
+                        try {
+                            const { data: decodedPrivkey } = nip19.decode(credentials.nsec);
+                            privkeyHex = Buffer.from(decodedPrivkey).toString('hex');
+                        } catch (error) {
+                            console.error("Invalid nsec format:", error);
+                            return null;
+                        }
+                    }
+
+                    // Find user with matching privkey
+                    const user = await prisma.user.findFirst({
+                        where: { privkey: privkeyHex },
+                        include: {
+                            role: true,
+                            purchased: true,
+                            userCourses: true,
+                            userLessons: true,
+                            nip05: true,
+                            lightningAddress: true,
+                            userBadges: true
+                        }
+                    });
+
+                    if (!user) {
+                        console.error("No user found with provided recovery key");
+                        return null;
+                    }
+
+                    return user;
+                } catch (error) {
+                    console.error("Recovery authorization error:", error);
+                    return null;
+                }
+            }
         })
     ],
     callbacks: {
diff --git a/src/pages/auth/signin.js b/src/pages/auth/signin.js
index 1b7b079..b7d546d 100644
--- a/src/pages/auth/signin.js
+++ b/src/pages/auth/signin.js
@@ -7,7 +7,9 @@ import { InputText } from 'primereact/inputtext';
 
 export default function SignIn() {
   const [email, setEmail] = useState("")
+  const [nsec, setNsec] = useState("")
   const [showEmailInput, setShowEmailInput] = useState(false)
+  const [showRecoveryInput, setShowRecoveryInput] = useState(false)
   const {ndk, addSigner} = useNDKContext();
   const { data: session, status } = useSession();
   const router = useRouter();
@@ -68,6 +70,25 @@ export default function SignIn() {
     }
   };
 
+  const handleRecoverySignIn = async (e) => {
+    e.preventDefault()
+    try {
+      const result = await signIn("recovery", { 
+        nsec,
+        redirect: false,
+        callbackUrl: '/'
+      });
+
+      if (result?.ok) {
+        router.push('/');
+      } else {
+        console.error("Recovery login failed:", result?.error);
+      }
+    } catch (error) {
+      console.error("Recovery sign in error:", error);
+    }
+  }
+
   useEffect(() => {
     // Redirect if already signed in
     if (session?.user) {
@@ -124,6 +145,45 @@ export default function SignIn() {
         rounded
         onClick={handleAnonymousSignIn}
       />
+      <GenericButton
+        label={"recover account"}
+        icon="pi pi-key"
+        className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
+        rounded
+        onClick={() => setShowRecoveryInput(!showRecoveryInput)}
+      />
+      {showRecoveryInput && (
+        <form onSubmit={handleRecoverySignIn} className="flex flex-col items-center bg-gray-700 w-fit mx-auto p-4 rounded-lg">
+          <div className="text-center mb-4 max-w-[350px]">
+            <p className="text-yellow-400 mb-2">⚠️ Recovery Notice</p>
+            <p className="text-gray-200 mb-2">
+              🔑 This recovery option is only for accounts created through:
+            </p>
+            <ul className="text-gray-300 mb-2 text-left list-none">
+              <li>📧 Email Login</li>
+              <li>👤 Anonymous Login</li>
+              <li>🐙 GitHub Login</li>
+            </ul>
+            <p className="text-red-400 text-sm">
+              ⛔ Do NOT enter your personal Nostr nsec here! Only use the recovery key provided by PlebDevs (available on your profile page).
+            </p>
+          </div>
+          <InputText
+            type="password"
+            value={nsec}
+            onChange={(e) => setNsec(e.target.value)}
+            placeholder="Enter recovery key (nsec or hex)"
+            className="w-[250px] my-4"
+          />
+          <GenericButton
+            type="submit"
+            label={"Recover Account"}
+            icon="pi pi-lock-open"
+            className="text-[#f8f8ff] w-fit my-4"
+            rounded
+          />
+        </form>
+      )}
     </div>
   )
 }
\ No newline at end of file