Files
award/src/backend/services/auth.service.js
Joerg e88537754f feat: implement comprehensive admin functionality
- Add admin role system with role and isAdmin fields to users table
- Create admin_actions audit log table for tracking all admin operations
- Implement admin CLI tool for user management (create, promote, demote, list, check)
- Add admin authentication with role-based access control
- Create admin service layer with system statistics and user management
- Implement user impersonation system with proper security checks
- Add admin API endpoints for user management and system statistics
- Create admin dashboard UI with overview, users, and action logs
- Fix admin stats endpoint and user deletion with proper foreign key handling
- Add admin link to navigation bar for admin users

Database:
- Add role and isAdmin columns to users table
- Create admin_actions table for audit trail
- Migration script: add-admin-functionality.js

CLI:
- src/backend/scripts/admin-cli.js - Admin user management tool

Backend:
- src/backend/services/admin.service.js - Admin business logic
- Updated auth.service.js with admin helper functions
- Enhanced index.js with admin routes and middleware
- Export sqlite connection from config for raw SQL operations

Frontend:
- src/frontend/src/routes/admin/+page.svelte - Admin dashboard
- Updated api.js with adminAPI functions
- Added Admin link to navigation bar

Security:
- Admin-only endpoints with role verification
- Audit logging for all admin actions
- Impersonation with 1-hour token expiration
- Foreign key constraint handling for user deletion
- Cannot delete self or other admins
- Last admin protection
2026-01-21 09:43:56 +01:00

244 lines
5.9 KiB
JavaScript

import { eq } from 'drizzle-orm';
import { db } from '../config.js';
import { users } from '../db/schema/index.js';
/**
* Hash a password using Bun's built-in password hashing
* @param {string} password - Plain text password
* @returns {Promise<string>} Hashed password
*/
async function hashPassword(password) {
return Bun.password.hash(password);
}
/**
* Verify a password against a hash
* @param {string} password - Plain text password
* @param {string} hash - Hashed password
* @returns {Promise<boolean>} True if password matches
*/
async function verifyPassword(password, hash) {
return Bun.password.verify(password, hash);
}
/**
* Register a new user
* @param {Object} userData - User registration data
* @param {string} userData.email - User email
* @param {string} userData.password - Plain text password
* @param {string} userData.callsign - Ham radio callsign
* @returns {Promise<Object|null>} Created user object (without password) or null if email exists
*/
export async function registerUser({ email, password, callsign }) {
// Check if user already exists
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (existingUser) {
return null;
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const newUser = await db
.insert(users)
.values({
email,
passwordHash,
callsign: callsign.toUpperCase(),
})
.returning();
// Return user without password hash
const { passwordHash: _, ...userWithoutPassword } = newUser[0];
return userWithoutPassword;
}
/**
* Authenticate user with email and password
* @param {string} email - User email
* @param {string} password - Plain text password
* @returns {Promise<Object>} User object (without password) if authenticated
* @throws {Error} If credentials are invalid
*/
export async function authenticateUser(email, password) {
// Find user by email
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user) {
return null;
}
// Verify password
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) {
return null;
}
// Return user without password hash
const { passwordHash: _, ...userWithoutPassword } = user;
return userWithoutPassword;
}
/**
* Get user by ID
* @param {number} userId - User ID
* @returns {Promise<Object|null>} User object (without password) or null
*/
export async function getUserById(userId) {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) return null;
const { passwordHash: _, ...userWithoutPassword } = user;
return userWithoutPassword;
}
/**
* Update user's LoTW credentials (encrypted)
* @param {number} userId - User ID
* @param {string} lotwUsername - LoTW username
* @param {string} lotwPassword - LoTW password (will be encrypted)
* @returns {Promise<void>}
*/
export async function updateLoTWCredentials(userId, lotwUsername, lotwPassword) {
// Simple encryption for storage (in production, use a proper encryption library)
// For now, we'll store as-is but marked for encryption
await db
.update(users)
.set({
lotwUsername,
lotwPassword, // TODO: Encrypt before storing
updatedAt: new Date(),
})
.where(eq(users.id, userId));
}
/**
* Update user's DCL API key
* @param {number} userId - User ID
* @param {string} dclApiKey - DCL API key
* @returns {Promise<void>}
*/
export async function updateDCLCredentials(userId, dclApiKey) {
await db
.update(users)
.set({
dclApiKey,
updatedAt: new Date(),
})
.where(eq(users.id, userId));
}
/**
* Check if user is admin
* @param {number} userId - User ID
* @returns {Promise<boolean>} True if user is admin
*/
export async function isAdmin(userId) {
const [user] = await db
.select({ isAdmin: users.isAdmin })
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user?.isAdmin === true || user?.isAdmin === 1;
}
/**
* Get all admin users
* @returns {Promise<Array>} Array of admin users (without passwords)
*/
export async function getAdminUsers() {
const adminUsers = await db
.select({
id: users.id,
email: users.email,
callsign: users.callsign,
role: users.role,
isAdmin: users.isAdmin,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.isAdmin, 1));
return adminUsers;
}
/**
* Update user role
* @param {number} userId - User ID
* @param {string} role - New role ('user' or 'admin')
* @param {boolean} isAdmin - Admin flag
* @returns {Promise<void>}
*/
export async function updateUserRole(userId, role, isAdmin) {
await db
.update(users)
.set({
role,
isAdmin: isAdmin ? 1 : 0,
updatedAt: new Date(),
})
.where(eq(users.id, userId));
}
/**
* Get all users (for admin use)
* @returns {Promise<Array>} Array of all users (without passwords)
*/
export async function getAllUsers() {
const allUsers = await db
.select({
id: users.id,
email: users.email,
callsign: users.callsign,
role: users.role,
isAdmin: users.isAdmin,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.orderBy(users.createdAt);
return allUsers;
}
/**
* Get user by ID (for admin use)
* @param {number} userId - User ID
* @returns {Promise<Object|null>} Full user object (without password) or null
*/
export async function getUserByIdFull(userId) {
const [user] = await db
.select({
id: users.id,
email: users.email,
callsign: users.callsign,
role: users.role,
isAdmin: users.isAdmin,
lotwUsername: users.lotwUsername,
dclApiKey: users.dclApiKey,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user || null;
}