diff --git a/.env.prod b/.env.prod index c9b8649..fdadb5a 100644 --- a/.env.prod +++ b/.env.prod @@ -8,7 +8,7 @@ DOMAIN=bsa.madeamess.online VITE_API_URL=https://api.bsa.madeamess.online # Authentication Configuration (Secure production keys) -JWT_SECRET=VipCoord2025JwtSecretKey8f9a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z +# JWT_SECRET - No longer needed! Keys are auto-generated and rotated every 24 hours SESSION_SECRET=VipCoord2025SessionSecret9g8f7e6d5c4b3a2z1y0x9w8v7u6t5s4r3q2p1o0n9m8l7k6j5i4h3g2f1e # Google OAuth Configuration diff --git a/.gitignore b/.gitignore index b1f7f79..98bd687 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ desktop.ini # Backup directories (exclude from repo) vip-coordinator-backup-*/ +# Copy directories that shouldn't be in repo +vip-coordinator - Copy/ + # ZIP files (exclude from repo) *.zip diff --git a/backend/src/config/simpleAuth.ts b/backend/src/config/simpleAuth.ts index 618abf1..284df71 100644 --- a/backend/src/config/simpleAuth.ts +++ b/backend/src/config/simpleAuth.ts @@ -1,49 +1,16 @@ -import jwt from 'jsonwebtoken'; +import jwtKeyManager, { User } from '../services/jwtKeyManager'; -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +// JWT Key Manager now handles all token operations with automatic rotation +// No more static JWT_SECRET needed! -export interface User { - id: string; - google_id: string; - email: string; - name: string; - profile_picture_url?: string; - role: 'driver' | 'coordinator' | 'administrator'; - created_at?: string; - last_login?: string; - is_active?: boolean; - updated_at?: string; -} +export { User } from '../services/jwtKeyManager'; export function generateToken(user: User): string { - return jwt.sign( - { - id: user.id, - google_id: user.google_id, - email: user.email, - name: user.name, - profile_picture_url: user.profile_picture_url, - role: user.role - }, - JWT_SECRET, - { expiresIn: '24h' } - ); + return jwtKeyManager.generateToken(user); } export function verifyToken(token: string): User | null { - try { - const decoded = jwt.verify(token, JWT_SECRET) as any; - return { - id: decoded.id, - google_id: decoded.google_id, - email: decoded.email, - name: decoded.name, - profile_picture_url: decoded.profile_picture_url, - role: decoded.role - }; - } catch (error) { - return null; - } + return jwtKeyManager.verifyToken(token); } // Simple Google OAuth2 client using fetch diff --git a/backend/src/index.ts b/backend/src/index.ts index 0bdb2fe..fc006f2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -8,6 +8,7 @@ import scheduleValidationService from './services/scheduleValidationService'; import FlightTrackingScheduler from './services/flightTrackingScheduler'; import enhancedDataService from './services/enhancedDataService'; import databaseService from './services/databaseService'; +import jwtKeyManager from './services/jwtKeyManager'; // Initialize JWT Key Manager dotenv.config(); @@ -745,6 +746,34 @@ app.post('/api/admin/test-api/:apiType', async (req: Request, res: Response) => } }); +// JWT Key Management endpoints (admin only) +app.get('/api/admin/jwt-status', requireAuth, requireRole(['administrator']), (req: Request, res: Response) => { + const jwtKeyManager = require('./services/jwtKeyManager').default; + const status = jwtKeyManager.getStatus(); + + res.json({ + keyRotationEnabled: true, + rotationInterval: '24 hours', + gracePeriod: '24 hours', + ...status, + message: 'JWT keys are automatically rotated every 24 hours for enhanced security' + }); +}); + +app.post('/api/admin/jwt-rotate', requireAuth, requireRole(['administrator']), (req: Request, res: Response) => { + const jwtKeyManager = require('./services/jwtKeyManager').default; + + try { + jwtKeyManager.forceRotation(); + res.json({ + success: true, + message: 'JWT key rotation triggered successfully. New tokens will use the new key.' + }); + } catch (error) { + res.status(500).json({ error: 'Failed to rotate JWT keys' }); + } +}); + // Initialize database and start server async function startServer() { try { diff --git a/backend/src/services/jwtKeyManager.ts b/backend/src/services/jwtKeyManager.ts new file mode 100644 index 0000000..1209a90 --- /dev/null +++ b/backend/src/services/jwtKeyManager.ts @@ -0,0 +1,183 @@ +import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; + +export interface User { + id: string; + google_id: string; + email: string; + name: string; + profile_picture_url?: string; + role: 'driver' | 'coordinator' | 'administrator'; + created_at?: string; + last_login?: string; + is_active?: boolean; + updated_at?: string; +} + +class JWTKeyManager { + private currentSecret: string; + private previousSecret: string | null = null; + private rotationInterval: NodeJS.Timeout | null = null; + private gracePeriodTimeout: NodeJS.Timeout | null = null; + + constructor() { + console.log('๐Ÿ”‘ Initializing JWT Key Manager with automatic rotation'); + this.currentSecret = this.generateSecret(); + this.startRotation(); + } + + private generateSecret(): string { + const secret = crypto.randomBytes(64).toString('hex'); + console.log('๐Ÿ”„ Generated new JWT signing key (length:', secret.length, 'chars)'); + return secret; + } + + private startRotation() { + // Rotate every 24 hours (86400000 ms) + this.rotationInterval = setInterval(() => { + this.rotateKey(); + }, 24 * 60 * 60 * 1000); + + console.log('โฐ JWT key rotation scheduled every 24 hours'); + + // Also rotate on startup after 1 hour to test the system + setTimeout(() => { + console.log('๐Ÿงช Performing initial key rotation test...'); + this.rotateKey(); + }, 60 * 60 * 1000); // 1 hour + } + + private rotateKey() { + console.log('๐Ÿ”„ Rotating JWT signing key...'); + + // Store current secret as previous + this.previousSecret = this.currentSecret; + + // Generate new current secret + this.currentSecret = this.generateSecret(); + + console.log('โœ… JWT key rotation completed. Grace period: 24 hours'); + + // Clear any existing grace period timeout + if (this.gracePeriodTimeout) { + clearTimeout(this.gracePeriodTimeout); + } + + // Clean up previous secret after 24 hours (grace period) + this.gracePeriodTimeout = setTimeout(() => { + this.previousSecret = null; + console.log('๐Ÿงน Grace period ended. Previous JWT key cleaned up'); + }, 24 * 60 * 60 * 1000); + } + + generateToken(user: User): string { + const payload = { + id: user.id, + google_id: user.google_id, + email: user.email, + name: user.name, + profile_picture_url: user.profile_picture_url, + role: user.role, + iat: Math.floor(Date.now() / 1000) // Issued at time + }; + + return jwt.sign(payload, this.currentSecret, { + expiresIn: '24h', + issuer: 'vip-coordinator', + audience: 'vip-coordinator-users' + }); + } + + verifyToken(token: string): User | null { + try { + // Try current secret first + const decoded = jwt.verify(token, this.currentSecret, { + issuer: 'vip-coordinator', + audience: 'vip-coordinator-users' + }) as any; + + return { + id: decoded.id, + google_id: decoded.google_id, + email: decoded.email, + name: decoded.name, + profile_picture_url: decoded.profile_picture_url, + role: decoded.role + }; + } catch (error) { + // Try previous secret during grace period + if (this.previousSecret) { + try { + const decoded = jwt.verify(token, this.previousSecret, { + issuer: 'vip-coordinator', + audience: 'vip-coordinator-users' + }) as any; + + console.log('๐Ÿ”„ Token verified using previous key (grace period)'); + + return { + id: decoded.id, + google_id: decoded.google_id, + email: decoded.email, + name: decoded.name, + profile_picture_url: decoded.profile_picture_url, + role: decoded.role + }; + } catch (gracePeriodError) { + console.log('โŒ Token verification failed with both current and previous keys'); + return null; + } + } + + console.log('โŒ Token verification failed:', error instanceof Error ? error.message : 'Unknown error'); + return null; + } + } + + // Get status for monitoring/debugging + getStatus() { + return { + hasCurrentKey: !!this.currentSecret, + hasPreviousKey: !!this.previousSecret, + rotationActive: !!this.rotationInterval, + gracePeriodActive: !!this.gracePeriodTimeout + }; + } + + // Cleanup on shutdown + destroy() { + console.log('๐Ÿ›‘ Shutting down JWT Key Manager...'); + + if (this.rotationInterval) { + clearInterval(this.rotationInterval); + this.rotationInterval = null; + } + + if (this.gracePeriodTimeout) { + clearTimeout(this.gracePeriodTimeout); + this.gracePeriodTimeout = null; + } + + console.log('โœ… JWT Key Manager shutdown complete'); + } + + // Manual rotation for testing/emergency + forceRotation() { + console.log('๐Ÿšจ Manual key rotation triggered'); + this.rotateKey(); + } +} + +// Singleton instance +export const jwtKeyManager = new JWTKeyManager(); + +// Graceful shutdown handling +process.on('SIGTERM', () => { + jwtKeyManager.destroy(); +}); + +process.on('SIGINT', () => { + jwtKeyManager.destroy(); +}); + +export default jwtKeyManager; \ No newline at end of file diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index bbe5641..c6fd797 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -1,6 +1,6 @@ // API Configuration -// Use environment variable with fallback to production URL -export const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.bsa.madeamess.online'; +// Use environment variable with fallback to localhost for development +export const API_BASE_URL = (import.meta as any).env.VITE_API_URL || 'http://localhost:3000'; // Helper function for API calls export const apiCall = (endpoint: string, options?: RequestInit) => {