plebdevs/src/pages/api/auth/[...nextauth].js

301 lines
11 KiB
JavaScript
Raw Normal View History

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";
2024-12-08 17:24:08 -06:00
import GithubProvider from "next-auth/providers/github";
2024-09-06 12:32:23 -05:00
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/db/prisma";
2024-10-06 15:49:32 -05:00
import nodemailer from 'nodemailer';
import { findKind0Fields } from "@/utils/nostr";
2024-12-08 17:24:08 -06:00
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { bytesToHex } from '@noble/hashes/utils';
import { updateUser, getUserByPubkey, createUser, getUserById, getUserByEmail } from "@/db/models/userModels";
import { createRole } from "@/db/models/roleModels";
2024-09-17 13:55:51 -05:00
import appConfig from "@/config/appConfig";
import NDK from "@nostr-dev-kit/ndk";
2024-12-08 17:24:08 -06:00
// Initialize NDK for Nostr interactions
const ndk = new NDK({
explicitRelayUrls: appConfig.defaultRelayUrls
2024-12-08 17:24:08 -06:00
});
2024-12-08 17:24:08 -06:00
/**
* Handles Nostr profile synchronization and user creation/update
* @param {string} pubkey - User's public key
* @returns {Promise<Object|null>} User object or null if failed
*/
const syncNostrProfile = async (pubkey) => {
2024-08-12 17:27:47 -05:00
await ndk.connect();
const user = ndk.getUser({ pubkey });
try {
const profile = await user.fetchProfile();
2024-12-08 17:24:08 -06:00
const fields = await findKind0Fields(profile);
let dbUser = await getUserByPubkey(pubkey);
2024-10-04 16:41:49 -05:00
if (dbUser) {
2024-12-08 17:24:08 -06:00
// Update existing user if kind0 fields differ
if (fields.avatar !== dbUser.avatar || fields.username !== dbUser.username) {
const updates = {
...(fields.avatar !== dbUser.avatar && { avatar: fields.avatar }),
...(fields.username !== dbUser.username && {
username: fields.username,
name: fields.username
})
};
await updateUser(dbUser.id, updates);
dbUser = await getUserByPubkey(pubkey);
2024-10-04 16:41:49 -05:00
}
} else {
2024-12-08 17:24:08 -06:00
// Create new user
const username = fields.username || pubkey.slice(0, 8);
const payload = {
pubkey,
username,
avatar: fields.avatar,
name: username
};
2024-08-12 17:27:47 -05:00
2024-12-08 17:24:08 -06:00
dbUser = await createUser(payload);
2024-10-04 16:41:49 -05:00
2024-12-08 17:24:08 -06:00
// Create author role if applicable
if (appConfig.authorPubkeys.includes(pubkey)) {
const role = await createRole({
userId: dbUser.id,
admin: true,
subscribed: false,
});
if (role) {
await updateUser(dbUser.id, { role: role.id });
dbUser = await getUserByPubkey(pubkey);
2024-10-04 16:41:49 -05:00
}
2024-08-12 17:27:47 -05:00
}
}
2024-12-08 17:24:08 -06:00
return { ...dbUser, kind0: fields };
2024-08-12 17:27:47 -05:00
} catch (error) {
2024-12-08 17:24:08 -06:00
console.error("Nostr profile sync error:", error);
return null;
2024-08-12 17:27:47 -05:00
}
2024-12-08 17:24:08 -06:00
};
/**
* Generates an ephemeral keypair for non-Nostr login methods
* @returns {Object} Object containing public and private keys
*/
const generateEphemeralKeypair = () => {
const privkey = generateSecretKey();
const pubkey = getPublicKey(privkey);
// pubkey is hex, privkey is bytes need to convert to hex
return {
pubkey,
privkey: bytesToHex(privkey)
};
};
2024-08-12 17:27:47 -05:00
export const authOptions = {
2024-09-06 12:32:23 -05:00
adapter: PrismaAdapter(prisma),
providers: [
2024-12-08 17:24:08 -06:00
// Nostr login provider
CredentialsProvider({
id: "nostr",
name: "Nostr",
2024-12-08 17:24:08 -06:00
credentials: {
pubkey: { label: "Public Key", type: "text" }
},
authorize: async (credentials) => {
if (!credentials?.pubkey) return null;
return await syncNostrProfile(credentials.pubkey);
}
}),
// Anonymous login provider
CredentialsProvider({
id: "anonymous",
name: "Anonymous",
credentials: {
pubkey: { label: "Public Key", type: "text" },
2024-12-08 17:24:08 -06:00
privkey: { label: "Private Key", type: "text" }
},
authorize: async (credentials) => {
2024-12-08 17:24:08 -06:00
const keys = (credentials?.pubkey && credentials?.pubkey !== 'null' && credentials?.privkey && credentials?.privkey !== 'null') ?
{ pubkey: credentials.pubkey, privkey: credentials.privkey } :
generateEphemeralKeypair();
let user = await getUserByPubkey(keys.pubkey);
if (!user) {
user = await createUser({
...keys,
username: `anon-${keys.pubkey.slice(0, 8)}`,
name: `anon-${keys.pubkey.slice(0, 8)}`
});
}
2024-12-08 17:24:08 -06:00
return { ...user, privkey: keys.privkey };
}
}),
2024-12-08 17:24:08 -06:00
// Email provider with simpler configuration
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-12-08 17:24:08 -06:00
from: process.env.EMAIL_FROM
2024-09-06 12:32:23 -05:00
}),
2024-12-08 17:24:08 -06:00
// Github provider with ephemeral keypair generation
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
profile: async (profile) => {
2024-12-08 17:24:08 -06:00
const keys = generateEphemeralKeypair();
return {
id: profile.id.toString(),
2024-12-08 17:24:08 -06:00
pubkey: keys.pubkey,
privkey: keys.privkey,
name: profile.login,
email: profile.email,
avatar: profile.avatar_url
2024-12-08 17:24:08 -06:00
};
}
2024-12-08 17:24:08 -06:00
})
],
callbacks: {
2024-12-08 17:24:08 -06:00
// Move email handling to the signIn callback
async signIn({ user, account, profile, email }) {
// Only handle email provider sign ins
if (account?.provider === "email") {
try {
// Check if this is an existing user
const existingUser = await getUserByEmail(user.email);
if (!existingUser && user) {
// First time login: generate keypair
const keys = generateEphemeralKeypair();
2024-12-08 17:24:08 -06:00
const newUser = {
pubkey: keys.pubkey,
privkey: keys.privkey,
username: user.email.split('@')[0],
email: user.email,
avatar: user.image,
name: user.email.split('@')[0],
}
// Update the user with the new keypair
const createdUser = await createUser(newUser);
console.log("Created user", createdUser);
return createdUser;
} else {
console.log("User already exists", existingUser);
}
2024-12-08 17:24:08 -06:00
return true;
} catch (error) {
console.error("Email sign in error:", error);
return false;
}
}
2024-12-08 17:24:08 -06:00
return true; // Allow other provider sign ins
2024-10-06 16:53:36 -05:00
},
2024-12-08 17:24:08 -06:00
async session({ session, user, token }) {
const userData = token.user || user;
if (userData) {
const fullUser = await getUserById(userData.id);
2024-12-08 17:24:08 -06:00
// Get the user's GitHub account if it exists
const githubAccount = await prisma.account.findFirst({
where: {
userId: fullUser.id,
provider: 'github'
}
});
session.user = {
2024-12-08 17:24:08 -06:00
...session.user,
id: fullUser.id,
pubkey: fullUser.pubkey,
privkey: fullUser.privkey,
role: fullUser.role,
username: fullUser.username,
avatar: fullUser.avatar,
name: fullUser.name,
userCourses: fullUser.userCourses,
userLessons: fullUser.userLessons,
purchased: fullUser.purchased,
nip05: fullUser.nip05,
lightningAddress: fullUser.lightningAddress
};
2024-12-08 17:24:08 -06:00
// Add GitHub account info to session if it exists
if (githubAccount) {
session.account = {
provider: githubAccount.provider,
type: githubAccount.type,
providerAccountId: githubAccount.providerAccountId,
access_token: githubAccount.access_token,
token_type: githubAccount.token_type,
scope: githubAccount.scope,
};
}
}
return session;
},
2024-12-09 12:43:33 -06:00
async jwt({ token, user, account, profile }) {
// If we are linking a github account to an existing email or anon account (we have privkey)
if (account?.provider === "github" && user?.id && user?.pubkey && user?.privkey) {
console.log("Linking GitHub account to existing user", account, profile);
try {
// First update the user's profile with GitHub info
const updatedUser = await updateUser(user.id, {
name: profile?.login || profile?.name,
username: profile?.login || profile?.name,
avatar: profile?.avatar_url,
image: profile?.avatar_url,
});
console.log("Updated user", updatedUser);
// Get the updated user
const existingUser = await getUserById(updatedUser?.id);
if (existingUser) {
token.user = existingUser;
}
} catch (error) {
console.error("Error linking GitHub account:", error);
}
}
// If we are linking a github account to a nostr account (we do not have privkey)
if (account?.provider === "github" && account?.userId && account?.pubkey) {
try {
// I think we just need auth + account in session and thats it?
} catch (error) {
console.error("Error linking GitHub account:", error);
}
}
2024-12-08 17:24:08 -06:00
if (user) {
token.user = user;
2024-10-08 19:24:40 -05:00
}
2024-12-08 17:24:08 -06:00
if (account) {
token.account = account;
}
return token;
}
},
2024-12-08 17:24:08 -06:00
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
2024-12-08 17:24:08 -06:00
debug: process.env.NODE_ENV === 'development',
};
export default NextAuth(authOptions);