Files
Reactive-Resume/src/integrations/auth/config.ts
T
2026-01-23 09:30:17 +01:00

210 lines
6.4 KiB
TypeScript

import { BetterAuthError } from "@better-auth/core/error";
import { passkey } from "@better-auth/passkey";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { apiKey, type GenericOAuthConfig, genericOAuth, twoFactor } from "better-auth/plugins";
import { username } from "better-auth/plugins/username";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
import { hashPassword, verifyPassword } from "@/utils/password";
import { generateId, toUsername } from "@/utils/string";
import { schema } from "../drizzle";
import { sendEmail } from "../email/service";
function isCustomOAuthProviderEnabled() {
const hasDiscovery = Boolean(env.OAUTH_DISCOVERY_URL);
const hasManual =
Boolean(env.OAUTH_AUTHORIZATION_URL) && Boolean(env.OAUTH_TOKEN_URL) && Boolean(env.OAUTH_USER_INFO_URL);
return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
}
const getAuthConfig = () => {
const authConfigs: GenericOAuthConfig[] = [];
if (isCustomOAuthProviderEnabled()) {
authConfigs.push({
providerId: "custom",
disableSignUp: env.FLAG_DISABLE_SIGNUPS,
clientId: env.OAUTH_CLIENT_ID as string,
clientSecret: env.OAUTH_CLIENT_SECRET as string,
discoveryUrl: env.OAUTH_DISCOVERY_URL,
authorizationUrl: env.OAUTH_AUTHORIZATION_URL,
tokenUrl: env.OAUTH_TOKEN_URL,
userInfoUrl: env.OAUTH_USER_INFO_URL,
scopes: env.OAUTH_SCOPES,
redirectURI: `${env.APP_URL}/api/auth/oauth2/callback/custom`,
mapProfileToUser: async (profile) => {
if (!profile.email) {
throw new BetterAuthError(
"OAuth Provider did not return an email address. This is required for user creation.",
{ cause: "EMAIL_REQUIRED" },
);
}
const email = profile.email;
const name = profile.name ?? profile.preferred_username ?? email.split("@")[0];
const username = profile.preferred_username ?? email.split("@")[0];
const image = profile.image ?? profile.picture ?? profile.avatar_url;
return {
name,
email,
image,
username,
displayUsername: username,
emailVerified: true,
};
},
} satisfies GenericOAuthConfig);
}
return betterAuth({
appName: "Reactive Resume",
baseURL: env.APP_URL,
secret: env.AUTH_SECRET,
database: drizzleAdapter(db, { schema, provider: "pg" }),
telemetry: { enabled: false },
trustedOrigins: [env.APP_URL],
advanced: {
database: { generateId },
useSecureCookies: env.APP_URL.startsWith("https://"),
},
emailAndPassword: {
enabled: !env.FLAG_DISABLE_EMAIL_AUTH,
autoSignIn: true,
minPasswordLength: 6,
maxPasswordLength: 64,
requireEmailVerification: false,
disableSignUp: env.FLAG_DISABLE_SIGNUPS || env.FLAG_DISABLE_EMAIL_AUTH,
sendResetPassword: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
text: `To reset your password, please visit the following URL: ${url}. If you did not request a password reset, please ignore this email.`,
});
},
password: {
hash: (password) => hashPassword(password),
verify: ({ password, hash }) => verifyPassword(password, hash),
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Verify your email",
text: `You recently signed up for an account on Reactive Resume.\nTo verify your email, please visit the following URL: ${url}`,
});
},
},
user: {
changeEmail: {
enabled: true,
sendChangeEmailVerification: async ({ user, newEmail, url }) => {
await sendEmail({
to: newEmail,
subject: "Verify your new email",
text: `You recently requested to change your email on Reactive Resume from ${user.email} to ${newEmail}.\nTo verify this change, please visit the following URL: ${url}\nIf you did not request this change, please ignore this email.`,
});
},
},
additionalFields: {
username: {
type: "string",
required: true,
},
},
},
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"],
},
},
socialProviders: {
google: {
enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET,
disableSignUp: env.FLAG_DISABLE_SIGNUPS,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientId: env.GOOGLE_CLIENT_ID!,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientSecret: env.GOOGLE_CLIENT_SECRET!,
mapProfileToUser: async (profile) => {
return {
name: profile.name,
email: profile.email,
image: profile.picture,
username: profile.email.split("@")[0],
displayUsername: profile.email.split("@")[0],
emailVerified: true,
};
},
},
github: {
enabled: !!env.GITHUB_CLIENT_ID && !!env.GITHUB_CLIENT_SECRET,
disableSignUp: env.FLAG_DISABLE_SIGNUPS,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientId: env.GITHUB_CLIENT_ID!,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientSecret: env.GITHUB_CLIENT_SECRET!,
mapProfileToUser: async (profile) => {
return {
name: profile.name,
email: profile.email,
image: profile.avatar_url,
username: profile.login,
displayUsername: profile.login,
emailVerified: true,
};
},
},
},
plugins: [
apiKey({
enableSessionForAPIKeys: true,
rateLimit: {
enabled: true,
timeWindow: 1000 * 60 * 60 * 24, // 1 day
maxRequests: 500, // 500 requests per day
},
}),
username({
minUsernameLength: 3,
maxUsernameLength: 64,
usernameNormalization: (value) => toUsername(value),
displayUsernameNormalization: (value) => toUsername(value),
usernameValidator: (username) => /^[a-z0-9._-]+$/.test(username),
validationOrder: { username: "post-normalization", displayUsername: "post-normalization" },
}),
twoFactor({
issuer: "Reactive Resume",
}),
passkey({
origin: env.APP_URL,
rpName: "Reactive Resume",
rpID: new URL(env.APP_URL).hostname,
}),
genericOAuth({
config: authConfigs,
}),
tanstackStartCookies(),
],
});
};
export const auth = getAuthConfig();