Add administrator sign-in and private-safe room directory
Connect personal operator-provisioned admin keys, persisted roles and eight-hour sessions, current/all-session revocation and a filtered room directory. Additive schema protects final owner, revokes disabled accounts and records safe audit history. No game state changes; remaining administration scope stays explicit. Verified: typecheck, lint, 5196 offline tests, 52 HTTP tests, production build; authenticated local HTTP and desktop/mobile browser checks. Independent privacy/security review fixed malformed nested roster searches and read-time session revocation. Preserved 1,671 prior rooms and 3,538 seats. Container/deployed acceptance follows publication.
This commit is contained in:
1 parent
36a03bda9c
commit
c2d392e155
27 files changed
+3164
-13
No files matched your search
+66
-6
@@ -5,13 +5,20 @@ import {
|
||||
primaryKey,
|
||||
index,
|
||||
uniqueIndex,
|
||||
check,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
export const rooms = sqliteTable('rooms', {
|
||||
code: text('code').primaryKey(),
|
||||
state: text('state').notNull(),
|
||||
version: integer('version').notNull().default(0),
|
||||
updatedAt: integer('updated_at').notNull(),
|
||||
});
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const rooms = sqliteTable(
|
||||
'rooms',
|
||||
{
|
||||
code: text('code').primaryKey(),
|
||||
state: text('state').notNull(),
|
||||
version: integer('version').notNull().default(0),
|
||||
updatedAt: integer('updated_at').notNull(),
|
||||
},
|
||||
(table) => [index('rooms_updated_at').on(table.updatedAt)],
|
||||
);
|
||||
export const seats = sqliteTable('seats', {
|
||||
tokenHash: text('token_hash').primaryKey(),
|
||||
revoked: integer('revoked').notNull().default(0),
|
||||
@@ -152,3 +159,56 @@ export const roomMessages = sqliteTable(
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const adminAccounts = sqliteTable(
|
||||
'admin_accounts',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
role: text('role', { enum: ['owner', 'operator', 'viewer'] }).notNull(),
|
||||
keyHash: text('key_hash').notNull().unique(),
|
||||
enabled: integer('enabled').notNull().default(1),
|
||||
sessionGeneration: integer('session_generation').notNull().default(0),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
updatedAt: integer('updated_at').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
check(
|
||||
'admin_accounts_role',
|
||||
sql`${table.role} IN ('owner', 'operator', 'viewer')`,
|
||||
),
|
||||
check('admin_accounts_enabled', sql`${table.enabled} IN (0, 1)`),
|
||||
check('admin_accounts_generation', sql`${table.sessionGeneration} >= 0`),
|
||||
],
|
||||
);
|
||||
|
||||
export const adminSessions = sqliteTable(
|
||||
'admin_sessions',
|
||||
{
|
||||
tokenHash: text('token_hash').primaryKey(),
|
||||
id: text('id').notNull().unique(),
|
||||
adminId: text('admin_id')
|
||||
.notNull()
|
||||
.references(() => adminAccounts.id, { onDelete: 'cascade' }),
|
||||
generation: integer('generation').notNull(),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
expiresAt: integer('expires_at').notNull(),
|
||||
revokedAt: integer('revoked_at'),
|
||||
},
|
||||
(table) => [index('admin_sessions_account').on(table.adminId)],
|
||||
);
|
||||
|
||||
// Audit records intentionally have no cascading account/room dependency.
|
||||
// `detail` contains safe operation IDs only, never credentials or game state.
|
||||
export const adminAudit = sqliteTable(
|
||||
'admin_audit',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
actorAdminId: text('actor_admin_id'),
|
||||
targetAdminId: text('target_admin_id').notNull(),
|
||||
action: text('action').notNull(),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
detail: text('detail').notNull().default('{}'),
|
||||
},
|
||||
(table) => [index('admin_audit_created_at').on(table.createdAt)],
|
||||
);
|
||||
Reference in new issue
Block a user