Email login is working

This commit is contained in:
austinkelsay 2024-09-06 12:32:23 -05:00
parent 9534f83e65
commit 9528010829
11 changed files with 283 additions and 86 deletions

19
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/bitcoin-connect-react": "^3.5.3",
"@getalby/lightning-tools": "^5.0.3", "@getalby/lightning-tools": "^5.0.3",
"@getalby/sdk": "^3.6.1", "@getalby/sdk": "^3.6.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@nostr-dev-kit/ndk": "^2.10.0", "@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@prisma/client": "^5.17.0", "@prisma/client": "^5.17.0",
@ -25,7 +26,7 @@
"next": "14.2.5", "next": "14.2.5",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"next-remove-imports": "^1.0.12", "next-remove-imports": "^1.0.12",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.15",
"nostr-tools": "^2.7.1", "nostr-tools": "^2.7.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.7.0", "primereact": "^10.7.0",
@ -916,6 +917,16 @@
"crypto-js": "4.2.0" "crypto-js": "4.2.0"
} }
}, },
"node_modules/@next-auth/prisma-adapter": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz",
"integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==",
"license": "ISC",
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
}
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.2.5", "version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
@ -8832,9 +8843,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "6.9.14", "version": "6.9.15",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz",
"integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"

View File

@ -13,6 +13,7 @@
"@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/bitcoin-connect-react": "^3.5.3",
"@getalby/lightning-tools": "^5.0.3", "@getalby/lightning-tools": "^5.0.3",
"@getalby/sdk": "^3.6.1", "@getalby/sdk": "^3.6.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@nostr-dev-kit/ndk": "^2.10.0", "@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@prisma/client": "^5.17.0", "@prisma/client": "^5.17.0",
@ -26,7 +27,7 @@
"next": "14.2.5", "next": "14.2.5",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"next-remove-imports": "^1.0.12", "next-remove-imports": "^1.0.12",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.15",
"nostr-tools": "^2.7.1", "nostr-tools": "^2.7.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.7.0", "primereact": "^10.7.0",

View File

@ -0,0 +1,71 @@
/*
Warnings:
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "email" TEXT,
ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT,
ADD COLUMN "name" TEXT,
ADD COLUMN "privkey" TEXT,
ALTER COLUMN "pubkey" DROP NOT NULL;
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"oauth_token_secret" TEXT,
"oauth_token" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -9,7 +9,12 @@ generator client {
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
pubkey String @unique pubkey String? @unique
privkey String?
name String?
email String? @unique
emailVerified DateTime?
image String?
username String? @unique username String? @unique
avatar String? avatar String?
purchased Purchase[] purchased Purchase[]
@ -18,10 +23,49 @@ model User {
courseDrafts CourseDraft[] courseDrafts CourseDraft[]
drafts Draft[] drafts Draft[]
role Role? role Role?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
oauth_token_secret String?
oauth_token String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Role { model Role {
id String @id @default(uuid()) id String @id @default(uuid())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View File

@ -46,7 +46,7 @@ const UserAvatar = () => {
return null; // Or return a loader/spinner/placeholder return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) { } else if (user && Object.keys(user).length > 0) {
// User exists, show username or pubkey // User exists, show username or pubkey
const displayName = user.username || user.pubkey.slice(0, 10) + '...'; const displayName = user.username || user?.email || user?.pubkey.slice(0, 10) + '...';
const items = [ const items = [
{ {
@ -81,7 +81,7 @@ const UserAvatar = () => {
className={styles.logo} className={styles.logo}
/> />
</div> </div>
<Menu model={items} popup ref={menu} /> <Menu model={items} popup ref={menu} className='w-[250px] break-words' />
</> </>
); );
} else { } else {

View File

@ -70,7 +70,7 @@ const UserProfile = () => {
</div> </div>
<h1 className="text-center text-2xl my-2"> <h1 className="text-center text-2xl my-2">
{user.username || "Anon"} {user.username || user?.email || "Anon"}
</h1> </h1>
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4"> <h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
{user.pubkey} {user.pubkey}

View File

@ -70,7 +70,7 @@ const UserSettings = () => {
</div> </div>
<h1 className="text-center text-2xl my-2"> <h1 className="text-center text-2xl my-2">
{user.username || "Anon"} {user.username || user?.email || "Anon"}
</h1> </h1>
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4"> <h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
{user.pubkey} {user.pubkey}

View File

@ -85,12 +85,15 @@ export const addCoursePurchaseToUser = async (userId, purchaseData) => {
export const createUser = async (data) => { export const createUser = async (data) => {
return await prisma.user.create({ return await prisma.user.create({
data, data: {
...data,
emailVerified: data.email ? new Date() : null,
},
}); });
}; };
export const updateUser = async (id, data) => { export const updateUser = async (id, data) => {
console.log("user modelllll", id, data) console.log("Updating user", id, data)
return await prisma.user.update({ return await prisma.user.update({
where: { id }, where: { id },
data, data,
@ -169,3 +172,18 @@ export const expireUserSubscriptions = async (userIds) => {
await prisma.$transaction(updatePromises); await prisma.$transaction(updatePromises);
return userIds.length; return userIds.length;
}; };
export const getUserByEmail = async (email) => {
return await prisma.user.findUnique({
where: { email },
include: {
role: true,
purchased: {
include: {
course: true,
resource: true,
},
},
},
});
};

View File

@ -1,8 +1,14 @@
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import NDK from "@nostr-dev-kit/ndk"; import NDK from "@nostr-dev-kit/ndk";
import axios from "axios"; import axios from "axios";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/db/prisma";
import { findKind0Fields } from "@/utils/nostr"; import { findKind0Fields } from "@/utils/nostr";
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import { bytesToHex } from '@noble/hashes/utils'
import { updateUser } from "@/db/models/userModels";
const relayUrls = [ const relayUrls = [
"wss://nos.lol/", "wss://nos.lol/",
@ -43,7 +49,6 @@ const authorize = async (pubkey) => {
// Create user // Create user
if (profile) { if (profile) {
const fields = await findKind0Fields(profile); const fields = await findKind0Fields(profile);
console.log('FEEEEELDS', fields);
const payload = { pubkey, ...fields }; const payload = { pubkey, ...fields };
const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload); const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload);
@ -58,6 +63,7 @@ const authorize = async (pubkey) => {
export default NextAuth({ export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [ providers: [
CredentialsProvider({ CredentialsProvider({
id: "nostr", id: "nostr",
@ -72,15 +78,43 @@ export default NextAuth({
return null; return null;
}, },
}), }),
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD
}
},
from: process.env.EMAIL_FROM
}),
], ],
callbacks: { callbacks: {
async jwt({ token, trigger, user }) { async jwt({ token, trigger, user }) {
console.log('TRIGGER', trigger);
if (trigger === "update") { if (trigger === "update") {
// if we trigger an update call the authorize function again // if we trigger an update call the authorize function again
const newUser = await authorize(token.user.pubkey); const newUser = await authorize(token.user.pubkey);
token.user = newUser; token.user = newUser;
} }
// if the user has no pubkey, generate a new key pair
if (token && token?.user && token?.user?.id && !token.user?.pubkey) {
try {
let sk = generateSecretKey()
let pk = getPublicKey(sk)
let skHex = bytesToHex(sk)
const updatedUser = await updateUser(token.user.id, {pubkey: pk, privkey: skHex});
if (!updatedUser) {
console.error("Failed to update user");
return null;
}
token.user = updatedUser;
} catch (error) {
console.error("Ephemeral key pair generation error:", error);
return null;
}
}
// Add combined user object to the token // Add combined user object to the token
if (user) { if (user) {
token.user = user; token.user = user;
@ -88,6 +122,7 @@ export default NextAuth({
return token; return token;
}, },
async session({ session, token }) { async session({ session, token }) {
console.log('SESSION', session);
// Add user from token to session // Add user from token to session
session.user = token.user; session.user = token.user;
session.jwt = token; session.jwt = token;
@ -97,7 +132,6 @@ export default NextAuth({
return baseUrl; return baseUrl;
}, },
async signOut({ token, session }) { async signOut({ token, session }) {
console.log('signOut', token, session);
token = {} token = {}
session = {} session = {}
return true return true

View File

@ -2,14 +2,18 @@ import { getUserById, getUserByPubkey, updateUser, deleteUser } from "@/db/model
export default async function handler(req, res) { export default async function handler(req, res) {
const { slug } = req.query; const { slug } = req.query;
// Determine if slug is a pubkey or an ID // Determine if slug is a pubkey, ID, or email
const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug); const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug);
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(slug);
try { try {
let user; let user;
if (isPubkey) { if (isPubkey) {
// If slug is a pubkey // If slug is a pubkey
user = await getUserByPubkey(slug); user = await getUserByPubkey(slug);
} else if (isEmail) {
// If slug is an email
user = await getUserByEmail(slug);
} else { } else {
// Assume slug is an ID // Assume slug is an ID
const id = parseInt(slug); const id = parseInt(slug);

View File

@ -2,36 +2,32 @@ import { signIn, useSession } from "next-auth/react"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useNDKContext } from "@/context/NDKContext"; import { useNDKContext } from "@/context/NDKContext";
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
export default function SignIn() { export default function SignIn() {
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [showEmailInput, setShowEmailInput] = useState(false)
const [nostrPubkey, setNostrPubkey] = useState("") const [nostrPubkey, setNostrPubkey] = useState("")
const [nostrPrivkey, setNostrPrivkey] = useState("") const [nostrPrivkey, setNostrPrivkey] = useState("")
const {ndk, addSigner} = useNDKContext(); const {ndk, addSigner} = useNDKContext();
const { data: session, status } = useSession(); // Get the current session's data and status const { data: session, status } = useSession(); // Get the current session's data and status
useEffect(() => { useEffect(() => {
console.log("session", session) console.log("session", session)
}, [session]) }, [session])
const handleEmailSignIn = (e) => { const handleEmailSignIn = async (e) => {
e.preventDefault() e.preventDefault()
signIn("email", { email }) await signIn("email", { email, callbackUrl: '/' })
} }
const handleNostrSignIn = async (e) => { const handleNostrSignIn = async (e) => {
e.preventDefault() e.preventDefault()
if (!ndk.signer) { if (!ndk.signer) {
await addSigner(); await addSigner();
} }
try { try {
const user = await ndk.signer.user() const user = await ndk.signer.user()
const pubkey = user?._pubkey const pubkey = user?._pubkey
signIn("nostr", { pubkey }) signIn("nostr", { pubkey })
} catch (error) { } catch (error) {
@ -66,8 +62,26 @@ export default function SignIn() {
icon="pi pi-envelope" icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto" className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded rounded
onClick={handleEmailSignIn} onClick={() => setShowEmailInput(!showEmailInput)}
/> />
{showEmailInput && (
<form onSubmit={handleEmailSignIn} className="flex flex-col items-center bg-gray-700 w-fit mx-auto p-4 rounded-lg">
<InputText
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
className="w-[250px] my-4"
/>
<Button
type="submit"
label={"Submit"}
icon="pi pi-check"
className="text-[#f8f8ff] w-fit my-4"
rounded
/>
</form>
)}
</div> </div>
) )
} }