Initial commit - Current state of vip-coordinator

This commit is contained in:
2026-01-24 09:30:26 +01:00
commit aa900505b9
96 changed files with 31868 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
import express, { Request, Response, NextFunction } from 'express';
import {
fetchAuth0UserProfile,
isAuth0Configured,
verifyAccessToken,
VerifiedAccessToken,
Auth0UserProfile,
getCachedProfile,
cacheAuth0Profile
} from '../config/simpleAuth';
import databaseService from '../services/databaseService';
type AuthedRequest = Request & {
auth?: {
token: string;
claims: VerifiedAccessToken;
profile?: Auth0UserProfile | null;
};
user?: any;
};
const router = express.Router();
function mapUserForResponse(user: any) {
return {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
approval_status: user.approval_status,
created_at: user.created_at,
last_login: user.last_login,
provider: 'auth0'
};
}
async function syncUserWithDatabase(claims: VerifiedAccessToken, token: string): Promise<{ user: any; profile: Auth0UserProfile | null }> {
const auth0Id = claims.sub;
const initialAdminEmails = (process.env.INITIAL_ADMIN_EMAILS || '')
.split(',')
.map(email => email.trim().toLowerCase())
.filter(Boolean);
let profile: Auth0UserProfile | null = null;
let user = await databaseService.getUserById(auth0Id);
if (user) {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
const isSeedAdmin = initialAdminEmails.includes((user.email || '').toLowerCase());
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
return { user, profile };
}
const cacheKey = auth0Id;
profile = getCachedProfile(cacheKey) || null;
if (!profile) {
profile = await fetchAuth0UserProfile(token, cacheKey, claims.exp);
cacheAuth0Profile(cacheKey, profile, claims.exp);
}
if (!profile.email) {
throw new Error('Auth0 profile did not include an email address');
}
const existingByEmail = await databaseService.getUserByEmail(profile.email);
if (existingByEmail && existingByEmail.id !== auth0Id) {
await databaseService.migrateUserId(existingByEmail.id, auth0Id);
user = await databaseService.getUserById(auth0Id);
} else if (existingByEmail) {
user = existingByEmail;
}
const displayName = profile.name || profile.nickname || profile.email;
const picture = typeof profile.picture === 'string' ? profile.picture : undefined;
const isSeedAdmin = initialAdminEmails.includes(profile.email.toLowerCase());
if (!user) {
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = isSeedAdmin
? 'administrator'
: approvedUserCount === 0
? 'administrator'
: 'coordinator';
user = await databaseService.createUser({
id: auth0Id,
google_id: auth0Id,
email: profile.email,
name: displayName,
profile_picture_url: picture,
role
});
if (role === 'administrator') {
user = await databaseService.updateUserApprovalStatus(profile.email, 'approved');
}
} else {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
}
return { user, profile };
}
export async function requireAuth(req: AuthedRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const claims = await verifyAccessToken(token);
const { user, profile } = await syncUserWithDatabase(claims, token);
req.auth = { token, claims, profile };
req.user = user;
if (user.approval_status !== 'approved') {
return res.status(403).json({
error: 'pending_approval',
message: 'Your account is pending administrator approval.',
user: mapUserForResponse(user)
});
}
return next();
} catch (error: any) {
console.error('Auth0 token verification failed:', error);
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
export function requireRole(roles: string[]) {
return (req: AuthedRequest, res: Response, next: NextFunction) => {
const user = req.user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
router.get('/setup', async (_req: Request, res: Response) => {
try {
const userCount = await databaseService.getUserCount();
res.json({
setupCompleted: isAuth0Configured(),
firstAdminCreated: userCount > 0,
oauthConfigured: isAuth0Configured(),
authProvider: 'auth0'
});
} catch (error) {
console.error('Error checking setup status:', error);
res.status(500).json({ error: 'Database connection error' });
}
});
router.get('/me', requireAuth, async (req: AuthedRequest, res: Response) => {
res.json({
user: mapUserForResponse(req.user),
auth0: {
sub: req.auth?.claims.sub,
scope: req.auth?.claims.scope
}
});
});
router.post('/logout', (_req: Request, res: Response) => {
res.json({ message: 'Logged out successfully' });
});
router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => {
res.json({
authenticated: true,
user: mapUserForResponse(req.user)
});
});
// USER MANAGEMENT ENDPOINTS
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (_req: Request, res: Response) => {
try {
const users = await databaseService.getAllUsers();
res.json(users.map(mapUserForResponse));
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// Update user role (admin only)
router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const { role } = req.body;
if (!['administrator', 'coordinator', 'driver'].includes(role)) {
return res.status(400).json({ error: 'Invalid role' });
}
try {
const user = await databaseService.updateUserRole(email, role);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
user: mapUserForResponse(user)
});
} catch (error) {
console.error('Error updating user role:', error);
res.status(500).json({ error: 'Failed to update user role' });
}
});
// Delete user (admin only)
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: AuthedRequest, res: Response) => {
const { email } = req.params;
const currentUser = req.user;
// Prevent admin from deleting themselves
if (email === currentUser.email) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
try {
const deletedUser = await databaseService.deleteUser(email);
if (!deletedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ success: true, message: 'User deleted successfully' });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ error: 'Failed to delete user' });
}
});
// Get user by email (admin only)
router.get('/users/:email', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
try {
const user = await databaseService.getUserByEmail(email);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(mapUserForResponse(user));
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Failed to fetch user' });
}
});
// USER APPROVAL ENDPOINTS
// Get pending users (admin only)
router.get('/users/pending/list', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const pendingUsers = await databaseService.getPendingUsers();
const userList = pendingUsers.map(mapUserForResponse);
res.json(userList);
} catch (error) {
console.error('Error fetching pending users:', error);
res.status(500).json({ error: 'Failed to fetch pending users' });
}
});
// Approve or deny user (admin only)
router.patch('/users/:email/approval', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const { status } = req.body;
if (!['approved', 'denied'].includes(status)) {
return res.status(400).json({ error: 'Invalid approval status. Must be "approved" or "denied"' });
}
try {
const user = await databaseService.updateUserApprovalStatus(email, status);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
message: `User ${status} successfully`,
user: mapUserForResponse(user)
});
} catch (error) {
console.error('Error updating user approval:', error);
res.status(500).json({ error: 'Failed to update user approval' });
}
});
export default router;