164 lines
4.2 KiB
TypeScript
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;
|
|
}
|