(Feat): Initial Commit
This commit is contained in:
163
backend-deno/routes/auth.ts
Normal file
163
backend-deno/routes/auth.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { hash, compare } from "bcrypt";
|
||||
import { db } from "../config/database.ts";
|
||||
import { config } from "../config/env.ts";
|
||||
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 hash(newPassword, config.BCRYPT_ROUNDS);
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user