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:
airano
2026-02-12 06:08:51 +03:30
commit 97b427831a
227 changed files with 48411 additions and 0 deletions

105
apps/web/lib/auth.ts Normal file
View 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
});