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.
84 lines
3.5 KiB
SQL
84 lines
3.5 KiB
SQL
CREATE TABLE `admin_accounts` (
|
|
`id` text PRIMARY KEY NOT NULL,
|
|
`name` text NOT NULL,
|
|
`role` text NOT NULL,
|
|
`key_hash` text NOT NULL,
|
|
`enabled` integer DEFAULT 1 NOT NULL,
|
|
`session_generation` integer DEFAULT 0 NOT NULL,
|
|
`created_at` integer NOT NULL,
|
|
`updated_at` integer NOT NULL,
|
|
CONSTRAINT "admin_accounts_role" CHECK("admin_accounts"."role" IN ('owner', 'operator', 'viewer')),
|
|
CONSTRAINT "admin_accounts_enabled" CHECK("admin_accounts"."enabled" IN (0, 1)),
|
|
CONSTRAINT "admin_accounts_generation" CHECK("admin_accounts"."session_generation" >= 0)
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE UNIQUE INDEX `admin_accounts_key_hash_unique` ON `admin_accounts` (`key_hash`);--> statement-breakpoint
|
|
CREATE TABLE `admin_audit` (
|
|
`id` text PRIMARY KEY NOT NULL,
|
|
`actor_admin_id` text,
|
|
`target_admin_id` text NOT NULL,
|
|
`action` text NOT NULL,
|
|
`created_at` integer NOT NULL,
|
|
`detail` text DEFAULT '{}' NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE INDEX `admin_audit_created_at` ON `admin_audit` (`created_at`);--> statement-breakpoint
|
|
CREATE TABLE `admin_sessions` (
|
|
`token_hash` text PRIMARY KEY NOT NULL,
|
|
`id` text NOT NULL,
|
|
`admin_id` text NOT NULL,
|
|
`generation` integer NOT NULL,
|
|
`created_at` integer NOT NULL,
|
|
`expires_at` integer NOT NULL,
|
|
`revoked_at` integer,
|
|
FOREIGN KEY (`admin_id`) REFERENCES `admin_accounts`(`id`) ON UPDATE no action ON DELETE cascade
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE UNIQUE INDEX `admin_sessions_id_unique` ON `admin_sessions` (`id`);--> statement-breakpoint
|
|
CREATE INDEX `admin_sessions_account` ON `admin_sessions` (`admin_id`);--> statement-breakpoint
|
|
CREATE INDEX `rooms_updated_at` ON `rooms` (`updated_at`);
|
|
--> statement-breakpoint
|
|
-- D1 operator SQL must be atomic without client-managed BEGIN/COMMIT.
|
|
-- Drizzle records the table schema; these SQLite triggers retain the account
|
|
-- audit and revocation invariants for every writer, including the setup CLI.
|
|
CREATE TRIGGER `admin_accounts_provision_audit`
|
|
AFTER INSERT ON `admin_accounts`
|
|
BEGIN
|
|
INSERT INTO admin_audit (id, actor_admin_id, target_admin_id, action, created_at, detail)
|
|
VALUES (lower(hex(randomblob(16))), NULL, NEW.id, 'provision', unixepoch() * 1000,
|
|
json_object('role', NEW.role));
|
|
END;
|
|
--> statement-breakpoint
|
|
CREATE TRIGGER `admin_accounts_last_owner_update`
|
|
BEFORE UPDATE OF enabled, role ON `admin_accounts`
|
|
WHEN OLD.enabled = 1 AND OLD.role = 'owner'
|
|
AND (NEW.enabled != 1 OR NEW.role != 'owner')
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM admin_accounts WHERE id != OLD.id AND enabled = 1 AND role = 'owner'
|
|
)
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'Cannot remove the final enabled administrator owner.');
|
|
END;
|
|
--> statement-breakpoint
|
|
CREATE TRIGGER `admin_accounts_last_owner_delete`
|
|
BEFORE DELETE ON `admin_accounts`
|
|
WHEN OLD.enabled = 1 AND OLD.role = 'owner'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM admin_accounts WHERE id != OLD.id AND enabled = 1 AND role = 'owner'
|
|
)
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'Cannot remove the final enabled administrator owner.');
|
|
END;
|
|
--> statement-breakpoint
|
|
CREATE TRIGGER `admin_accounts_revoke`
|
|
AFTER UPDATE OF enabled ON `admin_accounts`
|
|
WHEN OLD.enabled = 1 AND NEW.enabled = 0
|
|
BEGIN
|
|
UPDATE admin_accounts SET session_generation = session_generation + 1 WHERE id = NEW.id;
|
|
UPDATE admin_sessions SET revoked_at = unixepoch() * 1000
|
|
WHERE admin_id = NEW.id AND revoked_at IS NULL;
|
|
INSERT INTO admin_audit (id, actor_admin_id, target_admin_id, action, created_at, detail)
|
|
VALUES (lower(hex(randomblob(16))), NULL, NEW.id, 'revoke', unixepoch() * 1000,
|
|
json_object('previousRole', OLD.role));
|
|
END;
|