Files

381 lines
9.4 KiB
JavaScript

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;