Initial commit - Church Music Database
This commit is contained in:
380
new-site/backend/api/auth.js
Normal file
380
new-site/backend/api/auth.js
Normal file
@@ -0,0 +1,380 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user