Files
AIHostingTycoon/apps/server/src/db/schema.ts
T
josh 4881907c28 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>
2026-04-27 19:25:16 -04:00

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'),
});