import express from "express"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import { v4 as uuidv4 } from "uuid"; import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { validate, loginValidation, registerValidation, } from "../middleware/validate.js"; const router = express.Router(); // In-memory store (replace with database in production) const users = new Map(); const webAuthnCredentials = new Map(); const sessions = new Map(); // Helper to generate JWT const generateToken = (user) => { return jwt.sign( { id: user.id, email: user.email, role: user.role, name: user.name }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || "7d" }, ); }; // Register router.post( "/register", validate(registerValidation), async (req, res, next) => { try { const { name, email, password } = req.body; // Check if user exists const existingUser = Array.from(users.values()).find( (u) => u.email === email, ); if (existingUser) { return res.status(400).json({ error: "Email already registered" }); } // Hash password const hashedPassword = await bcrypt.hash(password, 12); // Create user const user = { id: uuidv4(), name, email, password: hashedPassword, role: users.size === 0 ? "admin" : "volunteer", // First user is admin createdAt: new Date().toISOString(), }; users.set(user.id, user); // Generate token const token = generateToken(user); res.status(201).json({ message: "User registered successfully", token, user: { id: user.id, name: user.name, email: user.email, role: user.role, }, }); } catch (error) { next(error); } }, ); // Login router.post("/login", validate(loginValidation), async (req, res, next) => { try { const { email, password } = req.body; // Find user const user = Array.from(users.values()).find((u) => u.email === email); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); } // Check password const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(401).json({ error: "Invalid credentials" }); } // Generate token const token = generateToken(user); // Create session const sessionId = uuidv4(); sessions.set(sessionId, { userId: user.id, createdAt: new Date().toISOString(), lastActive: new Date().toISOString(), }); res.json({ message: "Login successful", token, user: { id: user.id, name: user.name, email: user.email, role: user.role, }, }); } catch (error) { next(error); } }); // Get current user router.get("/me", (req, res) => { // This route should be protected - req.user comes from auth middleware if (!req.user) { return res.status(401).json({ error: "Not authenticated" }); } const user = users.get(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } res.json({ user: { id: user.id, name: user.name, email: user.email, role: user.role }, }); }); // Logout router.post("/logout", (req, res) => { // Invalidate session on client side res.json({ message: "Logged out successfully" }); }); // Google OAuth router.post("/google", async (req, res, next) => { try { const { token } = req.body; // Verify Google token (simplified - use passport-google-oauth20 in production) // For now, return mock response res.json({ message: "Google OAuth not yet configured", token: null, user: null, }); } catch (error) { next(error); } }); // WebAuthn Registration Options router.post("/webauthn/register-options", async (req, res, next) => { try { if (!req.user) { return res.status(401).json({ error: "Authentication required" }); } const user = users.get(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } const options = await generateRegistrationOptions({ rpName: process.env.RP_NAME || "Worship Platform", rpID: process.env.RP_ID || "localhost", userID: user.id, userName: user.email, userDisplayName: user.name, attestationType: "none", authenticatorSelection: { residentKey: "preferred", userVerification: "preferred", }, }); // Store challenge for verification sessions.set(`webauthn-${user.id}`, { challenge: options.challenge, createdAt: new Date().toISOString(), }); res.json(options); } catch (error) { next(error); } }); // WebAuthn Registration Verification router.post("/webauthn/register", async (req, res, next) => { try { if (!req.user) { return res.status(401).json({ error: "Authentication required" }); } const session = sessions.get(`webauthn-${req.user.id}`); if (!session) { return res.status(400).json({ error: "Registration session expired" }); } const verification = await verifyRegistrationResponse({ response: req.body, expectedChallenge: session.challenge, expectedOrigin: process.env.RP_ORIGIN || "http://localhost:5100", expectedRPID: process.env.RP_ID || "localhost", }); if (!verification.verified) { return res .status(400) .json({ error: "Registration verification failed" }); } // Store credential const { registrationInfo } = verification; webAuthnCredentials.set(req.user.id, { credentialID: registrationInfo.credentialID, credentialPublicKey: registrationInfo.credentialPublicKey, counter: registrationInfo.counter, transports: req.body.response.transports, }); // Clean up session sessions.delete(`webauthn-${req.user.id}`); res.json({ message: "Biometric registration successful" }); } catch (error) { next(error); } }); // WebAuthn Authentication Options router.post("/webauthn/authenticate-options", async (req, res, next) => { try { const { email } = req.body; // Find user by email or use session const user = email ? Array.from(users.values()).find((u) => u.email === email) : null; const options = await generateAuthenticationOptions({ rpID: process.env.RP_ID || "localhost", userVerification: "preferred", allowCredentials: user && webAuthnCredentials.has(user.id) ? [ { id: webAuthnCredentials.get(user.id).credentialID, type: "public-key", transports: webAuthnCredentials.get(user.id).transports, }, ] : [], }); // Store challenge const sessionKey = user ? `webauthn-auth-${user.id}` : `webauthn-auth-temp`; sessions.set(sessionKey, { challenge: options.challenge, userId: user?.id, createdAt: new Date().toISOString(), }); res.json(options); } catch (error) { next(error); } }); // WebAuthn Authentication Verification router.post("/webauthn/authenticate", async (req, res, next) => { try { // Find credential and user let userId = null; let credential = null; for (const [id, cred] of webAuthnCredentials.entries()) { if ( Buffer.compare( cred.credentialID, Buffer.from(req.body.id, "base64url"), ) === 0 ) { userId = id; credential = cred; break; } } if (!userId || !credential) { return res.status(400).json({ error: "Credential not found" }); } const session = sessions.get(`webauthn-auth-${userId}`); if (!session) { return res.status(400).json({ error: "Authentication session expired" }); } const verification = await verifyAuthenticationResponse({ response: req.body, expectedChallenge: session.challenge, expectedOrigin: process.env.RP_ORIGIN || "http://localhost:5100", expectedRPID: process.env.RP_ID || "localhost", authenticator: credential, }); if (!verification.verified) { return res .status(400) .json({ error: "Authentication verification failed" }); } // Update counter credential.counter = verification.authenticationInfo.newCounter; const user = users.get(userId); const token = generateToken(user); // Clean up session sessions.delete(`webauthn-auth-${userId}`); res.json({ message: "Biometric authentication successful", token, user: { id: user.id, name: user.name, email: user.email, role: user.role, }, }); } catch (error) { next(error); } }); // Switch profile router.post("/switch-profile", (req, res, next) => { try { const { profileId } = req.body; // In a real app, this would switch to a different profile/user context const user = users.get(profileId); if (!user) { return res.status(404).json({ error: "Profile not found" }); } const token = generateToken(user); res.json({ message: "Profile switched successfully", token, user: { id: user.id, name: user.name, email: user.email, role: user.role, }, }); } catch (error) { next(error); } }); export default router;