4881907c28
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>
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, index } from 'drizzle-orm/pg-core';
|
|
|
|
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(),
|
|
});
|
|
|
|
export const saves = pgTable('saves', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
userId: uuid('user_id').notNull().references(() => users.id),
|
|
companyName: text('company_name').notNull(),
|
|
saveVersion: integer('save_version').notNull(),
|
|
gameData: jsonb('game_data').notNull(),
|
|
tickCount: integer('tick_count').notNull().default(0),
|
|
era: text('era').notNull().default('startup'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
}, (table) => [
|
|
index('saves_user_id_idx').on(table.userId),
|
|
]);
|
|
|
|
export const leaderboard = pgTable('leaderboard', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
userId: uuid('user_id').notNull().references(() => users.id),
|
|
companyName: text('company_name').notNull(),
|
|
category: text('category').notNull(),
|
|
score: integer('score').notNull(),
|
|
era: text('era').notNull(),
|
|
tickCount: integer('tick_count').notNull(),
|
|
submittedAt: timestamp('submitted_at').defaultNow().notNull(),
|
|
}, (table) => [
|
|
index('leaderboard_category_score_idx').on(table.category, table.score),
|
|
]);
|
|
|
|
export const achievements = pgTable('achievements', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
userId: uuid('user_id').notNull().references(() => users.id),
|
|
achievementId: text('achievement_id').notNull(),
|
|
unlockedAt: timestamp('unlocked_at').defaultNow().notNull(),
|
|
}, (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'),
|
|
});
|