Restore from Gitea ZIP snapshot (12.08.2026) after full instance reinstall

Git history was lost when the previous Gitea instance was wiped and
reinstalled due to an unresolved corruption bug — this commit is the
last known-good file content, exported before the reinstall. Prior
commit history is not recoverable through this path.
This commit is contained in:
Claude Sonnet 5
2026-08-12 19:24:02 +00:00
commit c4e2132717
96 changed files with 16108 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import path from "node:path";
import fs from "node:fs";
import * as schema from "./schema";
const dataDir = process.env.DATA_DIR ?? path.join(process.cwd(), "data");
fs.mkdirSync(dataDir, { recursive: true });
const sqlite = new Database(path.join(dataDir, "db.sqlite"));
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("foreign_keys = ON");
// Next's build-time page-data collection evaluates route modules across
// several worker processes concurrently — without this, two workers racing
// to open/initialize a fresh db.sqlite can throw SQLITE_BUSY instead of
// just waiting for the other's lock to clear (hit repeatedly in the
// sibling top-tickets project; baking the fix in from day one here).
sqlite.pragma("busy_timeout = 30000");
export const db = drizzle(sqlite, { schema });
export type DB = typeof db;
@@ -0,0 +1,40 @@
CREATE TABLE `ai_config` (
`id` text PRIMARY KEY NOT NULL,
`anthropic_api_key_enc` text NOT NULL,
`model` text DEFAULT 'claude-sonnet-5' NOT NULL,
`enabled` integer DEFAULT false NOT NULL,
`verified_at` integer,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `games` (
`id` text PRIMARY KEY NOT NULL,
`owner_id` text NOT NULL,
`title` text NOT NULL,
`description` text,
`template_type` text NOT NULL,
`status` text DEFAULT 'draft' NOT NULL,
`definition` text NOT NULL,
`play_count` integer DEFAULT 0 NOT NULL,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `sessions` (
`token_hash` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`password_hash` text NOT NULL,
`name` text NOT NULL,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
@@ -0,0 +1,41 @@
CREATE TABLE `coin_purchases` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`coins` integer NOT NULL,
`price_rub` integer NOT NULL,
`provider` text,
`status` text DEFAULT 'pending' NOT NULL,
`provider_payment_id` text,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
`completed_at` integer,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `coin_transactions` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`amount` integer NOT NULL,
`reason` text NOT NULL,
`related_game_id` text,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`related_game_id`) REFERENCES `games`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE TABLE `game_progress` (
`id` text PRIMARY KEY NOT NULL,
`game_id` text NOT NULL,
`user_id` text NOT NULL,
`state` text NOT NULL,
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `game_progress_game_user_unique` ON `game_progress` (`game_id`,`user_id`);--> statement-breakpoint
CREATE TABLE `wallets` (
`user_id` text PRIMARY KEY NOT NULL,
`balance` integer DEFAULT 0 NOT NULL,
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
@@ -0,0 +1,20 @@
CREATE TABLE `prestige` (
`user_id` text PRIMARY KEY NOT NULL,
`crystals` integer DEFAULT 0 NOT NULL,
`click_bonus_level` integer DEFAULT 0 NOT NULL,
`gold_bonus_level` integer DEFAULT 0 NOT NULL,
`updated_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `prestige_claims` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`game_id` text NOT NULL,
`crystals_awarded` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch('subsec') * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `prestige_claims_user_game_unique` ON `prestige_claims` (`user_id`,`game_id`);
@@ -0,0 +1,278 @@
{
"version": "6",
"dialect": "sqlite",
"id": "ef845fe8-d266-400e-9a8c-264d90d11b26",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"ai_config": {
"name": "ai_config",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"anthropic_api_key_enc": {
"name": "anthropic_api_key_enc",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'claude-sonnet-5'"
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"verified_at": {
"name": "verified_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"games": {
"name": "games",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner_id": {
"name": "owner_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"template_type": {
"name": "template_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"definition": {
"name": "definition",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"play_count": {
"name": "play_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"games_owner_id_users_id_fk": {
"name": "games_owner_id_users_id_fk",
"tableFrom": "games",
"tableTo": "users",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
@@ -0,0 +1,576 @@
{
"version": "6",
"dialect": "sqlite",
"id": "2da73b1a-5598-47e5-beef-ce9e8f1db0c5",
"prevId": "ef845fe8-d266-400e-9a8c-264d90d11b26",
"tables": {
"ai_config": {
"name": "ai_config",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"anthropic_api_key_enc": {
"name": "anthropic_api_key_enc",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'claude-sonnet-5'"
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"verified_at": {
"name": "verified_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"coin_purchases": {
"name": "coin_purchases",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"coins": {
"name": "coins",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"price_rub": {
"name": "price_rub",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"provider_payment_id": {
"name": "provider_payment_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
},
"completed_at": {
"name": "completed_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"coin_purchases_user_id_users_id_fk": {
"name": "coin_purchases_user_id_users_id_fk",
"tableFrom": "coin_purchases",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"coin_transactions": {
"name": "coin_transactions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"related_game_id": {
"name": "related_game_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"coin_transactions_user_id_users_id_fk": {
"name": "coin_transactions_user_id_users_id_fk",
"tableFrom": "coin_transactions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"coin_transactions_related_game_id_games_id_fk": {
"name": "coin_transactions_related_game_id_games_id_fk",
"tableFrom": "coin_transactions",
"tableTo": "games",
"columnsFrom": [
"related_game_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"game_progress": {
"name": "game_progress",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"game_id": {
"name": "game_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"game_progress_game_user_unique": {
"name": "game_progress_game_user_unique",
"columns": [
"game_id",
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"game_progress_game_id_games_id_fk": {
"name": "game_progress_game_id_games_id_fk",
"tableFrom": "game_progress",
"tableTo": "games",
"columnsFrom": [
"game_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"game_progress_user_id_users_id_fk": {
"name": "game_progress_user_id_users_id_fk",
"tableFrom": "game_progress",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"games": {
"name": "games",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner_id": {
"name": "owner_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"template_type": {
"name": "template_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"definition": {
"name": "definition",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"play_count": {
"name": "play_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"games_owner_id_users_id_fk": {
"name": "games_owner_id_users_id_fk",
"tableFrom": "games",
"tableTo": "users",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"wallets": {
"name": "wallets",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"wallets_user_id_users_id_fk": {
"name": "wallets_user_id_users_id_fk",
"tableFrom": "wallets",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
@@ -0,0 +1,721 @@
{
"version": "6",
"dialect": "sqlite",
"id": "5562a6c0-8876-47fe-8d5c-b8d76083c13a",
"prevId": "2da73b1a-5598-47e5-beef-ce9e8f1db0c5",
"tables": {
"ai_config": {
"name": "ai_config",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"anthropic_api_key_enc": {
"name": "anthropic_api_key_enc",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'claude-sonnet-5'"
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"verified_at": {
"name": "verified_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"coin_purchases": {
"name": "coin_purchases",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"coins": {
"name": "coins",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"price_rub": {
"name": "price_rub",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'pending'"
},
"provider_payment_id": {
"name": "provider_payment_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
},
"completed_at": {
"name": "completed_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"coin_purchases_user_id_users_id_fk": {
"name": "coin_purchases_user_id_users_id_fk",
"tableFrom": "coin_purchases",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"coin_transactions": {
"name": "coin_transactions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"related_game_id": {
"name": "related_game_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"coin_transactions_user_id_users_id_fk": {
"name": "coin_transactions_user_id_users_id_fk",
"tableFrom": "coin_transactions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"coin_transactions_related_game_id_games_id_fk": {
"name": "coin_transactions_related_game_id_games_id_fk",
"tableFrom": "coin_transactions",
"tableTo": "games",
"columnsFrom": [
"related_game_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"game_progress": {
"name": "game_progress",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"game_id": {
"name": "game_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"game_progress_game_user_unique": {
"name": "game_progress_game_user_unique",
"columns": [
"game_id",
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"game_progress_game_id_games_id_fk": {
"name": "game_progress_game_id_games_id_fk",
"tableFrom": "game_progress",
"tableTo": "games",
"columnsFrom": [
"game_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"game_progress_user_id_users_id_fk": {
"name": "game_progress_user_id_users_id_fk",
"tableFrom": "game_progress",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"games": {
"name": "games",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner_id": {
"name": "owner_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"template_type": {
"name": "template_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"definition": {
"name": "definition",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"play_count": {
"name": "play_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"games_owner_id_users_id_fk": {
"name": "games_owner_id_users_id_fk",
"tableFrom": "games",
"tableTo": "users",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"prestige": {
"name": "prestige",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"crystals": {
"name": "crystals",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"click_bonus_level": {
"name": "click_bonus_level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"gold_bonus_level": {
"name": "gold_bonus_level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"prestige_user_id_users_id_fk": {
"name": "prestige_user_id_users_id_fk",
"tableFrom": "prestige",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"prestige_claims": {
"name": "prestige_claims",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"game_id": {
"name": "game_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"crystals_awarded": {
"name": "crystals_awarded",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"prestige_claims_user_game_unique": {
"name": "prestige_claims_user_game_unique",
"columns": [
"user_id",
"game_id"
],
"isUnique": true
}
},
"foreignKeys": {
"prestige_claims_user_id_users_id_fk": {
"name": "prestige_claims_user_id_users_id_fk",
"tableFrom": "prestige_claims",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"prestige_claims_game_id_games_id_fk": {
"name": "prestige_claims_game_id_games_id_fk",
"tableFrom": "prestige_claims",
"tableTo": "games",
"columnsFrom": [
"game_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"wallets": {
"name": "wallets",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch('subsec') * 1000)"
}
},
"indexes": {},
"foreignKeys": {
"wallets_user_id_users_id_fk": {
"name": "wallets_user_id_users_id_fk",
"tableFrom": "wallets",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1785518228143,
"tag": "0000_marvelous_tyrannus",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1785534147942,
"tag": "0001_fearless_proudstar",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1785552846668,
"tag": "0002_stale_lucky_pierre",
"breakpoints": true
}
]
}
+149
View File
@@ -0,0 +1,149 @@
import { sql } from "drizzle-orm";
import { sqliteTable, text, integer, uniqueIndex } from "drizzle-orm/sqlite-core";
import crypto from "node:crypto";
const id = () =>
text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID());
const timestamps = {
createdAt: integer("created_at", { mode: "timestamp_ms" })
.notNull()
.default(sql`(unixepoch('subsec') * 1000)`),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.notNull()
.default(sql`(unixepoch('subsec') * 1000)`),
};
/** Every registered account can both play and create games — no role split. */
export const users = sqliteTable("users", {
id: id(),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
name: text("name").notNull(),
createdAt: timestamps.createdAt,
});
export const sessions = sqliteTable("sessions", {
// primary key is the SHA-256 hash of the raw session token — the raw
// token only ever lives in the client's httpOnly cookie.
tokenHash: text("token_hash").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: timestamps.createdAt,
});
export const games = sqliteTable("games", {
id: id(),
ownerId: text("owner_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
title: text("title").notNull(),
description: text("description"),
templateType: text("template_type", { enum: ["quiz", "clicker", "maze", "snake"] }).notNull(),
status: text("status", { enum: ["draft", "published"] }).notNull().default("draft"),
// JSON string validated against the templateType's zod schema (src/lib/games/*).
definition: text("definition").notNull(),
playCount: integer("play_count").notNull().default(0),
createdAt: timestamps.createdAt,
updatedAt: timestamps.updatedAt,
});
/** Singleton — same shape as top-tickets' telegramConfig/mailboxConfig. */
export const aiConfig = sqliteTable("ai_config", {
id: id(),
anthropicApiKeyEnc: text("anthropic_api_key_enc").notNull(),
model: text("model").notNull().default("claude-sonnet-5"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
verifiedAt: integer("verified_at", { mode: "timestamp_ms" }),
createdAt: timestamps.createdAt,
});
/** One save slot per (game, player) — upserted, JSON shape per templateType (src/lib/games/*). */
export const gameProgress = sqliteTable(
"game_progress",
{
id: id(),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
state: text("state").notNull(),
updatedAt: timestamps.updatedAt,
},
(table) => [uniqueIndex("game_progress_game_user_unique").on(table.gameId, table.userId)],
);
export const wallets = sqliteTable("wallets", {
userId: text("user_id")
.primaryKey()
.references(() => users.id, { onDelete: "cascade" }),
balance: integer("balance").notNull().default(0),
updatedAt: timestamps.updatedAt,
});
/** Ledger — every balance change, for audit/history. */
export const coinTransactions = sqliteTable("coin_transactions", {
id: id(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// Positive = credit, negative = debit.
amount: integer("amount").notNull(),
reason: text("reason", { enum: ["signup_bonus", "manual_save", "purchase"] }).notNull(),
relatedGameId: text("related_game_id").references(() => games.id, { onDelete: "set null" }),
createdAt: timestamps.createdAt,
});
/** A real-money order for coins — always "pending" until a payment provider is wired in. */
export const coinPurchases = sqliteTable("coin_purchases", {
id: id(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
coins: integer("coins").notNull(),
priceRub: integer("price_rub").notNull(),
provider: text("provider"),
status: text("status", { enum: ["pending", "completed", "failed"] }).notNull().default("pending"),
providerPaymentId: text("provider_payment_id"),
createdAt: timestamps.createdAt,
completedAt: integer("completed_at", { mode: "timestamp_ms" }),
});
/**
* A separate, non-real-money currency earned by clearing Clicker
* dungeons and spent on permanent account-wide upgrades — deliberately
* kept apart from wallets/coinTransactions so clicker play can never be
* used to farm the real-money coin economy.
*/
export const prestige = sqliteTable("prestige", {
userId: text("user_id")
.primaryKey()
.references(() => users.id, { onDelete: "cascade" }),
crystals: integer("crystals").notNull().default(0),
clickBonusLevel: integer("click_bonus_level").notNull().default(0),
goldBonusLevel: integer("gold_bonus_level").notNull().default(0),
updatedAt: timestamps.updatedAt,
});
/** One row per (user, game) that has ever paid out a completion reward — makes claiming idempotent. */
export const prestigeClaims = sqliteTable(
"prestige_claims",
{
id: id(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
crystalsAwarded: integer("crystals_awarded").notNull(),
createdAt: timestamps.createdAt,
},
(table) => [uniqueIndex("prestige_claims_user_game_unique").on(table.userId, table.gameId)],
);