Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
import NextAuth from 'next-auth';
|
|
import GitHub from 'next-auth/providers/github';
|
|
import { createDb, userQueries } from '@skillhub/db';
|
|
import { sendWelcomeEmail } from './email';
|
|
|
|
// Determine secure cookie prefix based on AUTH_URL protocol
|
|
const useSecureCookies = process.env.AUTH_URL?.startsWith('https://') ?? process.env.NODE_ENV === 'production';
|
|
const cookiePrefix = useSecureCookies ? '__Secure-' : '';
|
|
|
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
providers: [
|
|
GitHub({
|
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
// Custom profile to prevent "Cannot read properties of undefined (reading 'toString')"
|
|
// when GitHub API returns unexpected data (e.g. after a failed token exchange)
|
|
profile(profile) {
|
|
return {
|
|
id: String(profile.id ?? ''),
|
|
name: (profile.name ?? profile.login) as string,
|
|
email: profile.email as string | null,
|
|
image: profile.avatar_url as string | null,
|
|
};
|
|
},
|
|
}),
|
|
],
|
|
trustHost: true, // Trust Host header from reverse proxy (Coolify, Nginx, etc.)
|
|
session: { strategy: 'jwt' },
|
|
// Explicit cookie config to ensure PKCE works behind reverse proxies
|
|
cookies: {
|
|
pkceCodeVerifier: {
|
|
name: `${cookiePrefix}authjs.pkce.code_verifier`,
|
|
options: {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
path: '/',
|
|
secure: useSecureCookies,
|
|
maxAge: 900, // 15 minutes
|
|
},
|
|
},
|
|
},
|
|
callbacks: {
|
|
async signIn({ profile }) {
|
|
if (!profile?.id) return false;
|
|
|
|
// On mirror servers the database is a read-only replica,
|
|
// so skip the upsert. The user session still works via JWT.
|
|
const isPrimary = process.env.IS_PRIMARY_SERVER !== 'false';
|
|
if (isPrimary) {
|
|
try {
|
|
const db = createDb();
|
|
|
|
// Check if user already exists (to detect first login)
|
|
const existingUser = await userQueries.getByGithubId(db, String(profile.id));
|
|
|
|
// Check if user is in the admin list
|
|
const adminUsers = (process.env.ADMIN_GITHUB_USERS || '')
|
|
.split(',')
|
|
.map((u) => u.trim().toLowerCase())
|
|
.filter(Boolean);
|
|
const isAdmin = adminUsers.includes((profile.login as string).toLowerCase());
|
|
|
|
await userQueries.upsertFromGithub(db, {
|
|
githubId: String(profile.id),
|
|
username: profile.login as string,
|
|
displayName: profile.name as string | undefined,
|
|
email: profile.email as string | undefined,
|
|
avatarUrl: profile.avatar_url as string | undefined,
|
|
isAdmin,
|
|
});
|
|
|
|
// For new users with an email, send welcome/onboarding email
|
|
// (no auto-subscribe to newsletter — user can opt-in via link in the email)
|
|
if (!existingUser && profile.email) {
|
|
const email = (profile.email as string).toLowerCase().trim();
|
|
sendWelcomeEmail(email, 'en', profile.login as string).catch((err) => {
|
|
console.error('[Auth] Failed to send welcome email:', err);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Database error during sign-in (allowing login anyway):', err);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
async jwt({ token, profile }) {
|
|
if (profile) {
|
|
token.githubId = String(profile.id);
|
|
token.username = profile.login;
|
|
token.avatarUrl = profile.avatar_url;
|
|
}
|
|
return token;
|
|
},
|
|
async session({ session, token }) {
|
|
if (session.user) {
|
|
session.user.githubId = token.githubId as string;
|
|
session.user.username = token.username as string;
|
|
session.user.avatarUrl = token.avatarUrl as string;
|
|
}
|
|
return session;
|
|
},
|
|
},
|
|
// Use default NextAuth pages - no custom pages needed
|
|
});
|