import { BETTER_AUTH_SECRET, BETTER_AUTH_URL, ORIGIN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, OSU_CLIENT_ID, OSU_CLIENT_SECRET } from '$app/env/private'; import { building, dev } from '$app/env'; import { betterAuth } from 'better-auth/minimal'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { genericOAuth } from 'better-auth/plugins'; import { APIError, createAuthMiddleware } from 'better-auth/api'; import { sveltekitCookies } from 'better-auth/svelte-kit'; import { getRequestEvent } from '$app/server'; import { db } from '#lib/server/db/index.js'; import * as schema from '#lib/server/db/schema/index.js'; import { osuProfile } from './osu-profile'; export { isAdmin, requireAdmin, requireUser } from './authorization'; if (!building && (!BETTER_AUTH_SECRET || BETTER_AUTH_SECRET.length < 32)) { throw new Error('Set BETTER_AUTH_SECRET to a random secret of at least 32 characters'); } if (!building && !dev && !BETTER_AUTH_URL && !ORIGIN) { throw new Error('Set BETTER_AUTH_URL to the public site origin'); } export const authProviders = { google: !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET), osu: !!(OSU_CLIENT_ID && OSU_CLIENT_SECRET) }; export const auth = betterAuth({ baseURL: BETTER_AUTH_URL || ORIGIN || 'http://localhost:5173', secret: BETTER_AUTH_SECRET || 'build-only-placeholder-not-a-runtime-secret', database: drizzleAdapter(db, { provider: 'pg', schema }), emailAndPassword: { enabled: true, minPasswordLength: 12, maxPasswordLength: 128 }, user: { additionalFields: { role: { type: 'string', required: false, defaultValue: 'user', input: false } } }, account: { accountLinking: { enabled: true, disableImplicitLinking: true, allowDifferentEmails: true, trustedProviders: ['osu'] } }, rateLimit: { enabled: true }, socialProviders: authProviders.google ? { google: { clientId: GOOGLE_CLIENT_ID!, clientSecret: GOOGLE_CLIENT_SECRET! } } : {}, hooks: { before: createAuthMiddleware(async (ctx) => { // osu! has no email scope. Reserve its internal identifiers against credential signup. if ( ctx.path === '/sign-up/email' && typeof ctx.body?.email === 'string' && ctx.body.email.toLowerCase().endsWith('.invalid') ) { throw new APIError('BAD_REQUEST', { message: 'Please use a deliverable email address' }); } }) }, plugins: [ genericOAuth({ config: authProviders.osu ? [ { providerId: 'osu', clientId: OSU_CLIENT_ID!, clientSecret: OSU_CLIENT_SECRET!, authorizationUrl: 'https://osu.ppy.sh/oauth/authorize', tokenUrl: 'https://osu.ppy.sh/oauth/token', scopes: ['identify'], getUserInfo: async (tokens) => { const response = await fetch('https://osu.ppy.sh/api/v2/me', { headers: { Authorization: `Bearer ${tokens.accessToken}`, Accept: 'application/json' }, signal: AbortSignal.timeout(10000) }); if (!response.ok) throw new Error('Could not load osu! profile'); return osuProfile(await response.json()); } } ] : [] }), sveltekitCookies(getRequestEvent) ] });