2026-04-28 20:56:06 -04:00

164 lines
4.2 KiB
TypeScript

import { env } from "cloudflare:workers";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "@/db";
import { z } from "zod";
import { baseAuthConfig } from "@/lib/auth-config";
import { getOrCreateDefaultHostedOrganization } from "@/server/auth/default-hosted-organization";
import {
sendHostedPasswordResetEmail,
sendHostedVerificationEmail,
} from "@/server/email/loops";
const hostedBaseUrlSchema = z
.string()
.url()
.refine((value) => {
const url = new URL(value);
return (
url.protocol === "https:" ||
(url.protocol === "http:" && url.hostname === "localhost")
);
}, "BETTER_AUTH_URL must use https or localhost");
function createAuth() {
const baseUrl = getHostedBaseUrl();
const bypassEmail = Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true";
const auth = betterAuth({
baseURL: baseUrl,
secret: getHostedSecret(),
...baseAuthConfig,
emailAndPassword: {
...baseAuthConfig.emailAndPassword,
requireEmailVerification: !bypassEmail,
resetPasswordTokenExpiresIn: 60 * 60,
revokeSessionsOnPasswordReset: true,
sendResetPassword: async ({ user, url }) => {
await sendHostedPasswordResetEmail({
email: user.email,
resetUrl: url,
});
},
},
emailVerification: bypassEmail
? undefined
: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendHostedVerificationEmail({
email: user.email,
confirmationUrl: url,
});
},
},
trustedOrigins: getTrustedOrigins(baseUrl),
database: drizzleAdapter(db, {
provider: "sqlite",
}),
plugins: [...baseAuthConfig.plugins, tanstackStartCookies()],
databaseHooks: {
session: {
create: {
before: async (session) => {
// Inject Better Auth's createOrganization here so the helper can
// stay reusable without importing auth.ts and creating a cycle.
const organizationId = await getOrCreateDefaultHostedOrganization(
session.userId,
(body) => auth.api.createOrganization({ body }),
);
return {
data: {
...session,
activeOrganizationId: organizationId,
},
};
},
},
},
},
});
return auth;
}
let authInstance: ReturnType<typeof createAuth> | null = null;
function getTrustedOrigins(baseUrl: string) {
const trustedOrigins = [baseUrl];
if (process.env.NODE_ENV !== "production") {
trustedOrigins.push(
"http://open-seo.localhost:1355",
"http://*.open-seo.localhost:1355",
"https://open-seo.localhost:1355",
"https://*.open-seo.localhost:1355",
);
}
return trustedOrigins;
}
function getHostedBaseUrl() {
const baseUrl = env.BETTER_AUTH_URL?.trim();
if (!baseUrl) {
throw new Error("BETTER_AUTH_URL is required in hosted mode");
}
return hostedBaseUrlSchema.parse(baseUrl);
}
function getHostedSecret() {
const secret = env.BETTER_AUTH_SECRET?.trim();
if (!secret) {
throw new Error("BETTER_AUTH_SECRET is required in hosted mode");
}
if (secret.length < 32) {
throw new Error("BETTER_AUTH_SECRET must be at least 32 characters");
}
return secret;
}
function hasHostedAuthEmailConfig() {
const loopsVars = [
"LOOPS_API_KEY",
"LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID",
"LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID",
];
return loopsVars.every((name) => {
const value: unknown = Reflect.get(env, name);
return typeof value === "string" && value.trim() !== "";
});
}
export function hasHostedAuthConfig() {
try {
getHostedBaseUrl();
getHostedSecret();
return (
Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true" ||
hasHostedAuthEmailConfig()
);
} catch {
return false;
}
}
export function getAuth() {
if (authInstance) {
return authInstance;
}
authInstance = createAuth();
return authInstance;
}