381 lines
9.4 KiB
JavaScript
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;
|