const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; const CASDOOR_ENDPOINT = process.env.CASDOOR_ENDPOINT || 'https://auth.falahos.my' const CASDOOR_CLIENT_ID = process.env.CASDOOR_CLIENT_ID || '' app.use(cors()); app.use(express.json()); // AWS-level standard: proper static file serving with cache control const distPath = path.join(__dirname, 'dist'); if (!fs.existsSync(distPath)) { console.warn(JSON.stringify({ ts: new Date().toISOString(), level: 'warn', service: 'app', msg: 'dist directory does not exist! The frontend will not be served.' })); } app.use(express.static(distPath, { maxAge: '1d', // Cache static assets for performance setHeaders: (res, path) => { if (path.endsWith('.html')) { // Don't cache HTML to ensure users get the latest version on reload res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } } })); // ── UmmahID / Casdoor auth ──────────────────────────────────────────────── function decodeJwtPayload(token) { try { const payload = token.split('.')[1] return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) } catch { return {} } } // Returns safe public config — no secrets exposed app.get('/api/auth/config', (_req, res) => { if (!CASDOOR_CLIENT_ID) { return res.status(503).json({ error: 'CASDOOR_CLIENT_ID not configured on server' }) } res.json({ clientId: CASDOOR_CLIENT_ID, endpoint: CASDOOR_ENDPOINT, authorizePath: '/login/oauth/authorize', }) }) // PKCE token exchange — code_verifier replaces client_secret for public clients app.post('/api/auth/token', async (req, res) => { const { code, codeVerifier, redirectUri } = req.body if (!code || !codeVerifier || !redirectUri) { return res.status(400).json({ error: 'Missing code, codeVerifier, or redirectUri' }) } try { const tokenRes = await fetch(`${CASDOOR_ENDPOINT}/api/login/oauth/access_token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'authorization_code', client_id: CASDOOR_CLIENT_ID, code, redirect_uri: redirectUri, code_verifier: codeVerifier, }), }) const data = await tokenRes.json() if (!tokenRes.ok || data.error) { return res.status(400).json({ error: data.error_description || data.error || 'Token exchange failed' }) } const claims = decodeJwtPayload(data.id_token || data.access_token) res.json({ accessToken: data.access_token, user: { id: claims.sub || claims.id || '', email: claims.email || '', fullName: claims.name || claims.preferred_username || '', role: claims['falah/role'] || claims.role || 'user', }, }) } catch (err) { console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'error', service: 'auth', msg: err.message })) res.status(502).json({ error: 'Could not reach Casdoor' }) } }) // ───────────────────────────────────────────────────────────────────────────── app.get('/health', (_req, res) => { res.json({ status: 'healthy', service: 'app', version: '1.3.0', timestamp: new Date().toISOString() }); }); app.get('*', (req, res, next) => { const indexPath = path.join(distPath, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath, (err) => { if (err) { console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'error', service: 'app', msg: 'Error sending index.html', error: err.message })); next(err); // Pass to global error handler } }); } else { res.status(404).send('Not Found: Application build missing.'); } }); // Global Error Handler app.use((err, req, res, next) => { console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'error', service: 'app', msg: 'Unhandled error in Express', error: err.stack || err.message })); if (!res.headersSent) { res.status(500).json({ error: 'Internal Server Error' }); } }); const server = app.listen(PORT, () => { console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'info', service: 'app', msg: `running on port ${PORT}` })); }); // Graceful Shutdown for DevOps / Orchestrators (ECS, K8s, Cloud Run) const shutdown = (signal) => { console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'info', service: 'app', msg: `Received ${signal}. Shutting down gracefully...` })); server.close(() => { console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'info', service: 'app', msg: 'Closed remaining connections.' })); process.exit(0); }); // Force shutdown after 10s if connections linger setTimeout(() => { console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'error', service: 'app', msg: 'Could not close connections in time, forcefully shutting down' })); process.exit(1); }, 10000); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));