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/lightning-tools": "^5.0.3",
"@getalby/sdk": "^3.6.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@prisma/client": "^5.17.0",
@ -25,7 +26,7 @@
"next": "14.2.5",
"next-auth": "^4.24.7",
"next-remove-imports": "^1.0.12",
"nodemailer": "^6.9.14",
"nodemailer": "^6.9.15",
"nostr-tools": "^2.7.1",
"primeicons": "^7.0.0",
"primereact": "^10.7.0",
@ -916,6 +917,16 @@
"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": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
@ -8832,9 +8843,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "6.9.14",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz",
"integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==",
"version": "6.9.15",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz",
"integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"

View File

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

@ -8,18 +8,62 @@ generator client {
}
model User {
id String @id @default(uuid())
pubkey String @unique
username String? @unique
avatar String?
purchased Purchase[]
courses Course[]
resources Resource[]
courseDrafts CourseDraft[]
drafts Draft[]
role Role?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
pubkey String? @unique
privkey String?
name String?
email String? @unique
emailVerified DateTime?
image String?
username String? @unique
avatar String?
purchased Purchase[]
courses Course[]
resources Resource[]
courseDrafts CourseDraft[]
drafts Draft[]
role Role?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
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 {

View File

@ -46,7 +46,7 @@ const UserAvatar = () => {
return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) {
// 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 = [
{
@ -81,7 +81,7 @@ const UserAvatar = () => {
className={styles.logo}
/>
</div>
<Menu model={items} popup ref={menu} />
<Menu model={items} popup ref={menu} className='w-[250px] break-words' />
</>
);
} else {

View File

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

View File

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

View File

@ -85,12 +85,15 @@ export const addCoursePurchaseToUser = async (userId, purchaseData) => {
export const createUser = async (data) => {
return await prisma.user.create({
data,
data: {
...data,
emailVerified: data.email ? new Date() : null,
},
});
};
export const updateUser = async (id, data) => {
console.log("user modelllll", id, data)
console.log("Updating user", id, data)
return await prisma.user.update({
where: { id },
data,
@ -169,3 +172,18 @@ export const expireUserSubscriptions = async (userIds) => {
await prisma.$transaction(updatePromises);
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 CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import NDK from "@nostr-dev-kit/ndk";
import axios from "axios";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/db/prisma";
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 = [
"wss://nos.lol/",
@ -43,7 +49,6 @@ const authorize = async (pubkey) => {
// Create user
if (profile) {
const fields = await findKind0Fields(profile);
console.log('FEEEEELDS', fields);
const payload = { pubkey, ...fields };
const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload);
@ -58,6 +63,7 @@ const authorize = async (pubkey) => {
export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
id: "nostr",
@ -72,15 +78,43 @@ export default NextAuth({
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: {
async jwt({ token, trigger, user }) {
console.log('TRIGGER', trigger);
if (trigger === "update") {
// if we trigger an update call the authorize function again
const newUser = await authorize(token.user.pubkey);
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
if (user) {
token.user = user;
@ -88,6 +122,7 @@ export default NextAuth({
return token;
},
async session({ session, token }) {
console.log('SESSION', session);
// Add user from token to session
session.user = token.user;
session.jwt = token;
@ -97,7 +132,6 @@ export default NextAuth({
return baseUrl;
},
async signOut({ token, session }) {
console.log('signOut', token, session);
token = {}
session = {}
return true

View File

@ -2,14 +2,18 @@ import { getUserById, getUserByPubkey, updateUser, deleteUser } from "@/db/model
export default async function handler(req, res) {
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 isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(slug);
try {
let user;
if (isPubkey) {
// If slug is a pubkey
user = await getUserByPubkey(slug);
} else if (isEmail) {
// If slug is an email
user = await getUserByEmail(slug);
} else {
// Assume slug is an ID
const id = parseInt(slug);

View File

@ -2,72 +2,86 @@ import { signIn, useSession } from "next-auth/react"
import { useState, useEffect } from "react"
import { useNDKContext } from "@/context/NDKContext";
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
export default function SignIn() {
const [email, setEmail] = useState("")
const [nostrPubkey, setNostrPubkey] = useState("")
const [nostrPrivkey, setNostrPrivkey] = useState("")
const [email, setEmail] = useState("")
const [showEmailInput, setShowEmailInput] = useState(false)
const [nostrPubkey, setNostrPubkey] = useState("")
const [nostrPrivkey, setNostrPrivkey] = useState("")
const {ndk, addSigner} = useNDKContext();
const { data: session, status } = useSession(); // Get the current session's data and status
const {ndk, addSigner} = useNDKContext();
useEffect(() => {
console.log("session", session)
}, [session])
const { data: session, status } = useSession(); // Get the current session's data and status
const handleEmailSignIn = async (e) => {
e.preventDefault()
await signIn("email", { email, callbackUrl: '/' })
}
useEffect(() => {
console.log("session", session)
}, [session])
const handleEmailSignIn = (e) => {
e.preventDefault()
signIn("email", { email })
const handleNostrSignIn = async (e) => {
e.preventDefault()
if (!ndk.signer) {
await addSigner();
}
const handleNostrSignIn = async (e) => {
e.preventDefault()
if (!ndk.signer) {
await addSigner();
}
try {
const user = await ndk.signer.user()
const pubkey = user?._pubkey
signIn("nostr", { pubkey })
} catch (error) {
console.error("Error signing Nostr event:", error)
}
try {
const user = await ndk.signer.user()
const pubkey = user?._pubkey
signIn("nostr", { pubkey })
} catch (error) {
console.error("Error signing Nostr event:", error)
}
}
const handleAnonymousSignIn = (e) => {
e.preventDefault()
signIn("anonymous")
}
const handleAnonymousSignIn = (e) => {
e.preventDefault()
signIn("anonymous")
}
return (
<div className="w-[100vw] min-bottom-bar:w-[82vw] mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Sign In</h1>
<Button
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleNostrSignIn}
/>
<Button
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleAnonymousSignIn}
/>
<Button
label={"login with email"}
icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleEmailSignIn}
/>
</div>
)
return (
<div className="w-[100vw] min-bottom-bar:w-[82vw] mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Sign In</h1>
<Button
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleNostrSignIn}
/>
<Button
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleAnonymousSignIn}
/>
<Button
label={"login with email"}
icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
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>
)
}