diff --git a/package-lock.json b/package-lock.json
index e6593e1..14e7e81 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
diff --git a/package.json b/package.json
index 92462fd..2e4dd94 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/prisma/migrations/20240906171109_email_auth/migration.sql b/prisma/migrations/20240906171109_email_auth/migration.sql
new file mode 100644
index 0000000..ec8cc37
--- /dev/null
+++ b/prisma/migrations/20240906171109_email_auth/migration.sql
@@ -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;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6b524fe..f3e0f57 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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 {
diff --git a/src/components/navbar/user/UserAvatar.js b/src/components/navbar/user/UserAvatar.js
index ff49abd..bb584d2 100644
--- a/src/components/navbar/user/UserAvatar.js
+++ b/src/components/navbar/user/UserAvatar.js
@@ -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}
/>
-
+
>
);
} else {
diff --git a/src/components/profile/UserProfile.js b/src/components/profile/UserProfile.js
index 098a72c..b616b08 100644
--- a/src/components/profile/UserProfile.js
+++ b/src/components/profile/UserProfile.js
@@ -70,7 +70,7 @@ const UserProfile = () => {
- {user.username || "Anon"}
+ {user.username || user?.email || "Anon"}
{user.pubkey}
diff --git a/src/components/profile/UserSettings.js b/src/components/profile/UserSettings.js
index 4587302..7edce70 100644
--- a/src/components/profile/UserSettings.js
+++ b/src/components/profile/UserSettings.js
@@ -70,7 +70,7 @@ const UserSettings = () => {
- {user.username || "Anon"}
+ {user.username || user?.email || "Anon"}
{user.pubkey}
diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js
index 0e23b54..c1bf435 100644
--- a/src/db/models/userModels.js
+++ b/src/db/models/userModels.js
@@ -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,
+ },
+ },
+ },
+ });
+};
diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js
index a853c1e..383f3f1 100644
--- a/src/pages/api/auth/[...nextauth].js
+++ b/src/pages/api/auth/[...nextauth].js
@@ -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
diff --git a/src/pages/api/users/[slug].js b/src/pages/api/users/[slug].js
index c602fc8..05dfc32 100644
--- a/src/pages/api/users/[slug].js
+++ b/src/pages/api/users/[slug].js
@@ -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);
diff --git a/src/pages/auth/signin.js b/src/pages/auth/signin.js
index d8edc06..f87318f 100644
--- a/src/pages/auth/signin.js
+++ b/src/pages/auth/signin.js
@@ -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 (
-
-
Sign In
-
-
-
-
- )
+ return (
+
+
Sign In
+
+
+
+ )
}
\ No newline at end of file