Initial release v1.0.0
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>
This commit is contained in:
105
apps/web/lib/auth.ts
Normal file
105
apps/web/lib/auth.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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
|
||||
});
|
||||
Reference in New Issue
Block a user