Backup: 2025-06-07 18:32 - Production setup complete

[Restore from backup: vip-coordinator-backup-2025-06-07-18-32-production-setup-complete]
This commit is contained in:
2025-06-07 18:32:00 +02:00
parent aa900505b9
commit ae3702c3b1
32 changed files with 2120 additions and 1494 deletions

View File

@@ -1,176 +1,64 @@
import express, { Request, Response, NextFunction } from 'express';
import {
fetchAuth0UserProfile,
isAuth0Configured,
verifyAccessToken,
VerifiedAccessToken,
Auth0UserProfile,
getCachedProfile,
cacheAuth0Profile
import {
generateToken,
verifyToken,
getGoogleAuthUrl,
exchangeCodeForTokens,
getGoogleUserInfo,
User
} 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) {
// Middleware to check authentication
export function requireAuth(req: Request, 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);
const user = verifyToken(token);
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' });
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
(req as any).user = user;
next();
}
// Middleware to check role
export function requireRole(roles: string[]) {
return (req: AuthedRequest, res: Response, next: NextFunction) => {
const user = req.user;
return (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
router.get('/setup', async (_req: Request, res: Response) => {
// Get current user
router.get('/me', requireAuth, (req: Request, res: Response) => {
res.json((req as any).user);
});
// Setup status endpoint (required by frontend)
router.get('/setup', async (req: Request, res: Response) => {
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
try {
const userCount = await databaseService.getUserCount();
res.json({
setupCompleted: isAuth0Configured(),
setupCompleted: !!(clientId && clientSecret && clientId !== 'your-google-client-id-from-console'),
firstAdminCreated: userCount > 0,
oauthConfigured: isAuth0Configured(),
authProvider: 'auth0'
oauthConfigured: !!(clientId && clientSecret)
});
} catch (error) {
console.error('Error checking setup status:', error);
@@ -178,35 +66,206 @@ router.get('/setup', async (_req: Request, res: Response) => {
}
});
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
}
});
// Start Google OAuth flow
router.get('/google', (req: Request, res: Response) => {
try {
const authUrl = getGoogleAuthUrl();
res.redirect(authUrl);
} catch (error) {
console.error('Error starting Google OAuth:', error);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
res.redirect(`${frontendUrl}?error=oauth_not_configured`);
}
});
router.post('/logout', (_req: Request, res: Response) => {
// Handle Google OAuth callback (this is where Google redirects back to)
router.get('/google/callback', async (req: Request, res: Response) => {
const { code, error } = req.query;
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
if (error) {
console.error('OAuth error:', error);
return res.redirect(`${frontendUrl}?error=${error}`);
}
if (!code) {
return res.redirect(`${frontendUrl}?error=no_code`);
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code as string);
// Get user info
const googleUser = await getGoogleUserInfo(tokens.access_token);
// Check if user exists or create new user
let user = await databaseService.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin, others need approval
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = approvedUserCount === 0 ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
// Auto-approve first admin, others need approval
if (approvedUserCount === 0) {
await databaseService.updateUserApprovalStatus(googleUser.email, 'approved');
user.approval_status = 'approved';
}
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Check if user is approved
if (user.approval_status !== 'approved') {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
return res.redirect(`${frontendUrl}?error=pending_approval&message=Your account is pending administrator approval`);
}
// Generate JWT token
const token = generateToken(user);
// Redirect to frontend with token
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
} catch (error) {
console.error('Error in OAuth callback:', error);
res.redirect(`${frontendUrl}?error=oauth_failed`);
}
});
// Exchange OAuth code for JWT token (alternative endpoint for frontend)
router.post('/google/exchange', async (req: Request, res: Response) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: 'Authorization code is required' });
}
try {
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code);
// Get user info
const googleUser = await getGoogleUserInfo(tokens.access_token);
// Check if user exists or create new user
let user = await databaseService.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin
const userCount = await databaseService.getUserCount();
const role = userCount === 0 ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Generate JWT token
const token = generateToken(user);
// Return token to frontend
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
} catch (error) {
console.error('Error in OAuth exchange:', error);
res.status(500).json({ error: 'Failed to exchange authorization code' });
}
});
// Get OAuth URL for frontend to redirect to
router.get('/google/url', (req: Request, res: Response) => {
try {
const authUrl = getGoogleAuthUrl();
res.json({ url: authUrl });
} catch (error) {
console.error('Error getting Google OAuth URL:', error);
res.status(500).json({ error: 'OAuth not configured' });
}
});
// Logout
router.post('/logout', (req: Request, res: Response) => {
// With JWT, logout is handled client-side by removing the token
res.json({ message: 'Logged out successfully' });
});
router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => {
res.json({
authenticated: true,
user: mapUserForResponse(req.user)
// Get auth status
router.get('/status', (req: Request, res: Response) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.json({ authenticated: false });
}
const token = authHeader.substring(7);
const user = verifyToken(token);
if (!user) {
return res.json({ authenticated: false });
}
res.json({
authenticated: true,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
});
// USER MANAGEMENT ENDPOINTS
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (_req: Request, res: Response) => {
router.get('/users', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const users = await databaseService.getAllUsers();
res.json(users.map(mapUserForResponse));
const userList = users.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google'
}));
res.json(userList);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
@@ -230,7 +289,12 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
res.json({
success: true,
user: mapUserForResponse(user)
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
console.error('Error updating user role:', error);
@@ -239,9 +303,9 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
});
// Delete user (admin only)
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: AuthedRequest, res: Response) => {
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const currentUser = req.user;
const currentUser = (req as any).user;
// Prevent admin from deleting themselves
if (email === currentUser.email) {
@@ -272,7 +336,17 @@ router.get('/users/:email', requireAuth, requireRole(['administrator']), async (
return res.status(404).json({ error: 'User not found' });
}
res.json(mapUserForResponse(user));
res.json({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google',
approval_status: user.approval_status
});
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Failed to fetch user' });
@@ -286,7 +360,16 @@ router.get('/users/pending/list', requireAuth, requireRole(['administrator']), a
try {
const pendingUsers = await databaseService.getPendingUsers();
const userList = pendingUsers.map(mapUserForResponse);
const userList = pendingUsers.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
provider: 'google',
approval_status: user.approval_status
}));
res.json(userList);
} catch (error) {
@@ -313,7 +396,13 @@ router.patch('/users/:email/approval', requireAuth, requireRole(['administrator'
res.json({
success: true,
message: `User ${status} successfully`,
user: mapUserForResponse(user)
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
approval_status: user.approval_status
}
});
} catch (error) {
console.error('Error updating user approval:', error);