feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
255
packages/auth/src/server.ts
Normal file
255
packages/auth/src/server.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { expo } from "@better-auth/expo";
|
||||
import { passkey } from "@better-auth/passkey";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { betterAuth } from "better-auth/minimal";
|
||||
import { nextCookies } from "better-auth/next-js";
|
||||
import {
|
||||
anonymous,
|
||||
magicLink,
|
||||
twoFactor,
|
||||
organization,
|
||||
admin,
|
||||
lastLoginMethod,
|
||||
} from "better-auth/plugins";
|
||||
|
||||
import * as schema from "@turbostarter/db/schema";
|
||||
import { creditTransaction, customer } from "@turbostarter/db/schema";
|
||||
import { db } from "@turbostarter/db/server";
|
||||
import { EmailTemplate } from "@turbostarter/email";
|
||||
import { sendEmail } from "@turbostarter/email/server";
|
||||
import { getLocaleFromRequest } from "@turbostarter/i18n/server";
|
||||
import { NodeEnv } from "@turbostarter/shared/constants";
|
||||
import { logger } from "@turbostarter/shared/logger";
|
||||
import { generateId } from "@turbostarter/shared/utils";
|
||||
|
||||
import { env } from "./env";
|
||||
import { getUrl } from "./lib/utils";
|
||||
import { AuthProvider, SocialProvider, VerificationType } from "./types";
|
||||
|
||||
/**
|
||||
* Credits for new free-tier users.
|
||||
* Configurable via FREE_TIER_CREDITS env var. Defaults: 10000 (dev), 100 (prod).
|
||||
*/
|
||||
const FREE_TIER_CREDITS =
|
||||
env.FREE_TIER_CREDITS ??
|
||||
(env.NODE_ENV === NodeEnv.DEVELOPMENT ? 10000 : 100);
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "TurboStarter",
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // 5 minutes
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteUser: {
|
||||
enabled: true,
|
||||
sendDeleteAccountVerification: async ({ user, url }, request) =>
|
||||
sendEmail({
|
||||
to: user.email,
|
||||
template: EmailTemplate.DELETE_ACCOUNT,
|
||||
locale: getLocaleFromRequest(request),
|
||||
variables: {
|
||||
url: getUrl({
|
||||
request,
|
||||
url,
|
||||
type: VerificationType.DELETE_ACCOUNT,
|
||||
}).toString(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
changeEmail: {
|
||||
enabled: true,
|
||||
updateEmailWithoutVerification: true,
|
||||
sendChangeEmailConfirmation: async ({ user, newEmail, url }, request) =>
|
||||
sendEmail({
|
||||
to: user.email,
|
||||
template: EmailTemplate.CHANGE_EMAIL,
|
||||
locale: getLocaleFromRequest(request),
|
||||
variables: {
|
||||
url: getUrl({
|
||||
request,
|
||||
url,
|
||||
type: VerificationType.CONFIRM_EMAIL,
|
||||
}).toString(),
|
||||
newEmail,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
trustedOrigins: [
|
||||
"chrome-extension://",
|
||||
"turbostarter://",
|
||||
/* Needed only for Apple ID authentication */
|
||||
"https://appleid.apple.com",
|
||||
...(env.NODE_ENV === NodeEnv.DEVELOPMENT
|
||||
? ["http://localhost*", "https://localhost*"]
|
||||
: []),
|
||||
...(process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") ?? []),
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
sendResetPassword: async ({ user, url }, request) =>
|
||||
sendEmail({
|
||||
to: user.email,
|
||||
template: EmailTemplate.RESET_PASSWORD,
|
||||
locale: getLocaleFromRequest(request),
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
}),
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
sendVerificationEmail: async ({ user, url }, request) =>
|
||||
sendEmail({
|
||||
to: user.email,
|
||||
template: EmailTemplate.CONFIRM_EMAIL,
|
||||
locale: getLocaleFromRequest(request),
|
||||
variables: {
|
||||
url: getUrl({
|
||||
request,
|
||||
url,
|
||||
type: VerificationType.CONFIRM_EMAIL,
|
||||
}).toString(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema,
|
||||
}),
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
after: async (user) => {
|
||||
// Auto-create customer record with free credits on signup
|
||||
const customerId = generateId();
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(customer).values({
|
||||
id: customerId,
|
||||
userId: user.id,
|
||||
customerId: `free_${user.id}`,
|
||||
status: "active",
|
||||
plan: "free",
|
||||
credits: FREE_TIER_CREDITS,
|
||||
});
|
||||
|
||||
await tx.insert(creditTransaction).values({
|
||||
id: generateId(),
|
||||
customerId,
|
||||
amount: FREE_TIER_CREDITS,
|
||||
type: "signup",
|
||||
reason: "Welcome credits for new user",
|
||||
balanceAfter: FREE_TIER_CREDITS,
|
||||
});
|
||||
});
|
||||
logger.info(`Created customer with ${FREE_TIER_CREDITS} credits for user ${user.id}`);
|
||||
} catch (error) {
|
||||
// Log but don't fail user creation if customer creation fails
|
||||
logger.error("Failed to create customer for user", { userId: user.id, error });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url }, ctx) =>
|
||||
sendEmail({
|
||||
to: email,
|
||||
template: EmailTemplate.MAGIC_LINK,
|
||||
locale: getLocaleFromRequest(ctx?.request),
|
||||
variables: {
|
||||
url: getUrl({
|
||||
request: ctx?.request,
|
||||
url,
|
||||
type: VerificationType.MAGIC_LINK,
|
||||
}).toString(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
passkey(),
|
||||
twoFactor(),
|
||||
anonymous(),
|
||||
admin(),
|
||||
organization({
|
||||
sendInvitationEmail: async (
|
||||
{ invitation, inviter, organization },
|
||||
request,
|
||||
) => {
|
||||
const url = getUrl({
|
||||
request,
|
||||
});
|
||||
url.searchParams.set("invitationId", invitation.id);
|
||||
url.searchParams.set("email", invitation.email);
|
||||
|
||||
return sendEmail({
|
||||
to: invitation.email,
|
||||
template: EmailTemplate.ORGANIZATION_INVITATION,
|
||||
locale: getLocaleFromRequest(request),
|
||||
variables: {
|
||||
url: url.toString(),
|
||||
inviter: inviter.user.name,
|
||||
organization: organization.name,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
lastLoginMethod({
|
||||
customResolveMethod: (ctx) => {
|
||||
switch (ctx.path) {
|
||||
case "/magic-link/verify":
|
||||
return AuthProvider.MAGIC_LINK;
|
||||
case "/passkey/verify-authentication":
|
||||
return AuthProvider.PASSKEY;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
expo(),
|
||||
nextCookies(),
|
||||
],
|
||||
socialProviders: {
|
||||
[SocialProvider.APPLE]: {
|
||||
clientId: env.APPLE_CLIENT_ID,
|
||||
clientSecret: env.APPLE_CLIENT_SECRET,
|
||||
appBundleIdentifier: env.APPLE_APP_BUNDLE_IDENTIFIER,
|
||||
},
|
||||
[SocialProvider.GOOGLE]: {
|
||||
clientId: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
[SocialProvider.GITHUB]: {
|
||||
clientId: env.GITHUB_CLIENT_ID,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
cookiePrefix: "turbostarter",
|
||||
cookies: {
|
||||
state: {
|
||||
attributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
log: (level, ...args) => logger[level](...args),
|
||||
},
|
||||
});
|
||||
|
||||
export type AuthErrorCode = keyof typeof auth.$ERROR_CODES;
|
||||
export type Session = typeof auth.$Infer.Session;
|
||||
export type User = Session["user"];
|
||||
export type Invitation = typeof auth.$Infer.Invitation;
|
||||
export type Organization = typeof auth.$Infer.Organization;
|
||||
export type ActiveOrganization = typeof auth.$Infer.ActiveOrganization;
|
||||
export type Member = typeof auth.$Infer.Member;
|
||||
Reference in New Issue
Block a user