Add auth system with invite-only registration and admin roles

JWT-based auth (hono/jwt + bcrypt), anonymous-first flow preserved.
Registration requires invite code when REQUIRE_INVITE=true. Admin
user seeded on startup (admin/admin, forced password reset). Login
accepts email or username. Admin invitations management page in
sidebar. Regular users get invite-a-friend button when USER_INVITATIONS > 0.
Frontend gate screen blocks game access for unregistered users with
invite code entry, registration, login, and password reset flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 19:25:16 -04:00
parent df01ac8e35
commit 4881907c28
20 changed files with 1161 additions and 48 deletions
+40
View File
@@ -0,0 +1,40 @@
import { sign, verify } from 'hono/jwt';
const JWT_EXPIRY_SECONDS = 30 * 24 * 60 * 60;
export function getJwtSecret(): string {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET env var is required');
return secret;
}
export async function createToken(
userId: string,
email: string | null,
role: string,
username: string | null,
mustResetPassword: boolean,
): Promise<string> {
const now = Math.floor(Date.now() / 1000);
return sign(
{ sub: userId, email, role, username, mustResetPassword, iat: now, exp: now + JWT_EXPIRY_SECONDS },
getJwtSecret(),
);
}
export async function verifyToken(token: string): Promise<{
sub: string;
email: string | null;
role: string;
username: string | null;
mustResetPassword: boolean;
}> {
const payload = await verify(token, getJwtSecret(), 'HS256');
return {
sub: payload.sub as string,
email: (payload.email as string) ?? null,
role: (payload.role as string) ?? 'user',
username: (payload.username as string) ?? null,
mustResetPassword: (payload.mustResetPassword as boolean) ?? false,
};
}