mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Email login is working
This commit is contained in:
parent
9534f83e65
commit
9528010829
19
package-lock.json
generated
19
package-lock.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
71
prisma/migrations/20240906171109_email_auth/migration.sql
Normal file
71
prisma/migrations/20240906171109_email_auth/migration.sql
Normal 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;
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user