brackt/app/lib/auth.server.ts
Chris Parsons 5268e07365
Migrate auth from Clerk to BetterAuth (#354)
* Fix BetterAuth field mapping and add owner-prefixed team names

- Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth
  adapter (was snake_case, causing user inserts to fail); add
  generateId: "uuid" so PostgreSQL UUID columns accept generated IDs
- Add prependOwnerToTeamName / stripOwnerFromTeamName utilities
- Invite join, league creation, and settings assign/remove all now
  manage the owner-prefix on team names atomically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Complete BetterAuth migration: auth flows, rename, and cleanup

- Add /forgot-password and /reset-password pages (full password reset flow)
- Add 'Forgot password?' link on login page
- Fix register.tsx: add explicit window.location fallback after signUp
- Rename commissionerAuditLog.actorClerkId → actorUserId (migration 0084)
  and update all references in model, routes, components, and tests
- Rewrite docs/agents/auth.md to document BetterAuth (removes all Clerk refs)
- Update test fixtures and schema comments to remove Clerk ID references;
  use UUID-format IDs throughout test data
- Update docs/agents/domain-models.md ownerId description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix BetterAuth ID generation, clean up review issues

- Set generateId: false so BetterAuth omits id from inserts; add
  $defaultFn(crypto.randomUUID) to accounts/sessions/verifications
  so Drizzle fills it in (fixes null id constraint violation on signup)
- Move renameTeam to static import; rename before removeTeamOwner so
  a failed rename leaves owner intact rather than leaving a stale prefix
- Clarify stripOwnerFromTeamName toTitleCase assumption in comment
- Annotate reset-password token fallback to explain loader guarantee

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 10:03:50 -07:00

69 lines
2.1 KiB
TypeScript

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import bcrypt from "bcrypt";
import { Resend } from "resend";
import { db } from "~/server/db";
if (!process.env.BETTER_AUTH_SECRET) {
throw new Error("BETTER_AUTH_SECRET is required");
}
const resend = new Resend(process.env.RESEND_API_KEY);
const googleProvider = process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
? { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET } }
: {};
const discordProvider = process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET
? { discord: { clientId: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET } }
: {};
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
usePlural: true,
}),
advanced: {
database: {
generateId: false,
},
},
user: {
fields: {
name: "displayName",
image: "imageUrl",
},
additionalFields: {
firstName: { type: "string", required: false, fieldName: "firstName" },
lastName: { type: "string", required: false, fieldName: "lastName" },
username: { type: "string", required: false, fieldName: "username" },
isAdmin: { type: "boolean", defaultValue: false, fieldName: "isAdmin" },
},
},
emailAndPassword: {
enabled: true,
password: {
hash: (password: string) => bcrypt.hash(password, 10),
verify: ({ hash, password }: { hash: string; password: string }) =>
bcrypt.compare(password, hash),
},
sendResetPassword: async ({ user, url }) => {
await resend.emails.send({
from: "Brackt <noreply@brackt.com>",
to: user.email,
subject: "Reset your Brackt password",
html: `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${url}">${url}</a></p>`,
});
},
},
socialProviders: {
...googleProvider,
...discordProvider,
},
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "discord"],
},
},
});