125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
import * as CONSTANTS from './constants';
|
|
import * as schema from '@db/schema';
|
|
|
|
import { admin, anonymous } from 'better-auth/plugins';
|
|
import { sendChangeEmailConfirmation, sendVerificationEmail } from './auth/email-verification';
|
|
|
|
import { betterAuth } from 'better-auth/minimal';
|
|
import { createProfileHook } from './auth/hooks/create-profile';
|
|
import { db } from '@db/drizzle';
|
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
import { config as emailAndPassword } from './auth/email-password';
|
|
import { loginLink } from './auth/plugins/login-link';
|
|
import { oAuthProxy } from 'better-auth/plugins/oauth-proxy';
|
|
import { resolveTrustedBrowserOrigin } from './utils';
|
|
import { sso } from '@better-auth/sso';
|
|
import { syncUserWithProfile } from './auth/hooks/sync-user';
|
|
import { tokenExchange } from './auth/plugins/token-exchange';
|
|
import { trackLoginHook } from './auth/hooks/track-login';
|
|
|
|
export const auth: ReturnType<typeof betterAuth> = betterAuth({
|
|
baseURL: CONSTANTS.BASE_URL,
|
|
database: drizzleAdapter(db, {
|
|
provider: 'pg',
|
|
schema
|
|
// debugLogs: true
|
|
}),
|
|
emailAndPassword: emailAndPassword,
|
|
user: {
|
|
changeEmail: {
|
|
enabled: true,
|
|
sendChangeEmailConfirmation
|
|
}
|
|
},
|
|
emailVerification: {
|
|
enabled: true,
|
|
sendVerificationEmail
|
|
},
|
|
socialProviders: {
|
|
google: {
|
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
accessType: 'offline',
|
|
prompt: 'select_account consent'
|
|
}
|
|
},
|
|
trustedOrigins: (request) => {
|
|
const origins = [...CONSTANTS.TRUSTED_ORIGINS];
|
|
const originHeader = request?.headers.get('origin');
|
|
const resolved = resolveTrustedBrowserOrigin(originHeader, CONSTANTS.TRUSTED_ORIGINS);
|
|
|
|
if (resolved && !origins.includes(resolved)) {
|
|
origins.push(resolved);
|
|
}
|
|
|
|
return origins;
|
|
},
|
|
advanced: {
|
|
cookiePrefix: 'classroomio',
|
|
crossSubDomainCookies: { enabled: false },
|
|
database: {
|
|
generateId: false
|
|
}
|
|
},
|
|
account: {
|
|
storeAccountCookie: true
|
|
},
|
|
session: {
|
|
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
|
updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
|
|
cookieCache: {
|
|
enabled: true,
|
|
maxAge: 60 * 60 // 1 hour
|
|
}
|
|
},
|
|
databaseHooks: {
|
|
user: {
|
|
create: {
|
|
after: async (user, ctx) => {
|
|
console.log('[auth] databaseHooks.user.create.after: running', { userId: user.id, ctx });
|
|
const request =
|
|
(ctx as { context?: { request?: Request }; request?: Request } | undefined)?.context?.request ??
|
|
(ctx as { request?: Request } | undefined)?.request;
|
|
await createProfileHook(user, request);
|
|
}
|
|
},
|
|
update: {
|
|
after: async (user) => {
|
|
console.log('[auth] databaseHooks.user.update.after: running', { userId: user.id });
|
|
await syncUserWithProfile(user);
|
|
}
|
|
}
|
|
},
|
|
session: {
|
|
create: {
|
|
after: async (session) => {
|
|
await trackLoginHook(session);
|
|
}
|
|
},
|
|
update: {
|
|
after: async (session) => {
|
|
await trackLoginHook(session);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [
|
|
admin(),
|
|
anonymous(),
|
|
sso({
|
|
// OIDC providers are registered dynamically per organization
|
|
// via the admin API (auth.api.registerSSOProvider)
|
|
}),
|
|
// Lets OAuth/SSO callbacks land on the canonical production URL
|
|
// (api.classroomio.com) while still completing the flow on the tenant
|
|
// host the user signed in from (<org>.classroomio.school or a BYOD
|
|
// domain). Without this, the state cookie set on the tenant host isn't
|
|
// sent to the api host on the Google redirect → state_security_mismatch.
|
|
oAuthProxy({
|
|
productionURL: CONSTANTS.BASE_URL
|
|
}),
|
|
loginLink(),
|
|
tokenExchange()
|
|
]
|
|
});
|