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
+12
View File
@@ -3,8 +3,11 @@ import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, index } from '
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
anonToken: uuid('anon_token').defaultRandom().notNull().unique(),
username: text('username').unique(),
email: text('email').unique(),
passwordHash: text('password_hash'),
role: text('role').notNull().default('user'),
mustResetPassword: boolean('must_reset_password').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
lastSeenAt: timestamp('last_seen_at').defaultNow().notNull(),
});
@@ -44,3 +47,12 @@ export const achievements = pgTable('achievements', {
}, (table) => [
index('achievements_user_id_idx').on(table.userId),
]);
export const invitations = pgTable('invitations', {
id: uuid('id').defaultRandom().primaryKey(),
code: text('code').notNull().unique(),
createdBy: uuid('created_by').notNull().references(() => users.id),
usedBy: uuid('used_by').references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
expiresAt: timestamp('expires_at'),
});