161 lines
7.6 KiB
TypeScript
161 lines
7.6 KiB
TypeScript
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"),
|
|
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"),
|
|
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),
|
|
]);
|