Files
award/src/backend/config.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

130 lines
4.0 KiB
JavaScript

import Database from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import * as schema from './db/schema/index.js';
import { join, dirname } from 'path';
import { existsSync, mkdirSync, appendFile } from 'fs';
import { fileURLToPath } from 'url';
// ===================================================================
// Configuration
// ===================================================================
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isDevelopment = process.env.NODE_ENV !== 'production';
// SECURITY: Require JWT_SECRET in production - no fallback for security
// This prevents JWT token forgery if environment variable is not set
if (!process.env.JWT_SECRET && !isDevelopment) {
throw new Error(
'FATAL: JWT_SECRET environment variable must be set in production. ' +
'Generate one with: openssl rand -base64 32'
);
}
export const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-key-change-in-production';
export const LOG_LEVEL = process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info');
// ===================================================================
// Logger
// ===================================================================
const logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
const currentLogLevel = logLevels[LOG_LEVEL] ?? 1;
// Log file paths
const logsDir = join(__dirname, '../../logs');
const backendLogFile = join(logsDir, 'backend.log');
// Ensure log directory exists
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true });
}
function formatLogMessage(level, message, data) {
const timestamp = new Date().toISOString();
let logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
logMessage += ' ' + JSON.stringify(data, null, 2);
}
return logMessage + '\n';
}
function log(level, message, data) {
if (logLevels[level] < currentLogLevel) return;
const logMessage = formatLogMessage(level, message, data);
// Append to file asynchronously (fire and forget for performance)
appendFile(backendLogFile, logMessage, (err) => {
if (err) console.error('Failed to write to log file:', err);
});
// Also log to console in development
if (isDevelopment) {
const timestamp = new Date().toISOString();
const consoleMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
console.log(consoleMessage, data);
} else {
console.log(consoleMessage);
}
}
}
export const logger = {
debug: (message, data) => log('debug', message, data),
info: (message, data) => log('info', message, data),
warn: (message, data) => log('warn', message, data),
error: (message, data) => log('error', message, data),
};
// Frontend logger - writes to separate log file
const frontendLogFile = join(logsDir, 'frontend.log');
export function logToFrontend(level, message, data = null, context = {}) {
if (logLevels[level] < currentLogLevel) return;
const timestamp = new Date().toISOString();
let logMessage = `[${timestamp}] [${context.userAgent || 'unknown'}] [${context.userId || 'anonymous'}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
logMessage += ' ' + JSON.stringify(data, null, 2);
}
logMessage += '\n';
// Append to frontend log file
appendFile(frontendLogFile, logMessage, (err) => {
if (err) console.error('Failed to write to frontend log file:', err);
});
}
export default logger;
// ===================================================================
// Database
// ===================================================================
// Get the directory containing this config file, then go to parent for db location
const dbPath = join(__dirname, 'award.db');
const sqlite = new Database(dbPath);
sqlite.exec('PRAGMA foreign_keys = ON');
export const db = drizzle({
client: sqlite,
schema,
});
export { sqlite };
export async function closeDatabase() {
sqlite.close();
}