2024-08-07 16:02:13 -05:00
|
|
|
import NextAuth from "next-auth";
|
|
|
|
import CredentialsProvider from "next-auth/providers/credentials";
|
2024-09-06 12:32:23 -05:00
|
|
|
import EmailProvider from "next-auth/providers/email";
|
|
|
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
|
|
|
import prisma from "@/db/prisma";
|
2024-10-06 15:49:32 -05:00
|
|
|
import nodemailer from 'nodemailer';
|
2024-08-07 16:02:13 -05:00
|
|
|
import { findKind0Fields } from "@/utils/nostr";
|
2024-09-06 12:32:23 -05:00
|
|
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
|
|
|
|
import { bytesToHex } from '@noble/hashes/utils'
|
2024-11-19 16:02:11 -06:00
|
|
|
import { updateUser, getUserByPubkey, createUser, getUserById } from "@/db/models/userModels";
|
2024-09-11 16:48:56 -05:00
|
|
|
import { createRole } from "@/db/models/roleModels";
|
2024-09-17 13:55:51 -05:00
|
|
|
import appConfig from "@/config/appConfig";
|
2024-11-18 16:42:40 -06:00
|
|
|
import GithubProvider from "next-auth/providers/github";
|
2024-11-18 18:05:55 -06:00
|
|
|
import NDK from "@nostr-dev-kit/ndk";
|
2024-08-07 16:02:13 -05:00
|
|
|
|
2024-11-15 11:08:56 -06:00
|
|
|
// todo: currently email accounts ephemeral privkey gets saved to db but not anon user, is this required at all given the newer auth setup?
|
|
|
|
|
2024-11-18 18:05:55 -06:00
|
|
|
const ndk = new NDK({
|
|
|
|
explicitRelayUrls: appConfig.defaultRelayUrls
|
|
|
|
})
|
|
|
|
|
2024-08-12 17:27:47 -05:00
|
|
|
const authorize = async (pubkey) => {
|
|
|
|
await ndk.connect();
|
|
|
|
const user = ndk.getUser({ pubkey });
|
|
|
|
|
|
|
|
try {
|
|
|
|
const profile = await user.fetchProfile();
|
|
|
|
|
|
|
|
// Check if user exists, create if not
|
2024-10-02 16:58:36 -05:00
|
|
|
let dbUser = await getUserByPubkey(pubkey);
|
2024-10-04 16:41:49 -05:00
|
|
|
|
2024-10-02 16:58:36 -05:00
|
|
|
if (dbUser) {
|
2024-08-12 17:27:47 -05:00
|
|
|
const fields = await findKind0Fields(profile);
|
2024-10-05 16:13:01 -05:00
|
|
|
// Only update 'avatar' or 'username' if they are different from kind0 fields on the dbUser
|
2024-10-05 16:37:44 -05:00
|
|
|
if (fields.avatar !== dbUser.avatar) {
|
|
|
|
const updatedUser = await updateUser(dbUser.id, { avatar: fields.avatar });
|
|
|
|
if (updatedUser) {
|
|
|
|
dbUser = await getUserByPubkey(pubkey);
|
|
|
|
}
|
|
|
|
} else if (fields.username !== dbUser.username) {
|
2024-12-06 18:47:05 -06:00
|
|
|
const updatedUser = await updateUser(dbUser.id, { username: fields.username, name: fields.username });
|
2024-10-05 16:37:44 -05:00
|
|
|
if (updatedUser) {
|
|
|
|
dbUser = await getUserByPubkey(pubkey);
|
2024-10-04 16:41:49 -05:00
|
|
|
}
|
|
|
|
}
|
2024-10-05 16:37:44 -05:00
|
|
|
// add the kind0 fields to the user
|
2024-10-05 16:13:01 -05:00
|
|
|
const combinedUser = { ...dbUser, kind0: fields };
|
2024-08-12 17:27:47 -05:00
|
|
|
|
2024-10-04 16:41:49 -05:00
|
|
|
return combinedUser;
|
2024-10-02 16:58:36 -05:00
|
|
|
} else {
|
2024-08-12 17:27:47 -05:00
|
|
|
// Create user
|
|
|
|
if (profile) {
|
|
|
|
const fields = await findKind0Fields(profile);
|
2024-12-06 18:47:05 -06:00
|
|
|
const payload = { pubkey, username: fields.username, avatar: fields.avatar, name: fields.username };
|
2024-08-12 17:27:47 -05:00
|
|
|
|
2024-10-04 16:41:49 -05:00
|
|
|
if (appConfig.authorPubkeys.includes(pubkey)) {
|
|
|
|
// create a new author role for this user
|
|
|
|
const createdUser = await createUser(payload);
|
|
|
|
const role = await createRole({
|
|
|
|
userId: createdUser.id,
|
|
|
|
admin: true,
|
|
|
|
subscribed: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!role) {
|
|
|
|
console.error("Failed to create role");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const updatedUser = await updateUser(createdUser.id, { role: role.id });
|
|
|
|
if (!updatedUser) {
|
|
|
|
console.error("Failed to update user");
|
|
|
|
return null;
|
|
|
|
}
|
2024-10-06 17:39:37 -05:00
|
|
|
|
2024-10-04 16:41:49 -05:00
|
|
|
const fullUser = await getUserByPubkey(pubkey);
|
|
|
|
|
2024-10-05 16:13:01 -05:00
|
|
|
return { ...fullUser, kind0: fields };
|
2024-10-04 16:41:49 -05:00
|
|
|
} else {
|
|
|
|
dbUser = await createUser(payload);
|
2024-10-05 16:13:01 -05:00
|
|
|
return { ...dbUser, kind0: fields };
|
2024-10-04 16:41:49 -05:00
|
|
|
}
|
2024-08-12 17:27:47 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Nostr login error:", error);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-09-08 18:41:51 -05:00
|
|
|
export const authOptions = {
|
2024-09-06 12:32:23 -05:00
|
|
|
adapter: PrismaAdapter(prisma),
|
2024-08-07 16:02:13 -05:00
|
|
|
providers: [
|
|
|
|
CredentialsProvider({
|
|
|
|
id: "nostr",
|
|
|
|
name: "Nostr",
|
|
|
|
credentials: {
|
|
|
|
pubkey: { label: "Public Key", type: "text" },
|
|
|
|
},
|
|
|
|
authorize: async (credentials) => {
|
|
|
|
if (credentials?.pubkey) {
|
2024-08-12 17:27:47 -05:00
|
|
|
return await authorize(credentials.pubkey);
|
2024-08-07 16:02:13 -05:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
}),
|
2024-09-06 12:32:23 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
},
|
2024-10-06 15:39:53 -05:00
|
|
|
from: process.env.EMAIL_FROM,
|
|
|
|
sendVerificationRequest: async ({ identifier, url, provider }) => {
|
2024-10-06 15:46:33 -05:00
|
|
|
// Use nodemailer to send the email
|
|
|
|
const transport = nodemailer.createTransport(provider.server);
|
|
|
|
await transport.sendMail({
|
|
|
|
to: identifier,
|
|
|
|
from: provider.from,
|
|
|
|
subject: `Sign in to ${new URL(url).host}`,
|
|
|
|
text: `Sign in to ${new URL(url).host}\n${url}\n\n`,
|
|
|
|
html: `<p>Sign in to <strong>${new URL(url).host}</strong></p><p><a href="${url}">Sign in</a></p>`,
|
|
|
|
});
|
2024-10-06 15:39:53 -05:00
|
|
|
}
|
2024-09-06 12:32:23 -05:00
|
|
|
}),
|
2024-11-18 16:42:40 -06:00
|
|
|
GithubProvider({
|
|
|
|
clientId: process.env.GITHUB_CLIENT_ID,
|
2024-11-18 18:05:55 -06:00
|
|
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
|
|
profile: async (profile) => {
|
|
|
|
return {
|
|
|
|
id: profile.id.toString(),
|
2024-11-19 16:02:11 -06:00
|
|
|
name: profile.login,
|
2024-11-18 18:05:55 -06:00
|
|
|
email: profile.email,
|
|
|
|
avatar: profile.avatar_url
|
|
|
|
}
|
|
|
|
}
|
2024-11-18 16:42:40 -06:00
|
|
|
}),
|
2024-10-08 19:24:40 -05:00
|
|
|
CredentialsProvider({
|
|
|
|
id: "anonymous",
|
|
|
|
name: "Anonymous",
|
|
|
|
credentials: {
|
|
|
|
pubkey: { label: "Public Key", type: "text" },
|
|
|
|
privkey: { label: "Private Key", type: "text" },
|
|
|
|
},
|
|
|
|
authorize: async (credentials) => {
|
|
|
|
let pubkey, privkey;
|
|
|
|
|
|
|
|
if (credentials?.pubkey && credentials?.pubkey !== "null" && credentials?.privkey && credentials?.privkey !== "null") {
|
|
|
|
// Use provided keys
|
|
|
|
pubkey = credentials.pubkey;
|
|
|
|
privkey = credentials.privkey;
|
|
|
|
} else {
|
|
|
|
// Generate new keys
|
|
|
|
const sk = generateSecretKey();
|
|
|
|
pubkey = getPublicKey(sk);
|
|
|
|
privkey = bytesToHex(sk);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if user exists in the database
|
|
|
|
let dbUser = await getUserByPubkey(pubkey);
|
2024-11-18 16:42:40 -06:00
|
|
|
|
2024-10-08 19:24:40 -05:00
|
|
|
if (!dbUser) {
|
|
|
|
// Create new user if not exists
|
|
|
|
dbUser = await createUser({
|
|
|
|
pubkey: pubkey,
|
2024-12-06 18:47:05 -06:00
|
|
|
username: pubkey.slice(0, 8),
|
2024-10-08 19:24:40 -05:00
|
|
|
});
|
2024-12-06 18:47:05 -06:00
|
|
|
} else {
|
|
|
|
// Check if this user has a linked GitHub account
|
|
|
|
const githubAccount = await prisma.account.findFirst({
|
|
|
|
where: {
|
|
|
|
userId: dbUser.id,
|
|
|
|
provider: 'github'
|
|
|
|
},
|
|
|
|
include: {
|
|
|
|
user: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (githubAccount) {
|
|
|
|
// Return the user with GitHub provider information
|
|
|
|
return {
|
|
|
|
...dbUser,
|
|
|
|
pubkey,
|
|
|
|
privkey,
|
|
|
|
// Add these fields to switch to GitHub provider
|
|
|
|
provider: 'github',
|
|
|
|
type: 'oauth',
|
|
|
|
providerAccountId: githubAccount.providerAccountId
|
|
|
|
};
|
|
|
|
}
|
2024-10-08 19:24:40 -05:00
|
|
|
}
|
2024-11-18 16:42:40 -06:00
|
|
|
|
2024-10-08 19:24:40 -05:00
|
|
|
// Return user object with pubkey and privkey
|
|
|
|
return { ...dbUser, pubkey, privkey };
|
|
|
|
},
|
|
|
|
}),
|
2024-08-07 16:02:13 -05:00
|
|
|
],
|
|
|
|
callbacks: {
|
2024-11-19 16:02:11 -06:00
|
|
|
async jwt({ token, user, account, trigger, profile }) {
|
2024-12-06 18:47:05 -06:00
|
|
|
if (user?.provider === 'github') {
|
|
|
|
// User has a linked GitHub account, use that as the primary provider
|
|
|
|
token.account = {
|
|
|
|
provider: 'github',
|
|
|
|
type: 'oauth',
|
|
|
|
providerAccountId: user.providerAccountId
|
|
|
|
};
|
|
|
|
// Add GitHub profile information
|
|
|
|
token.githubProfile = {
|
|
|
|
login: user.username,
|
|
|
|
avatar_url: user.avatar,
|
|
|
|
email: user.email
|
|
|
|
};
|
|
|
|
} else if (account) {
|
2024-11-19 16:02:11 -06:00
|
|
|
// Store GitHub-specific information
|
|
|
|
if (account.provider === 'github') {
|
2024-12-06 18:47:05 -06:00
|
|
|
token.account = account;
|
2024-11-19 16:02:11 -06:00
|
|
|
token.githubProfile = {
|
|
|
|
login: profile?.login,
|
|
|
|
avatar_url: profile?.avatar_url,
|
|
|
|
email: profile?.email,
|
|
|
|
};
|
|
|
|
}
|
2024-11-18 18:05:55 -06:00
|
|
|
}
|
|
|
|
|
2024-10-09 16:28:24 -05:00
|
|
|
if (trigger === "update" && account?.provider !== "anonymous") {
|
|
|
|
// if we trigger an update call the authorize function again
|
|
|
|
const newUser = await authorize(token.user.pubkey);
|
|
|
|
token.user = newUser;
|
|
|
|
}
|
|
|
|
|
2024-11-14 15:51:29 -06:00
|
|
|
// if we sign up with email and we don't have a pubkey or privkey, we need to generate them
|
|
|
|
if (trigger === "signUp" && account?.provider === "email" && !user.pubkey && !user.privkey) {
|
|
|
|
const sk = generateSecretKey();
|
|
|
|
const pubkey = getPublicKey(sk);
|
|
|
|
const privkey = bytesToHex(sk);
|
2024-11-18 16:42:40 -06:00
|
|
|
|
2024-11-14 15:51:29 -06:00
|
|
|
// Update the user in the database
|
|
|
|
await prisma.user.update({
|
|
|
|
where: { id: user.id },
|
|
|
|
data: { pubkey, privkey }
|
|
|
|
});
|
2024-11-18 16:42:40 -06:00
|
|
|
|
2024-11-14 15:51:29 -06:00
|
|
|
// Update the user object
|
|
|
|
user.pubkey = pubkey;
|
|
|
|
user.privkey = privkey;
|
|
|
|
}
|
|
|
|
|
2024-11-18 16:42:40 -06:00
|
|
|
// Add new condition for first-time GitHub sign up
|
|
|
|
if (trigger === "signUp" && account?.provider === "github") {
|
|
|
|
const sk = generateSecretKey();
|
|
|
|
const pubkey = getPublicKey(sk);
|
|
|
|
const privkey = bytesToHex(sk);
|
|
|
|
|
2024-11-19 16:02:11 -06:00
|
|
|
// Use GitHub login (username) from the profile stored in token
|
|
|
|
const githubUsername = token.githubProfile?.login;
|
2024-11-18 18:05:55 -06:00
|
|
|
|
|
|
|
// Update the user in the database with all GitHub details
|
2024-11-18 16:42:40 -06:00
|
|
|
await prisma.user.update({
|
|
|
|
where: { id: user.id },
|
2024-11-18 18:05:55 -06:00
|
|
|
data: {
|
|
|
|
pubkey,
|
|
|
|
privkey,
|
|
|
|
name: githubUsername,
|
|
|
|
username: githubUsername,
|
2024-11-19 16:02:11 -06:00
|
|
|
avatar: token.githubProfile?.avatar_url || null,
|
|
|
|
email: token.githubProfile?.email,
|
2024-11-18 18:05:55 -06:00
|
|
|
}
|
2024-11-18 16:42:40 -06:00
|
|
|
});
|
|
|
|
|
2024-11-18 18:05:55 -06:00
|
|
|
// Update the user object with all credentials
|
2024-11-18 16:42:40 -06:00
|
|
|
user.pubkey = pubkey;
|
|
|
|
user.privkey = privkey;
|
2024-11-18 18:05:55 -06:00
|
|
|
user.name = githubUsername;
|
|
|
|
user.username = githubUsername;
|
2024-11-19 16:02:11 -06:00
|
|
|
user.avatar = token.githubProfile?.avatar_url;
|
|
|
|
user.email = token.githubProfile?.email;
|
2024-11-18 16:42:40 -06:00
|
|
|
}
|
|
|
|
|
2024-12-06 18:47:05 -06:00
|
|
|
if (account && account.provider === "github" && user?.id && user?.pubkey) {
|
|
|
|
// we are linking a github account to an existing account
|
|
|
|
const updatedUser = await updateUser(user.id, {
|
|
|
|
name: profile?.login,
|
|
|
|
username: profile?.login,
|
|
|
|
avatar: profile?.avatar_url,
|
|
|
|
email: profile?.email,
|
|
|
|
privkey: user.privkey || token.privkey, // Preserve the existing privkey
|
|
|
|
image: profile?.avatar_url, // Also save to image field
|
|
|
|
});
|
|
|
|
|
|
|
|
if (updatedUser) {
|
|
|
|
user = await getUserById(user.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-06 16:53:36 -05:00
|
|
|
if (user) {
|
|
|
|
token.user = user;
|
2024-10-08 19:24:40 -05:00
|
|
|
if (user.pubkey && user.privkey) {
|
|
|
|
token.pubkey = user.pubkey;
|
|
|
|
token.privkey = user.privkey;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (account?.provider === 'anonymous') {
|
|
|
|
token.isAnonymous = true;
|
2024-10-06 16:53:36 -05:00
|
|
|
}
|
|
|
|
return token;
|
|
|
|
},
|
|
|
|
async session({ session, token }) {
|
2024-11-19 16:02:11 -06:00
|
|
|
// If this is a GitHub session, get the full user data from DB first
|
|
|
|
if (token.account?.provider === 'github') {
|
|
|
|
const dbUser = await getUserById(token.user.id);
|
|
|
|
|
|
|
|
// Start with the complete DB user as the base
|
|
|
|
session.user = dbUser;
|
|
|
|
|
|
|
|
// Override only the GitHub-specific fields
|
|
|
|
session.user = {
|
|
|
|
...dbUser, // This includes role, purchases, userCourses, userLessons, etc.
|
2024-12-06 18:47:05 -06:00
|
|
|
username: token.githubProfile?.login,
|
|
|
|
name: token.githubProfile?.login,
|
|
|
|
avatar: token.githubProfile?.avatar_url,
|
|
|
|
email: token.githubProfile?.email
|
2024-11-19 16:02:11 -06:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// For non-GitHub sessions, use the existing token.user
|
|
|
|
session.user = token.user;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep the rest of the session data
|
2024-11-18 18:05:55 -06:00
|
|
|
if (token.account) {
|
|
|
|
session.account = token.account;
|
2024-11-19 16:02:11 -06:00
|
|
|
if (token.account.provider === 'github') {
|
|
|
|
session.githubProfile = token.githubProfile;
|
|
|
|
}
|
2024-11-18 18:05:55 -06:00
|
|
|
}
|
2024-10-08 19:24:40 -05:00
|
|
|
if (token.pubkey && token.privkey) {
|
|
|
|
session.pubkey = token.pubkey;
|
|
|
|
session.privkey = token.privkey;
|
|
|
|
}
|
|
|
|
session.isAnonymous = token.isAnonymous;
|
2024-08-07 16:02:13 -05:00
|
|
|
return session;
|
|
|
|
},
|
|
|
|
async redirect({ url, baseUrl }) {
|
2024-08-07 17:06:53 -05:00
|
|
|
return baseUrl;
|
2024-08-07 16:02:13 -05:00
|
|
|
},
|
2024-08-13 16:28:25 -05:00
|
|
|
async signOut({ token, session }) {
|
|
|
|
token = {}
|
|
|
|
session = {}
|
|
|
|
return true
|
2024-10-04 16:41:49 -05:00
|
|
|
},
|
2024-10-08 19:24:40 -05:00
|
|
|
async signIn({ user, account }) {
|
|
|
|
if (account.provider === 'anonymous') {
|
2024-11-18 16:42:40 -06:00
|
|
|
return {
|
|
|
|
...user,
|
|
|
|
pubkey: user.pubkey,
|
|
|
|
privkey: user.privkey,
|
|
|
|
};
|
2024-10-08 19:24:40 -05:00
|
|
|
}
|
|
|
|
return true;
|
2024-11-18 16:42:40 -06:00
|
|
|
},
|
2024-08-07 16:02:13 -05:00
|
|
|
},
|
|
|
|
secret: process.env.NEXTAUTH_SECRET,
|
2024-08-07 17:06:53 -05:00
|
|
|
session: { strategy: "jwt" },
|
2024-08-07 16:02:13 -05:00
|
|
|
jwt: {
|
|
|
|
signingKey: process.env.JWT_SECRET,
|
|
|
|
},
|
|
|
|
pages: {
|
|
|
|
signIn: "/auth/signin",
|
2024-09-09 15:44:18 -05:00
|
|
|
}
|
2024-09-08 18:41:51 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
export default NextAuth(authOptions);
|