import { Router } from "@oak/oak"; import { compare, genSalt, hash } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; async function hashPassword(password: string): Promise { const salt = await genSalt(config.BCRYPT_ROUNDS); return await hash(password, salt); } import { authenticateToken, generateToken, getCurrentUser, } from "../middleware/auth.ts"; import { isStrongPassword, sanitizeInput } from "../middleware/security.ts"; import type { ChangePasswordRequest, LoginRequest, User, } from "../types/index.ts"; const router = new Router(); // Login router.post("/login", async (ctx) => { try { const body = await ctx.request.body.json() as LoginRequest; const { username, password } = body; // Input validation if (!username || !password) { ctx.response.status = 400; ctx.response.body = { error: "Username and password required" }; return; } // Sanitize input const sanitizedUsername = sanitizeInput(username); // Query user const users = await db.query( "SELECT * FROM users WHERE username = ? AND is_active = TRUE", [sanitizedUsername], ); if (users.length === 0) { // Use generic message to prevent user enumeration ctx.response.status = 401; ctx.response.body = { error: "Invalid credentials" }; return; } const user = users[0]; // Verify password const validPassword = await compare(password, user.password!); if (!validPassword) { ctx.response.status = 401; ctx.response.body = { error: "Invalid credentials" }; return; } // Generate JWT token const token = await generateToken({ id: user.id, username: user.username, role: user.role, departmentId: user.department_id, }); // Return user data without password const { password: _, ...userWithoutPassword } = user; ctx.response.body = { token, user: userWithoutPassword, }; } catch (error) { console.error("Login error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }); // Get current user router.get("/me", authenticateToken, async (ctx) => { try { const currentUser = getCurrentUser(ctx); const users = await db.query( "SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?", [currentUser.id], ); if (users.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "User not found" }; return; } ctx.response.body = users[0]; } catch (error) { console.error("Get user error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }); // Change password router.post("/change-password", authenticateToken, async (ctx) => { try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as ChangePasswordRequest; const { currentPassword, newPassword } = body; // Input validation if (!currentPassword || !newPassword) { ctx.response.status = 400; ctx.response.body = { error: "Current and new password required" }; return; } // Validate new password strength (only enforce in production or if explicitly enabled) if (config.isProduction()) { const passwordCheck = isStrongPassword(newPassword); if (!passwordCheck.valid) { ctx.response.status = 400; ctx.response.body = { error: passwordCheck.message }; return; } } else if (newPassword.length < 6) { ctx.response.status = 400; ctx.response.body = { error: "Password must be at least 6 characters" }; return; } // Get current password hash const users = await db.query( "SELECT password FROM users WHERE id = ?", [currentUser.id], ); if (users.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "User not found" }; return; } // Verify current password const validPassword = await compare(currentPassword, users[0].password!); if (!validPassword) { ctx.response.status = 401; ctx.response.body = { error: "Current password is incorrect" }; return; } // Hash new password with configured rounds const hashedPassword = await hashPassword(newPassword); // Update password await db.execute( "UPDATE users SET password = ? WHERE id = ?", [hashedPassword, currentUser.id], ); ctx.response.body = { message: "Password changed successfully" }; } catch (error) { console.error("Change password error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }); export default router;