From fc44fef91ad727c4328758c9a0d2aaf16c9333e0 Mon Sep 17 00:00:00 2001 From: Joerg Date: Wed, 21 Jan 2026 10:37:05 +0100 Subject: [PATCH] feat: add migration for admin actions and role fields Adds new tables and columns for admin functionality: - Create admin_actions table for audit logging - Create qso_changes table for sync job rollback support - Add role column to users (default: 'user') - Add is_admin column to users (default: false) No data loss - uses ALTER TABLE with safe defaults. Co-Authored-By: Claude --- drizzle/0002_nervous_layla_miller.sql | 25 + drizzle/meta/0002_snapshot.json | 756 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + 3 files changed, 788 insertions(+) create mode 100644 drizzle/0002_nervous_layla_miller.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_nervous_layla_miller.sql b/drizzle/0002_nervous_layla_miller.sql new file mode 100644 index 0000000..34b6b97 --- /dev/null +++ b/drizzle/0002_nervous_layla_miller.sql @@ -0,0 +1,25 @@ +CREATE TABLE `admin_actions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `admin_id` integer NOT NULL, + `action_type` text NOT NULL, + `target_user_id` integer, + `details` text, + `created_at` integer NOT NULL, + FOREIGN KEY (`admin_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`target_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `qso_changes` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `job_id` integer NOT NULL, + `qso_id` integer, + `change_type` text NOT NULL, + `before_data` text, + `after_data` text, + `created_at` integer NOT NULL, + FOREIGN KEY (`job_id`) REFERENCES `sync_jobs`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`qso_id`) REFERENCES `qsos`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +ALTER TABLE `users` ADD `role` text DEFAULT 'user' NOT NULL;--> statement-breakpoint +ALTER TABLE `users` ADD `is_admin` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..7887d45 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,756 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "542bddc5-2e08-49af-91b5-013a6c9584df", + "prevId": "b5c00e60-2f3c-4c2b-a540-0be8d9e856e6", + "tables": { + "admin_actions": { + "name": "admin_actions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "admin_id": { + "name": "admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "admin_actions_admin_id_users_id_fk": { + "name": "admin_actions_admin_id_users_id_fk", + "tableFrom": "admin_actions", + "tableTo": "users", + "columnsFrom": [ + "admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "admin_actions_target_user_id_users_id_fk": { + "name": "admin_actions_target_user_id_users_id_fk", + "tableFrom": "admin_actions", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "award_progress": { + "name": "award_progress", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "award_id": { + "name": "award_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worked_count": { + "name": "worked_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "confirmed_count": { + "name": "confirmed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_required": { + "name": "total_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worked_entities": { + "name": "worked_entities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirmed_entities": { + "name": "confirmed_entities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_calculated_at": { + "name": "last_calculated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_qso_sync_at": { + "name": "last_qso_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "award_progress_user_id_users_id_fk": { + "name": "award_progress_user_id_users_id_fk", + "tableFrom": "award_progress", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "award_progress_award_id_awards_id_fk": { + "name": "award_progress_award_id_awards_id_fk", + "tableFrom": "award_progress", + "tableTo": "awards", + "columnsFrom": [ + "award_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "awards": { + "name": "awards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qso_changes": { + "name": "qso_changes", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "job_id": { + "name": "job_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qso_id": { + "name": "qso_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "before_data": { + "name": "before_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "after_data": { + "name": "after_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "qso_changes_job_id_sync_jobs_id_fk": { + "name": "qso_changes_job_id_sync_jobs_id_fk", + "tableFrom": "qso_changes", + "tableTo": "sync_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "qso_changes_qso_id_qsos_id_fk": { + "name": "qso_changes_qso_id_qsos_id_fk", + "tableFrom": "qso_changes", + "tableTo": "qsos", + "columnsFrom": [ + "qso_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qsos": { + "name": "qsos", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "callsign": { + "name": "callsign", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qso_date": { + "name": "qso_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_on": { + "name": "time_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "band": { + "name": "band", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "freq": { + "name": "freq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "freq_rx": { + "name": "freq_rx", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grid": { + "name": "grid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grid_source": { + "name": "grid_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "continent": { + "name": "continent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cq_zone": { + "name": "cq_zone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "itu_zone": { + "name": "itu_zone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sat_name": { + "name": "sat_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sat_mode": { + "name": "sat_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "my_darc_dok": { + "name": "my_darc_dok", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "darc_dok": { + "name": "darc_dok", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_qsl_rdate": { + "name": "lotw_qsl_rdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_qsl_rstatus": { + "name": "lotw_qsl_rstatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dcl_qsl_rdate": { + "name": "dcl_qsl_rdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dcl_qsl_rstatus": { + "name": "dcl_qsl_rstatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_synced_at": { + "name": "lotw_synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "qsos_user_id_users_id_fk": { + "name": "qsos_user_id_users_id_fk", + "tableFrom": "qsos", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_jobs": { + "name": "sync_jobs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_jobs_user_id_users_id_fk": { + "name": "sync_jobs_user_id_users_id_fk", + "tableFrom": "sync_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "callsign": { + "name": "callsign", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lotw_username": { + "name": "lotw_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_password": { + "name": "lotw_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dcl_api_key": { + "name": "dcl_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 522bf20..eedd558 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1768641501799, "tag": "0001_free_hiroim", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1768988121232, + "tag": "0002_nervous_layla_miller", + "breakpoints": true } ] } \ No newline at end of file