Files
website/src/lib/server/auth.ts
Stan Runge 54924b39f0
Some checks failed
Test and deploy staging / staging (push) Failing after 43s
feat: upgrade to SvelteKit 3 and adapter-bun
Run the sveltekit-3 migration and finish its manual tasks:

- Move kit config into the sveltekit() Vite plugin; drop svelte.config.js
- Replace $lib with #lib subpath imports
- Declare env vars in src/env.ts and import them from $app/env/{private,public}
- Consolidate param matchers into src/params.ts
- Use error(status, message), Response.json, refreshAll and goto reset/shallow
- Fall back to a full page load when goto rejects a login return path

Switch from adapter-node to adapter-bun: the build script runs Vite under
Bun (the adapter requires it) and the image starts with `bun ./build`.
Runtime env names (PORT, BODY_SIZE_LIMIT, ADDRESS_HEADER, ...) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:17:30 +00:00

97 lines
3.1 KiB
TypeScript

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)
]
});