From 8f8abfc651f7b216c94018cf0ba0206683371a92 Mon Sep 17 00:00:00 2001 From: Joerg Date: Wed, 21 Jan 2026 11:41:41 +0100 Subject: [PATCH] refactor: remove redundant role field, keep only is_admin - Remove role column from users schema (migration 0003) - Update auth and admin services to use is_admin only - Remove role from JWT token payloads - Update admin CLI to use is_admin field - Update frontend admin page to use isAdmin boolean - Fix security: remove console.log dumping credentials in settings Co-Authored-By: Claude --- drizzle/0003_tired_warpath.sql | 1 + drizzle/meta/0003_snapshot.json | 748 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/backend/db/schema/index.js | 4 +- src/backend/index.js | 23 +- src/backend/scripts/admin-cli.js | 26 +- src/backend/services/admin.service.js | 13 +- src/backend/services/auth.service.js | 9 +- src/frontend/src/lib/api.js | 4 +- src/frontend/src/routes/admin/+page.svelte | 10 +- src/frontend/src/routes/settings/+page.svelte | 6 - 11 files changed, 789 insertions(+), 62 deletions(-) create mode 100644 drizzle/0003_tired_warpath.sql create mode 100644 drizzle/meta/0003_snapshot.json diff --git a/drizzle/0003_tired_warpath.sql b/drizzle/0003_tired_warpath.sql new file mode 100644 index 0000000..df65d27 --- /dev/null +++ b/drizzle/0003_tired_warpath.sql @@ -0,0 +1 @@ +ALTER TABLE `users` DROP COLUMN `role`; \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..38a3a03 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,748 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "071c98fb-6721-4da7-98cb-c16cb6aaf0c1", + "prevId": "542bddc5-2e08-49af-91b5-013a6c9584df", + "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 + }, + "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 eedd558..693fa4a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1768988121232, "tag": "0002_nervous_layla_miller", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1768989260562, + "tag": "0003_tired_warpath", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/backend/db/schema/index.js b/src/backend/db/schema/index.js index e79a798..95601bd 100644 --- a/src/backend/db/schema/index.js +++ b/src/backend/db/schema/index.js @@ -9,7 +9,6 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; * @property {string|null} lotwUsername * @property {string|null} lotwPassword * @property {string|null} dclApiKey - * @property {string} role * @property {boolean} isAdmin * @property {Date} createdAt * @property {Date} updatedAt @@ -23,8 +22,7 @@ export const users = sqliteTable('users', { lotwUsername: text('lotw_username'), lotwPassword: text('lotw_password'), // Encrypted dclApiKey: text('dcl_api_key'), // DCL API key for future use - role: text('role').notNull().default('user'), // 'user', 'admin' - isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false), // Simplified admin check + isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false), createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); diff --git a/src/backend/index.js b/src/backend/index.js index 8f079cf..7cfc1ad 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -195,7 +195,6 @@ const app = new Elysia() id: payload.userId, email: payload.email, callsign: payload.callsign, - role: payload.role, isAdmin: payload.isAdmin, impersonatedBy: payload.impersonatedBy, // Admin ID if impersonating }, @@ -400,7 +399,6 @@ const app = new Elysia() userId: user.id, email: user.email, callsign: user.callsign, - role: user.role, isAdmin: user.isAdmin, exp, }); @@ -1139,7 +1137,7 @@ const app = new Elysia() /** * POST /api/admin/users/:userId/role - * Update user role (admin only) + * Update user admin status (admin only) */ .post('/api/admin/users/:userId/role', async ({ user, params, body, set }) => { if (!user || !user.isAdmin) { @@ -1153,26 +1151,21 @@ const app = new Elysia() return { success: false, error: 'Invalid user ID' }; } - const { role, isAdmin } = body; + const { isAdmin } = body; - if (!role || typeof isAdmin !== 'boolean') { + if (typeof isAdmin !== 'boolean') { set.status = 400; - return { success: false, error: 'role and isAdmin are required' }; - } - - if (!['user', 'admin'].includes(role)) { - set.status = 400; - return { success: false, error: 'Invalid role' }; + return { success: false, error: 'isAdmin (boolean) is required' }; } try { - await changeUserRole(user.id, targetUserId, role, isAdmin); + await changeUserRole(user.id, targetUserId, isAdmin); return { success: true, - message: 'User role updated successfully', + message: 'User admin status updated successfully', }; } catch (error) { - logger.error('Error updating user role', { error: error.message, userId: user.id }); + logger.error('Error updating user admin status', { error: error.message, userId: user.id }); set.status = 400; return { success: false, @@ -1238,7 +1231,6 @@ const app = new Elysia() userId: targetUser.id, email: targetUser.email, callsign: targetUser.callsign, - role: targetUser.role, isAdmin: targetUser.isAdmin, impersonatedBy: user.id, // Admin ID who started impersonation exp, @@ -1299,7 +1291,6 @@ const app = new Elysia() userId: adminUser.id, email: adminUser.email, callsign: adminUser.callsign, - role: adminUser.role, isAdmin: adminUser.isAdmin, exp, }); diff --git a/src/backend/scripts/admin-cli.js b/src/backend/scripts/admin-cli.js index bca949a..94dd25f 100644 --- a/src/backend/scripts/admin-cli.js +++ b/src/backend/scripts/admin-cli.js @@ -69,15 +69,14 @@ function createAdminUser(email, password, callsign) { // Insert admin user const result = sqlite.query(` - INSERT INTO users (email, password_hash, callsign, role, is_admin, created_at, updated_at) - VALUES (?, ?, ?, 'admin', 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000) + INSERT INTO users (email, password_hash, callsign, is_admin, created_at, updated_at) + VALUES (?, ?, ?, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000) `).run(email, hashString, callsign); console.log(`✓ Admin user created successfully!`); console.log(` ID: ${result.lastInsertRowid}`); console.log(` Email: ${email}`); console.log(` Callsign: ${callsign}`); - console.log(` Role: admin`); console.log(`\nYou can now log in with these credentials.`); } @@ -86,7 +85,7 @@ function promoteUser(email) { // Check if user exists const user = sqlite.query(` - SELECT id, email, role, is_admin FROM users WHERE email = ? + SELECT id, email, is_admin FROM users WHERE email = ? `).get(email); if (!user) { @@ -94,7 +93,7 @@ function promoteUser(email) { process.exit(1); } - if (user.role === 'admin' && user.is_admin === 1) { + if (user.is_admin === 1) { console.log(`User ${email} is already an admin`); return; } @@ -102,7 +101,7 @@ function promoteUser(email) { // Update user to admin sqlite.query(` UPDATE users - SET role = 'admin', is_admin = 1, updated_at = strftime('%s', 'now') * 1000 + SET is_admin = 1, updated_at = strftime('%s', 'now') * 1000 WHERE email = ? `).run(email); @@ -114,7 +113,7 @@ function demoteUser(email) { // Check if user exists const user = sqlite.query(` - SELECT id, email, role, is_admin FROM users WHERE email = ? + SELECT id, email, is_admin FROM users WHERE email = ? `).get(email); if (!user) { @@ -122,14 +121,14 @@ function demoteUser(email) { process.exit(1); } - if (user.role !== 'admin' || user.is_admin !== 1) { + if (user.is_admin !== 1) { console.log(`User ${email} is not an admin`); return; } // Check if this is the last admin const adminCount = sqlite.query(` - SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND is_admin = 1 + SELECT COUNT(*) as count FROM users WHERE is_admin = 1 `).get(); if (adminCount.count === 1) { @@ -140,7 +139,7 @@ function demoteUser(email) { // Update user to regular user sqlite.query(` UPDATE users - SET role = 'user', is_admin = 0, updated_at = strftime('%s', 'now') * 1000 + SET is_admin = 0, updated_at = strftime('%s', 'now') * 1000 WHERE email = ? `).run(email); @@ -153,7 +152,7 @@ function listAdmins() { const admins = sqlite.query(` SELECT id, email, callsign, created_at FROM users - WHERE role = 'admin' AND is_admin = 1 + WHERE is_admin = 1 ORDER BY created_at ASC `).all(); @@ -176,7 +175,7 @@ function checkUser(email) { console.log(`Checking user status: ${email}\n`); const user = sqlite.query(` - SELECT id, email, callsign, role, is_admin FROM users WHERE email = ? + SELECT id, email, callsign, is_admin FROM users WHERE email = ? `).get(email); if (!user) { @@ -184,12 +183,11 @@ function checkUser(email) { process.exit(1); } - const isAdmin = user.role === 'admin' && user.is_admin === 1; + const isAdmin = user.is_admin === 1; console.log(`User found:`); console.log(` Email: ${user.email}`); console.log(` Callsign: ${user.callsign}`); - console.log(` Role: ${user.role}`); console.log(` Is Admin: ${isAdmin ? 'Yes ✓' : 'No'}`); } diff --git a/src/backend/services/admin.service.js b/src/backend/services/admin.service.js index 6d31bef..7157d0b 100644 --- a/src/backend/services/admin.service.js +++ b/src/backend/services/admin.service.js @@ -122,7 +122,6 @@ export async function getUserStats() { id: users.id, email: users.email, callsign: users.callsign, - role: users.role, isAdmin: users.isAdmin, qsoCount: sql`CAST(COUNT(${qsos.id}) AS INTEGER)`, lotwConfirmed: sql`CAST(SUM(CASE WHEN ${qsos.lotwQslRstatus} = 'Y' THEN 1 ELSE 0 END) AS INTEGER)`, @@ -250,19 +249,18 @@ export async function getImpersonationStatus(adminId, { limit = 10 } = {}) { } /** - * Update user role (admin operation) + * Update user admin status (admin operation) * @param {number} adminId - Admin user ID making the change * @param {number} targetUserId - User ID to update - * @param {string} newRole - New role ('user' or 'admin') * @param {boolean} newIsAdmin - New admin flag * @returns {Promise} * @throws {Error} If not admin or would remove last admin */ -export async function changeUserRole(adminId, targetUserId, newRole, newIsAdmin) { +export async function changeUserRole(adminId, targetUserId, newIsAdmin) { // Verify the requester is an admin const requesterIsAdmin = await isAdmin(adminId); if (!requesterIsAdmin) { - throw new Error('Only admins can change user roles'); + throw new Error('Only admins can change user admin status'); } // Get target user @@ -283,11 +281,10 @@ export async function changeUserRole(adminId, targetUserId, newRole, newIsAdmin) } } - // Update role + // Update admin status await db .update(users) .set({ - role: newRole, isAdmin: newIsAdmin ? 1 : 0, updatedAt: new Date(), }) @@ -295,8 +292,6 @@ export async function changeUserRole(adminId, targetUserId, newRole, newIsAdmin) // Log action await logAdminAction(adminId, 'role_change', targetUserId, { - oldRole: targetUser.role, - newRole: newRole, oldIsAdmin: targetUser.isAdmin, newIsAdmin: newIsAdmin, }); diff --git a/src/backend/services/auth.service.js b/src/backend/services/auth.service.js index 876b67d..07a5268 100644 --- a/src/backend/services/auth.service.js +++ b/src/backend/services/auth.service.js @@ -168,7 +168,6 @@ export async function getAdminUsers() { id: users.id, email: users.email, callsign: users.callsign, - role: users.role, isAdmin: users.isAdmin, createdAt: users.createdAt, }) @@ -179,17 +178,15 @@ export async function getAdminUsers() { } /** - * Update user role + * Update user admin status * @param {number} userId - User ID - * @param {string} role - New role ('user' or 'admin') * @param {boolean} isAdmin - Admin flag * @returns {Promise} */ -export async function updateUserRole(userId, role, isAdmin) { +export async function updateUserRole(userId, isAdmin) { await db .update(users) .set({ - role, isAdmin: isAdmin ? 1 : 0, updatedAt: new Date(), }) @@ -206,7 +203,6 @@ export async function getAllUsers() { id: users.id, email: users.email, callsign: users.callsign, - role: users.role, isAdmin: users.isAdmin, createdAt: users.createdAt, updatedAt: users.updatedAt, @@ -228,7 +224,6 @@ export async function getUserByIdFull(userId) { id: users.id, email: users.email, callsign: users.callsign, - role: users.role, isAdmin: users.isAdmin, lotwUsername: users.lotwUsername, dclApiKey: users.dclApiKey, diff --git a/src/frontend/src/lib/api.js b/src/frontend/src/lib/api.js index 1a6e016..1ef1e84 100644 --- a/src/frontend/src/lib/api.js +++ b/src/frontend/src/lib/api.js @@ -95,9 +95,9 @@ export const adminAPI = { getUserDetails: (userId) => apiRequest(`/admin/users/${userId}`), - updateUserRole: (userId, role, isAdmin) => apiRequest(`/admin/users/${userId}/role`, { + updateUserRole: (userId, isAdmin) => apiRequest(`/admin/users/${userId}/role`, { method: 'POST', - body: JSON.stringify({ role, isAdmin }), + body: JSON.stringify({ isAdmin }), }), deleteUser: (userId) => apiRequest(`/admin/users/${userId}`, { diff --git a/src/frontend/src/routes/admin/+page.svelte b/src/frontend/src/routes/admin/+page.svelte index c64ddcf..e1ece13 100644 --- a/src/frontend/src/routes/admin/+page.svelte +++ b/src/frontend/src/routes/admin/+page.svelte @@ -167,19 +167,19 @@ } } - async function handleRoleChange(userId, newRole, newIsAdmin) { + async function handleRoleChange(userId, newIsAdmin) { try { loading = true; - const data = await adminAPI.updateUserRole(userId, newRole, newIsAdmin); + const data = await adminAPI.updateUserRole(userId, newIsAdmin); if (data.success) { alert(data.message); await loadUsers(); } else { - alert('Failed to update user role: ' + (data.error || 'Unknown error')); + alert('Failed to update user admin status: ' + (data.error || 'Unknown error')); } } catch (err) { - alert('Failed to update user role: ' + err.message); + alert('Failed to update user admin status: ' + err.message); } finally { loading = false; showRoleChangeModal = false; @@ -518,7 +518,7 @@ diff --git a/src/frontend/src/routes/settings/+page.svelte b/src/frontend/src/routes/settings/+page.svelte index 71e97b5..a666e6d 100644 --- a/src/frontend/src/routes/settings/+page.svelte +++ b/src/frontend/src/routes/settings/+page.svelte @@ -25,14 +25,12 @@ try { loading = true; const response = await authAPI.getProfile(); - console.log('Loaded profile:', response.user); if (response.user) { lotwUsername = response.user.lotwUsername || ''; lotwPassword = ''; // Never pre-fill password for security hasLoTWCredentials = !!(response.user.lotwUsername && response.user.lotwPassword); dclApiKey = response.user.dclApiKey || ''; hasDCLCredentials = !!response.user.dclApiKey; - console.log('Has LoTW credentials:', hasLoTWCredentials, 'Has DCL credentials:', hasDCLCredentials); } } catch (err) { console.error('Failed to load profile:', err); @@ -50,8 +48,6 @@ error = null; successLoTW = false; - console.log('Saving LoTW credentials:', { lotwUsername, hasPassword: !!lotwPassword }); - await authAPI.updateLoTWCredentials({ lotwUsername, lotwPassword @@ -78,8 +74,6 @@ error = null; successDCL = false; - console.log('Saving DCL credentials:', { hasApiKey: !!dclApiKey }); - await authAPI.updateDCLCredentials({ dclApiKey });