170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { Router } from "@oak/oak";
|
|
import { hash, compare, genSalt } from "bcrypt";
|
|
import { db } from "../config/database.ts";
|
|
import { config } from "../config/env.ts";
|
|
|
|
// Helper function to hash password with proper salt generation
|
|
async function hashPassword(password: string): Promise<string> {
|
|
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
|
return await hash(password, salt);
|
|
}
|
|
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
|
|
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
|
|
import type { User, LoginRequest, ChangePasswordRequest } 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<User[]>(
|
|
"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<User[]>(
|
|
"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<User[]>(
|
|
"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;
|