import { Router } from "@oak/oak"; import { query, queryOne, execute } from "../db/postgres.ts"; import { hashPassword, verifyPassword, generateAccessToken, generateRefreshToken, generateRandomToken, verifyToken } from "../utils/auth.ts"; import { AppError } from "../middleware/error.ts"; import { authMiddleware } from "../middleware/auth.ts"; import type { User } from "../types/index.ts"; export const authRouter = new Router({ prefix: "/api/auth" }); // Register (first user becomes chef, or must be created by higher role) authRouter.post("/register", async (ctx) => { const body = await ctx.request.body.json(); const { email, password, first_name, last_name, phone, org_slug } = body; if (!email || !password || !first_name || !last_name || !org_slug) { throw new AppError("Missing required fields", 400); } // Find organization const org = await queryOne<{ id: string }>( "SELECT id FROM organizations WHERE slug = $1", [org_slug] ); if (!org) { throw new AppError("Organization not found", 404); } // Check if email already exists const existing = await queryOne<{ id: string }>( "SELECT id FROM users WHERE org_id = $1 AND email = $2", [org.id, email] ); if (existing) { throw new AppError("Email already registered", 409); } // Check if this is the first user (becomes chef) const userCount = await queryOne<{ count: string }>( "SELECT COUNT(*) as count FROM users WHERE org_id = $1", [org.id] ); const isFirstUser = parseInt(userCount?.count || "0") === 0; const role = isFirstUser ? "chef" : "mitarbeiter"; // Hash password and create user const passwordHash = await hashPassword(password); const result = await queryOne<{ id: string }>( `INSERT INTO users (org_id, email, password_hash, role, first_name, last_name, phone, active) VALUES ($1, $2, $3, $4, $5, $6, $7, true) RETURNING id`, [org.id, email, passwordHash, role, first_name, last_name, phone || null] ); ctx.response.status = 201; ctx.response.body = { message: "Registration successful", userId: result?.id, role, isFirstUser }; }); // Login authRouter.post("/login", async (ctx) => { const body = await ctx.request.body.json(); const { email, password, org_slug } = body; if (!email || !password || !org_slug) { throw new AppError("Email, password and organization required", 400); } // Find user with org const user = await queryOne( `SELECT u.*, o.slug as org_slug FROM users u JOIN organizations o ON u.org_id = o.id WHERE u.email = $1 AND o.slug = $2 AND u.active = true`, [email, org_slug] ); if (!user) { throw new AppError("Invalid credentials", 401); } // Verify password const valid = await verifyPassword(password, user.password_hash || ""); if (!valid) { throw new AppError("Invalid credentials", 401); } // Generate tokens const accessToken = await generateAccessToken(user.id, user.org_id, user.role, user.email); const refreshToken = await generateRefreshToken(user.id); // Store refresh token hash const tokenHash = await hashPassword(refreshToken); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); await execute( `INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)`, [user.id, tokenHash, expiresAt] ); // Update last login await execute( `UPDATE users SET last_login = NOW() WHERE id = $1`, [user.id] ); ctx.response.body = { accessToken, refreshToken, user: { id: user.id, email: user.email, role: user.role, first_name: user.first_name, last_name: user.last_name, }, }; }); // Refresh token authRouter.post("/refresh", async (ctx) => { const body = await ctx.request.body.json(); const { refreshToken } = body; if (!refreshToken) { throw new AppError("Refresh token required", 400); } // Verify JWT structure const payload = await verifyToken(refreshToken); if (!payload || (payload as unknown as { type?: string }).type !== "refresh") { throw new AppError("Invalid refresh token", 401); } // Get user const user = await queryOne( `SELECT * FROM users WHERE id = $1 AND active = true`, [payload.sub] ); if (!user) { throw new AppError("User not found", 404); } // Generate new access token const accessToken = await generateAccessToken(user.id, user.org_id, user.role, user.email); ctx.response.body = { accessToken }; }); // Logout (invalidate refresh tokens) authRouter.post("/logout", authMiddleware, async (ctx) => { const userId = ctx.state.auth.user.id; await execute( `DELETE FROM refresh_tokens WHERE user_id = $1`, [userId] ); ctx.response.body = { message: "Logged out successfully" }; }); // Get current user authRouter.get("/me", authMiddleware, async (ctx) => { const userId = ctx.state.auth.user.id; const user = await queryOne( `SELECT id, org_id, email, role, first_name, last_name, phone, avatar_url, created_at, last_login FROM users WHERE id = $1`, [userId] ); if (!user) { throw new AppError("User not found", 404); } ctx.response.body = { user }; }); // Change password authRouter.post("/change-password", authMiddleware, async (ctx) => { const userId = ctx.state.auth.user.id; const body = await ctx.request.body.json(); const { currentPassword, newPassword } = body; if (!currentPassword || !newPassword) { throw new AppError("Current and new password required", 400); } if (newPassword.length < 8) { throw new AppError("Password must be at least 8 characters", 400); } // Get current password hash const user = await queryOne<{ password_hash: string }>( `SELECT password_hash FROM users WHERE id = $1`, [userId] ); if (!user) { throw new AppError("User not found", 404); } // Verify current password const valid = await verifyPassword(currentPassword, user.password_hash); if (!valid) { throw new AppError("Current password is incorrect", 401); } // Hash and update new password const newHash = await hashPassword(newPassword); await execute( `UPDATE users SET password_hash = $1 WHERE id = $2`, [newHash, userId] ); // Invalidate all refresh tokens await execute( `DELETE FROM refresh_tokens WHERE user_id = $1`, [userId] ); ctx.response.body = { message: "Password changed successfully" }; });