import { sql } from "drizzle-orm"; import { blob, check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; export const admins = sqliteTable("admins", { id: text("id").primaryKey(), username: text("username").notNull(), usernameNorm: text("username_norm").notNull(), displayName: text("display_name").notNull(), passwordHash: text("password_hash").notNull(), status: text("status", { enum: ["active", "disabled"] }).notNull().default("active"), mustChangePassword: integer("must_change_password", { mode: "boolean" }).notNull().default(true), authVersion: integer("auth_version").notNull().default(1), version: integer("version").notNull().default(1), createdAt: integer("created_at").notNull(), createdBy: text("created_by"), passwordChangedAt: integer("password_changed_at"), lastLoginAt: integer("last_login_at"), disabledAt: integer("disabled_at"), disabledBy: text("disabled_by"), }, (table) => [ uniqueIndex("admins_username_norm_uq").on(table.usernameNorm), check("admins_status_ck", sql`${table.status} in ('active','disabled')`), check("admins_versions_ck", sql`${table.version} >= 1 and ${table.authVersion} >= 1`), ]); export const sessions = sqliteTable("sessions", { tokenHash: text("token_hash").primaryKey(), adminId: text("admin_id").notNull().references(() => admins.id, { onDelete: "cascade" }), csrfHash: text("csrf_hash").notNull(), authVersion: integer("auth_version").notNull(), createdAt: integer("created_at").notNull(), lastSeenAt: integer("last_seen_at").notNull(), idleExpiresAt: integer("idle_expires_at").notNull(), absoluteExpiresAt: integer("absolute_expires_at").notNull(), }, (table) => [index("sessions_admin_idx").on(table.adminId), index("sessions_expiry_idx").on(table.idleExpiresAt)]); export const expenses = sqliteTable("expenses", { id: text("id").primaryKey(), paidAt: integer("paid_at").notNull(), amountCents: integer("amount_cents").notNull(), note: text("note").notNull().default(""), invoiceMissingReason: text("invoice_missing_reason"), status: text("status", { enum: ["unreimbursed", "reimbursed"] }).notNull().default("unreimbursed"), version: integer("version").notNull().default(1), createdAt: integer("created_at").notNull(), createdBy: text("created_by").notNull().references(() => admins.id, { onDelete: "restrict" }), updatedAt: integer("updated_at").notNull(), updatedBy: text("updated_by").notNull().references(() => admins.id, { onDelete: "restrict" }), reimbursedAt: integer("reimbursed_at"), reimbursedBy: text("reimbursed_by").references(() => admins.id, { onDelete: "restrict" }), deletedAt: integer("deleted_at"), deletedBy: text("deleted_by").references(() => admins.id, { onDelete: "restrict" }), }, (table) => [ index("expenses_list_idx").on(table.deletedAt, table.status, table.paidAt), check("expenses_amount_ck", sql`${table.amountCents} > 0 and ${table.amountCents} <= 999999999999`), check("expenses_status_ck", sql`${table.status} in ('unreimbursed','reimbursed')`), check("expenses_version_ck", sql`${table.version} >= 1`), ]); export const attachments = sqliteTable("attachments", { id: text("id").primaryKey(), expenseId: text("expense_id").notNull().references(() => expenses.id, { onDelete: "cascade" }), kind: text("kind", { enum: ["payment_proof", "invoice"] }).notNull(), storagePath: text("storage_path").notNull(), originalName: text("original_name").notNull(), mimeType: text("mime_type").notNull(), sizeBytes: integer("size_bytes").notNull(), sha256: text("sha256").notNull(), createdAt: integer("created_at").notNull(), createdBy: text("created_by").notNull().references(() => admins.id, { onDelete: "restrict" }), }, (table) => [ uniqueIndex("attachments_path_uq").on(table.storagePath), index("attachments_expense_idx").on(table.expenseId), check("attachments_kind_ck", sql`${table.kind} in ('payment_proof','invoice')`), check("attachments_size_ck", sql`${table.sizeBytes} > 0`), ]); export const auditEvents = sqliteTable("audit_events", { id: integer("id").primaryKey({ autoIncrement: true }), occurredAt: integer("occurred_at").notNull(), requestId: text("request_id").notNull(), actorAdminId: text("actor_admin_id"), actorUsername: text("actor_username"), action: text("action").notNull(), targetType: text("target_type").notNull(), targetId: text("target_id"), outcome: text("outcome", { enum: ["success", "denied", "failure"] }).notNull(), beforeJson: text("before_json"), afterJson: text("after_json"), metadataJson: text("metadata_json"), }, (table) => [index("audit_time_idx").on(table.occurredAt), index("audit_target_idx").on(table.targetType, table.targetId)]); export const systemSettings = sqliteTable("system_settings", { key: text("key").primaryKey(), value: text("value").notNull(), updatedAt: integer("updated_at").notNull(), }); export const exportJobs = sqliteTable("export_jobs", { id: text("id").primaryKey(), adminId: text("admin_id").notNull().references(() => admins.id, { onDelete: "cascade" }), sessionHash: text("session_hash").notNull(), status: text("status", { enum: ["queued", "building", "ready", "failed", "expired"] }).notNull(), selectionJson: text("selection_json").notNull(), snapshotJson: text("snapshot_json").notNull(), filePath: text("file_path"), fileName: text("file_name").notNull(), sizeBytes: integer("size_bytes"), sha256: text("sha256"), errorMessage: text("error_message"), createdAt: integer("created_at").notNull(), readyAt: integer("ready_at"), expiresAt: integer("expires_at").notNull(), }, (table) => [index("exports_expiry_idx").on(table.expiresAt), index("exports_session_idx").on(table.sessionHash)]); export const loginAttempts = sqliteTable("login_attempts", { keyHash: text("key_hash").primaryKey(), windowStart: integer("window_start").notNull(), failures: integer("failures").notNull(), blockedUntil: integer("blocked_until"), }); export const fileDeletions = sqliteTable("file_deletions", { id: text("id").primaryKey(), storagePath: text("storage_path").notNull(), reason: text("reason").notNull(), status: text("status", { enum: ["pending", "complete", "failed"] }).notNull().default("pending"), attempts: integer("attempts").notNull().default(0), lastError: text("last_error"), createdAt: integer("created_at").notNull(), completedAt: integer("completed_at"), }, (table) => [index("file_deletions_status_idx").on(table.status)]); export const updateJobs = sqliteTable("update_jobs", { id: text("id").primaryKey(), adminId: text("admin_id").references(() => admins.id, { onDelete: "set null" }), sessionHash: text("session_hash"), requestId: text("request_id"), operation: text("operation", { enum: ["download", "apply"] }).notNull().default("apply"), status: text("status", { enum: ["queued", "downloading", "verifying", "staged", "backing_up", "applying", "completed", "failed", "cancelled"] }).notNull(), version: text("version").notNull(), platform: text("platform").notNull(), releaseUrl: text("release_url"), assetName: text("asset_name"), assetUrl: text("asset_url").notNull(), expectedSha256: text("expected_sha256"), actualSha256: text("actual_sha256"), downloadPath: text("download_path"), backupPath: text("backup_path"), sizeBytes: integer("size_bytes"), downloadedBytes: integer("downloaded_bytes"), downloadStartedAt: integer("download_started_at"), downloadSpeedBps: integer("download_speed_bps"), errorMessage: text("error_message"), createdAt: integer("created_at").notNull(), requestedAt: integer("requested_at"), startedAt: integer("started_at"), updatedAt: integer("updated_at").notNull(), completedAt: integer("completed_at"), }, (table) => [ index("update_jobs_status_idx").on(table.status, table.createdAt), index("update_jobs_admin_idx").on(table.adminId, table.createdAt), index("update_jobs_session_idx").on(table.sessionHash), ]);