diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index b0afbab1..377a9005 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -20,6 +20,11 @@ import https from "https"; import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; const app = express(); + +// Initialize auth middleware +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const requireAdmin = authManager.createAdminMiddleware(); app.use( cors({ origin: "*", @@ -172,7 +177,7 @@ app.get("/health", (req, res) => { res.json({ status: "ok" }); }); -app.get("/version", async (req, res) => { +app.get("/version", authenticateJWT, async (req, res) => { let localVersion = process.env.VERSION; if (!localVersion) { @@ -238,7 +243,7 @@ app.get("/version", async (req, res) => { } }); -app.get("/releases/rss", async (req, res) => { +app.get("/releases/rss", authenticateJWT, async (req, res) => { try { const page = parseInt(req.query.page as string) || 1; const per_page = Math.min( @@ -294,7 +299,7 @@ app.get("/releases/rss", async (req, res) => { } }); -app.get("/encryption/status", async (req, res) => { +app.get("/encryption/status", requireAdmin, async (req, res) => { try { const authManager = AuthManager.getInstance(); // Simplified status for new architecture @@ -317,7 +322,7 @@ app.get("/encryption/status", async (req, res) => { } }); -app.post("/encryption/initialize", async (req, res) => { +app.post("/encryption/initialize", requireAdmin, async (req, res) => { try { const authManager = AuthManager.getInstance(); @@ -346,7 +351,7 @@ app.post("/encryption/initialize", async (req, res) => { }); -app.post("/encryption/regenerate", async (req, res) => { +app.post("/encryption/regenerate", requireAdmin, async (req, res) => { try { const authManager = AuthManager.getInstance(); @@ -373,7 +378,7 @@ app.post("/encryption/regenerate", async (req, res) => { } }); -app.post("/encryption/regenerate-jwt", async (req, res) => { +app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => { try { const authManager = AuthManager.getInstance(); // JWT regeneration moved to SystemKeyManager directly @@ -397,22 +402,9 @@ app.post("/encryption/regenerate-jwt", async (req, res) => { }); // User data export endpoint - V2 KEK-DEK compatible -app.post("/database/export", async (req, res) => { +app.post("/database/export", authenticateJWT, async (req, res) => { try { - const authHeader = req.headers["authorization"]; - if (!authHeader?.startsWith("Bearer ")) { - return res.status(401).json({ error: "Missing Authorization header" }); - } - - const token = authHeader.split(" ")[1]; - const authManager = AuthManager.getInstance(); - const payload = await authManager.verifyJWTToken(token); - - if (!payload) { - return res.status(401).json({ error: "Invalid token" }); - } - - const userId = payload.userId; + const userId = (req as any).userId; const { format = 'encrypted', scope = 'user_data', includeCredentials = true, password } = req.body; // For plaintext export, need to unlock user data @@ -470,34 +462,13 @@ app.post("/database/export", async (req, res) => { }); // User data import endpoint - V2 KEK-DEK compatible -app.post("/database/import", upload.single("file"), async (req, res) => { +app.post("/database/import", authenticateJWT, upload.single("file"), async (req, res) => { try { - const authHeader = req.headers["authorization"]; - if (!authHeader?.startsWith("Bearer ")) { - // Clean up uploaded file - if (req.file?.path) { - try { fs.unlinkSync(req.file.path); } catch {} - } - return res.status(401).json({ error: "Missing Authorization header" }); - } - - const token = authHeader.split(" ")[1]; - const authManager = AuthManager.getInstance(); - const payload = await authManager.verifyJWTToken(token); - - if (!payload) { - // Clean up uploaded file - if (req.file?.path) { - try { fs.unlinkSync(req.file.path); } catch {} - } - return res.status(401).json({ error: "Invalid token" }); - } - if (!req.file) { return res.status(400).json({ error: "No file uploaded" }); } - const userId = payload.userId; + const userId = (req as any).userId; const { replaceExisting = false, skipCredentials = false, skipFileManagerData = false, dryRun = false, password } = req.body; apiLogger.info("Importing user data", { @@ -596,22 +567,9 @@ app.post("/database/import", upload.single("file"), async (req, res) => { }); // Export preview endpoint - validate export data without downloading -app.post("/database/export/preview", async (req, res) => { +app.post("/database/export/preview", authenticateJWT, async (req, res) => { try { - const authHeader = req.headers["authorization"]; - if (!authHeader?.startsWith("Bearer ")) { - return res.status(401).json({ error: "Missing Authorization header" }); - } - - const token = authHeader.split(" ")[1]; - const authManager = AuthManager.getInstance(); - const payload = await authManager.verifyJWTToken(token); - - if (!payload) { - return res.status(401).json({ error: "Invalid token" }); - } - - const userId = payload.userId; + const userId = (req as any).userId; const { format = 'encrypted', scope = 'user_data', includeCredentials = true } = req.body; apiLogger.info("Generating export preview", { @@ -653,7 +611,7 @@ app.post("/database/export/preview", async (req, res) => { } }); -app.post("/database/backup", async (req, res) => { +app.post("/database/backup", requireAdmin, async (req, res) => { try { const { customPath } = req.body; @@ -701,7 +659,7 @@ app.post("/database/backup", async (req, res) => { } }); -app.post("/database/restore", async (req, res) => { +app.post("/database/restore", requireAdmin, async (req, res) => { try { const { backupPath, targetPath } = req.body; diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index ddfc44c5..be57fcfb 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -4,6 +4,7 @@ import { dismissedAlerts } from "../db/schema.js"; import { eq, and } from "drizzle-orm"; import fetch from "node-fetch"; import { authLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; interface CacheEntry { data: any; @@ -107,31 +108,15 @@ async function fetchAlertsFromGitHub(): Promise { const router = express.Router(); -// Route: Get all active alerts +// Initialize auth middleware +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); + +// Route: Get alerts for the authenticated user (excluding dismissed ones) // GET /alerts -router.get("/", async (req, res) => { +router.get("/", authenticateJWT, async (req, res) => { try { - const alerts = await fetchAlertsFromGitHub(); - res.json({ - alerts, - cached: alertCache.get("termix_alerts") !== null, - total_count: alerts.length, - }); - } catch (error) { - authLogger.error("Failed to get alerts", error); - res.status(500).json({ error: "Failed to fetch alerts" }); - } -}); - -// Route: Get alerts for a specific user (excluding dismissed ones) -// GET /alerts/user/:userId -router.get("/user/:userId", async (req, res) => { - try { - const { userId } = req.params; - - if (!userId) { - return res.status(400).json({ error: "User ID is required" }); - } + const userId = (req as any).userId; const allAlerts = await fetchAlertsFromGitHub(); @@ -144,32 +129,33 @@ router.get("/user/:userId", async (req, res) => { dismissedAlertRecords.map((record) => record.alertId), ); - const userAlerts = allAlerts.filter( + const activeAlertsForUser = allAlerts.filter( (alert) => !dismissedAlertIds.has(alert.id), ); res.json({ - alerts: userAlerts, - total_count: userAlerts.length, - dismissed_count: dismissedAlertIds.size, + alerts: activeAlertsForUser, + cached: alertCache.get("termix_alerts") !== null, + total_count: activeAlertsForUser.length, }); } catch (error) { authLogger.error("Failed to get user alerts", error); - res.status(500).json({ error: "Failed to fetch user alerts" }); + res.status(500).json({ error: "Failed to fetch alerts" }); } }); -// Route: Dismiss an alert for a user -// POST /alerts/dismiss -router.post("/dismiss", async (req, res) => { - try { - const { userId, alertId } = req.body; +// Deprecated endpoint - use GET /alerts instead - if (!userId || !alertId) { - authLogger.warn("Missing userId or alertId in dismiss request"); - return res - .status(400) - .json({ error: "User ID and Alert ID are required" }); +// Route: Dismiss an alert for the authenticated user +// POST /alerts/dismiss +router.post("/dismiss", authenticateJWT, async (req, res) => { + try { + const { alertId } = req.body; + const userId = (req as any).userId; + + if (!alertId) { + authLogger.warn("Missing alertId in dismiss request", { userId }); + return res.status(400).json({ error: "Alert ID is required" }); } const existingDismissal = await db @@ -201,13 +187,9 @@ router.post("/dismiss", async (req, res) => { // Route: Get dismissed alerts for a user // GET /alerts/dismissed/:userId -router.get("/dismissed/:userId", async (req, res) => { +router.get("/dismissed", authenticateJWT, async (req, res) => { try { - const { userId } = req.params; - - if (!userId) { - return res.status(400).json({ error: "User ID is required" }); - } + const userId = (req as any).userId; const dismissedAlertRecords = await db .select({ @@ -227,16 +209,15 @@ router.get("/dismissed/:userId", async (req, res) => { } }); -// Route: Undismiss an alert for a user (remove from dismissed list) +// Route: Undismiss an alert for the authenticated user (remove from dismissed list) // DELETE /alerts/dismiss -router.delete("/dismiss", async (req, res) => { +router.delete("/dismiss", authenticateJWT, async (req, res) => { try { - const { userId, alertId } = req.body; + const { alertId } = req.body; + const userId = (req as any).userId; - if (!userId || !alertId) { - return res - .status(400) - .json({ error: "User ID and Alert ID are required" }); + if (!alertId) { + return res.status(400).json({ error: "Alert ID is required" }); } const result = await db diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 68d7929f..22f47303 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -138,6 +138,7 @@ interface JWTPayload { // JWT authentication middleware - only verify JWT, no data unlock required const authenticateJWT = authManager.createAuthMiddleware(); +const requireAdmin = authManager.createAdminMiddleware(); // Data access middleware - requires user to have unlocked data keys const requireDataAccess = authManager.createDataAccessMiddleware(); @@ -413,7 +414,7 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => { // Route: Get OIDC configuration // GET /users/oidc-config -router.get("/oidc-config", async (req, res) => { +router.get("/oidc-config", authenticateJWT, async (req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") @@ -956,7 +957,7 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => { // Route: Count users // GET /users/count -router.get("/count", async (req, res) => { +router.get("/count", authenticateJWT, async (req, res) => { try { const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") @@ -971,7 +972,7 @@ router.get("/count", async (req, res) => { // Route: DB health check (actually queries DB) // GET /users/db-health -router.get("/db-health", async (req, res) => { +router.get("/db-health", requireAdmin, async (req, res) => { try { db.$client.prepare("SELECT 1").get(); res.json({ status: "ok" }); @@ -983,7 +984,7 @@ router.get("/db-health", async (req, res) => { // Route: Get registration allowed status // GET /users/registration-allowed -router.get("/registration-allowed", async (req, res) => { +router.get("/registration-allowed", authenticateJWT, async (req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index d7ad7cf2..43033ca5 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -155,6 +155,53 @@ class AuthManager { }; } + /** + * Admin middleware - requires user to be authenticated and have admin privileges + */ + createAdminMiddleware() { + return async (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers["authorization"]; + if (!authHeader?.startsWith("Bearer ")) { + return res.status(401).json({ error: "Missing Authorization header" }); + } + + const token = authHeader.split(" ")[1]; + const payload = await this.verifyJWTToken(token); + + if (!payload) { + return res.status(401).json({ error: "Invalid token" }); + } + + // Check if user is admin + try { + const { db } = await import("../database/db/index.js"); + const { users } = await import("../database/db/schema.js"); + const { eq } = await import("drizzle-orm"); + + const user = await db.select().from(users).where(eq(users.id, payload.userId)); + + if (!user || user.length === 0 || !user[0].is_admin) { + databaseLogger.warn("Non-admin user attempted to access admin endpoint", { + operation: "admin_access_denied", + userId: payload.userId, + endpoint: req.path, + }); + return res.status(403).json({ error: "Admin access required" }); + } + + (req as any).userId = payload.userId; + (req as any).pendingTOTP = payload.pendingTOTP; + next(); + } catch (error) { + databaseLogger.error("Failed to verify admin privileges", error, { + operation: "admin_check_failed", + userId: payload.userId, + }); + return res.status(500).json({ error: "Failed to verify admin privileges" }); + } + }; + } + /** * User logout */