- 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
130 lines
4.0 KiB
JavaScript
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();
|
|
}
|