diff --git a/scripts/dev-set-password.ts b/scripts/dev-set-password.ts new file mode 100644 index 0000000..fffaf1f --- /dev/null +++ b/scripts/dev-set-password.ts @@ -0,0 +1,40 @@ +/** + * Dev helper: set a user's password directly (bypasses email reset). + * Usage: pnpm tsx scripts/dev-set-password.ts + */ +import 'dotenv/config'; +import { hashPassword } from 'better-auth/crypto'; +import { eq, and } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { user, account } from '@/lib/db/schema/users'; + +async function main() { + const [, , email, password] = process.argv; + if (!email || !password) { + console.error('Usage: pnpm tsx scripts/dev-set-password.ts '); + process.exit(1); + } + + const u = await db.query.user.findFirst({ where: eq(user.email, email) }); + if (!u) { + console.error(`User not found: ${email}`); + process.exit(1); + } + + const hash = await hashPassword(password); + const result = await db + .update(account) + .set({ password: hash, updatedAt: new Date() }) + .where(and(eq(account.userId, u.id), eq(account.providerId, 'credential'))) + .returning({ id: account.id }); + + if (result.length === 0) { + console.error(`No credential account row for ${email}`); + process.exit(1); + } + + console.log(`Updated password for ${email} (account id ${result[0]?.id}).`); + process.exit(0); +} + +main(); diff --git a/src/components/shared/branded-auth-shell.tsx b/src/components/shared/branded-auth-shell.tsx index 7403a3a..0a4d39d 100644 --- a/src/components/shared/branded-auth-shell.tsx +++ b/src/components/shared/branded-auth-shell.tsx @@ -10,15 +10,23 @@ const LOGO_URL = */ export function BrandedAuthShell({ children }: { children: React.ReactNode }) { return ( -
+
+ {/* + Full-viewport background layer — pinned to the visible viewport via + `fixed inset-0` so the marina image always reaches the actual screen + edges regardless of the iOS Safari URL bar showing/hiding. The shell's + layout layer above sits on top via z-index. + */} +
diff --git a/src/lib/auth/client.ts b/src/lib/auth/client.ts index 5ca0d94..714b202 100644 --- a/src/lib/auth/client.ts +++ b/src/lib/auth/client.ts @@ -2,8 +2,14 @@ import { createAuthClient } from 'better-auth/react'; +/** + * Use the current window origin as the auth API host so the same dev build + * works whether the page was loaded via http://localhost:3001 (Mac) or + * http://192.168.1.17:3001 (iPhone on LAN). Falls back to the build-time + * NEXT_PUBLIC_APP_URL during SSR / module-eval where `window` is undefined. + */ export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_APP_URL, + baseURL: typeof window !== 'undefined' ? window.location.origin : process.env.NEXT_PUBLIC_APP_URL, }); export const { useSession, signIn, signOut, getSession } = authClient; diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index f7f1267..8fe8b97 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -9,11 +9,43 @@ import { db } from '@/lib/db'; * Sessions are stored in PostgreSQL (not Redis) per SECURITY-GUIDELINES.md §1.2. * The drizzle adapter handles session persistence via the existing `sessions` table. */ +/** + * In dev, allow requests from any LAN IP so the same `pnpm dev` instance can + * serve both localhost (Mac) and the LAN IP (iPhone on Wi-Fi). In production, + * trustedOrigins is locked down to NEXT_PUBLIC_APP_URL only. + */ +/** + * In dev, allow localhost + any LAN-IP origin so the same `pnpm dev` instance + * can serve both Mac (localhost) and iPhone-on-Wi-Fi (192.168.x.x). The + * function form is preferred over a static list because the LAN IP can vary + * across networks. In production, lock down to NEXT_PUBLIC_APP_URL only. + */ +const isProd = process.env.NODE_ENV === 'production'; +const DEV_ORIGIN_PATTERNS = [ + /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/, + /^https?:\/\/192\.168\.\d+\.\d+(:\d+)?$/, + /^https?:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/, +]; + +const trustedOrigins: (request?: Request) => Promise = async (request) => { + if (isProd) { + const prodUrl = process.env.NEXT_PUBLIC_APP_URL; + return prodUrl ? [prodUrl] : []; + } + const origin = request?.headers.get('origin') ?? ''; + if (origin && DEV_ORIGIN_PATTERNS.some((re) => re.test(origin))) { + return [origin]; + } + return ['http://localhost:3000', 'http://localhost:3001']; +}; + export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', }), + trustedOrigins, + emailAndPassword: { enabled: true, minPasswordLength: 9,