2026-04-23 22:09:25 -07:00
|
|
|
import { useState, useEffect, useCallback, useRef } from "react";
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
import { authClient } from "~/lib/auth-client";
|
2026-04-23 22:09:25 -07:00
|
|
|
import type { getDraftPicksForSeason } from "~/models/draft-pick";
|
|
|
|
|
import type { findSeasonWithTeamsAndLeague } from "~/models/season";
|
|
|
|
|
import type { getSeasonTimers } from "~/models/draft-timer";
|
|
|
|
|
import type { getSeasonAutodraftSettings } from "~/models/autodraft-settings";
|
|
|
|
|
import type { AutodraftStatusEntry, QueueItem } from "~/hooks/useDraftRoomState";
|
|
|
|
|
|
|
|
|
|
type DraftPicks = Awaited<ReturnType<typeof getDraftPicksForSeason>>;
|
|
|
|
|
type Season = NonNullable<Awaited<ReturnType<typeof findSeasonWithTeamsAndLeague>>>;
|
|
|
|
|
type Timers = Awaited<ReturnType<typeof getSeasonTimers>>;
|
|
|
|
|
type AutodraftSettings = Awaited<ReturnType<typeof getSeasonAutodraftSettings>>;
|
|
|
|
|
|
|
|
|
|
interface UseDraftAuthRecoveryParams {
|
|
|
|
|
reconnectCount: number;
|
|
|
|
|
revalidate: () => void;
|
|
|
|
|
revalidatorState: "idle" | "loading" | "submitting";
|
|
|
|
|
currentUserId: string | null | undefined;
|
|
|
|
|
userAutodraftSettings: { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean } | null;
|
|
|
|
|
// Loader data for revalidation sync
|
|
|
|
|
draftPicks: DraftPicks;
|
|
|
|
|
season: Season;
|
|
|
|
|
userQueue: QueueItem[];
|
|
|
|
|
timers: Timers;
|
|
|
|
|
autodraftSettings: AutodraftSettings;
|
|
|
|
|
// State setters to sync after revalidation
|
|
|
|
|
setPicks: (picks: DraftPicks) => void;
|
|
|
|
|
setCurrentPick: (pick: number) => void;
|
|
|
|
|
setIsPaused: (paused: boolean) => void;
|
|
|
|
|
setIsDraftComplete: (complete: boolean) => void;
|
|
|
|
|
setQueue: (queue: QueueItem[]) => void;
|
|
|
|
|
setTeamTimers: (fn: (prev: Record<string, number>) => Record<string, number>) => void;
|
|
|
|
|
setAutodraftStatus: (fn: () => Record<string, AutodraftStatusEntry>) => void;
|
2026-04-30 20:33:00 -07:00
|
|
|
setIsSyncing: (value: boolean) => void;
|
2026-04-23 22:09:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function useDraftAuthRecovery({
|
|
|
|
|
reconnectCount,
|
|
|
|
|
revalidate,
|
|
|
|
|
revalidatorState,
|
|
|
|
|
currentUserId,
|
|
|
|
|
userAutodraftSettings,
|
|
|
|
|
draftPicks,
|
|
|
|
|
season,
|
|
|
|
|
userQueue,
|
|
|
|
|
timers,
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
setPicks,
|
|
|
|
|
setCurrentPick,
|
|
|
|
|
setIsPaused,
|
|
|
|
|
setIsDraftComplete,
|
|
|
|
|
setQueue,
|
|
|
|
|
setTeamTimers,
|
|
|
|
|
setAutodraftStatus,
|
2026-04-30 20:33:00 -07:00
|
|
|
setIsSyncing,
|
2026-04-23 22:09:25 -07:00
|
|
|
}: UseDraftAuthRecoveryParams) {
|
|
|
|
|
const [authDegraded, setAuthDegraded] = useState(false);
|
|
|
|
|
|
|
|
|
|
const [userAutodraft, setUserAutodraft] = useState({
|
|
|
|
|
isEnabled: userAutodraftSettings?.isEnabled || false,
|
|
|
|
|
mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on",
|
|
|
|
|
queueOnly: userAutodraftSettings?.queueOnly || false,
|
|
|
|
|
});
|
|
|
|
|
const userAutodraftRef = useRef(userAutodraft);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
userAutodraftRef.current = userAutodraft;
|
|
|
|
|
}, [userAutodraft]);
|
|
|
|
|
|
|
|
|
|
// Re-fetch loader data after each reconnect so stale picks/timers are refreshed.
|
|
|
|
|
const revalidationRetryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (reconnectCount > 0) {
|
|
|
|
|
revalidate();
|
|
|
|
|
if (revalidationRetryRef.current) {
|
|
|
|
|
clearTimeout(revalidationRetryRef.current);
|
|
|
|
|
}
|
|
|
|
|
revalidationRetryRef.current = setTimeout(() => {
|
|
|
|
|
revalidate();
|
|
|
|
|
revalidationRetryRef.current = null;
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
return () => {
|
|
|
|
|
if (revalidationRetryRef.current) {
|
|
|
|
|
clearTimeout(revalidationRetryRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [reconnectCount, revalidate]);
|
|
|
|
|
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
// Re-check session when the tab becomes visible — cookie-based sessions don't
|
|
|
|
|
// need manual token refresh, but a long-idle tab may have an expired session.
|
2026-04-23 22:09:25 -07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (authDegraded) return;
|
|
|
|
|
|
|
|
|
|
let aborted = false;
|
|
|
|
|
let inFlight = false;
|
|
|
|
|
|
|
|
|
|
const handleVisibilityChange = async () => {
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
if (document.visibilityState !== "visible" || inFlight) return;
|
2026-04-23 22:09:25 -07:00
|
|
|
inFlight = true;
|
|
|
|
|
try {
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
const { data: session } = await authClient.getSession();
|
2026-04-23 22:09:25 -07:00
|
|
|
if (aborted) return;
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
if (session) {
|
|
|
|
|
if (!currentUserId) revalidate();
|
2026-04-23 22:09:25 -07:00
|
|
|
} else {
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
setAuthDegraded(true);
|
2026-04-23 22:09:25 -07:00
|
|
|
}
|
|
|
|
|
} catch {
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
if (!aborted) setAuthDegraded(true);
|
2026-04-23 22:09:25 -07:00
|
|
|
} finally {
|
|
|
|
|
inFlight = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
|
2026-04-23 22:09:25 -07:00
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
|
|
|
return () => {
|
|
|
|
|
aborted = true;
|
|
|
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
|
|
|
};
|
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
|
|
|
}, [currentUserId, authDegraded, revalidate]);
|
2026-04-23 22:09:25 -07:00
|
|
|
|
2026-04-24 23:27:52 -07:00
|
|
|
// Periodically ping the session endpoint to keep it alive during long drafts.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (authDegraded) return;
|
|
|
|
|
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
|
|
|
|
const id = setInterval(async () => {
|
|
|
|
|
const { data: session } = await authClient.getSession();
|
|
|
|
|
if (!session) setAuthDegraded(true);
|
|
|
|
|
}, FIFTEEN_MINUTES);
|
|
|
|
|
return () => clearInterval(id);
|
|
|
|
|
}, [authDegraded]);
|
|
|
|
|
|
2026-04-23 22:09:25 -07:00
|
|
|
// Track revalidation lifecycle to sync local state from fresh loader data.
|
|
|
|
|
const revalidatorStateRef = useRef(revalidatorState);
|
|
|
|
|
const isRevalidatingRef = useRef(false);
|
|
|
|
|
const pendingPicksDuringRevalidationRef = useRef<DraftPicks>([]);
|
|
|
|
|
const draftPicksAtRevalidationStartRef = useRef(draftPicks);
|
|
|
|
|
const userQueueAtRevalidationStartRef = useRef(userQueue);
|
|
|
|
|
const pendingQueueMutationsRef = useRef(0);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const prev = revalidatorStateRef.current;
|
|
|
|
|
revalidatorStateRef.current = revalidatorState;
|
|
|
|
|
|
|
|
|
|
if (prev !== "loading" && revalidatorState === "loading") {
|
|
|
|
|
isRevalidatingRef.current = true;
|
|
|
|
|
pendingPicksDuringRevalidationRef.current = [];
|
|
|
|
|
draftPicksAtRevalidationStartRef.current = draftPicks;
|
|
|
|
|
userQueueAtRevalidationStartRef.current = userQueue;
|
|
|
|
|
} else if (prev === "loading" && revalidatorState === "idle") {
|
|
|
|
|
isRevalidatingRef.current = false;
|
|
|
|
|
|
|
|
|
|
if (draftPicks === draftPicksAtRevalidationStartRef.current) {
|
|
|
|
|
pendingPicksDuringRevalidationRef.current = [];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dbPickIds = new Set(draftPicks.map((p) => p.id));
|
|
|
|
|
const missedPicks = pendingPicksDuringRevalidationRef.current.filter(
|
|
|
|
|
(p) => !dbPickIds.has(p.id)
|
|
|
|
|
);
|
|
|
|
|
pendingPicksDuringRevalidationRef.current = [];
|
|
|
|
|
|
|
|
|
|
setPicks([...draftPicks, ...missedPicks]);
|
|
|
|
|
setCurrentPick(season.currentPickNumber || 1);
|
|
|
|
|
setIsPaused(season.draftPaused || false);
|
|
|
|
|
setIsDraftComplete(
|
|
|
|
|
season.status === "active" || season.status === "completed"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (currentUserId && userQueue !== userQueueAtRevalidationStartRef.current) {
|
|
|
|
|
setQueue(userQueue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (timers.length > 0) {
|
|
|
|
|
setTeamTimers((currentTimers) => {
|
|
|
|
|
const updated = { ...currentTimers };
|
|
|
|
|
timers.forEach((timer) => {
|
|
|
|
|
updated[timer.teamId] = timer.timeRemaining;
|
|
|
|
|
});
|
|
|
|
|
return updated;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAutodraftStatus(() => {
|
|
|
|
|
const status: Record<string, AutodraftStatusEntry> = {};
|
|
|
|
|
autodraftSettings.forEach((setting) => {
|
|
|
|
|
status[setting.teamId] = {
|
|
|
|
|
isEnabled: setting.isEnabled,
|
|
|
|
|
mode: setting.mode,
|
|
|
|
|
queueOnly: setting.queueOnly,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
return status;
|
|
|
|
|
});
|
2026-04-30 20:33:00 -07:00
|
|
|
setIsSyncing(false);
|
2026-04-23 22:09:25 -07:00
|
|
|
}
|
|
|
|
|
}, [revalidatorState, draftPicks, season, userQueue, timers, autodraftSettings, currentUserId,
|
2026-04-30 20:33:00 -07:00
|
|
|
setPicks, setCurrentPick, setIsPaused, setIsDraftComplete, setQueue, setTeamTimers, setAutodraftStatus, setIsSyncing]);
|
2026-04-23 22:09:25 -07:00
|
|
|
|
|
|
|
|
const authFetch = useCallback(async (url: string, init?: RequestInit): Promise<Response | null> => {
|
|
|
|
|
const response = await fetch(url, init);
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
setAuthDegraded(true);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return response;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
authDegraded,
|
|
|
|
|
authFetch,
|
|
|
|
|
userAutodraft,
|
|
|
|
|
setUserAutodraft,
|
|
|
|
|
userAutodraftRef,
|
|
|
|
|
isRevalidatingRef,
|
|
|
|
|
pendingPicksDuringRevalidationRef,
|
|
|
|
|
pendingQueueMutationsRef,
|
|
|
|
|
};
|
|
|
|
|
}
|