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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user