2026-06-03 14:00:29 +08:00
const express = require ( 'express' );
const cors = require ( 'cors' );
const path = require ( 'path' );
2026-07-06 10:05:37 +08:00
const fs = require ( 'fs' );
2026-06-03 14:00:29 +08:00
const app = express ();
const PORT = process . env . PORT || 3000 ;
2026-07-06 10:05:37 +08:00
const CASDOOR_ENDPOINT = process . env . CASDOOR_ENDPOINT || 'https://auth.falahos.my'
const CASDOOR_CLIENT_ID = process . env . CASDOOR_CLIENT_ID || ''
2026-06-03 14:00:29 +08:00
app . use ( cors ());
app . use ( express . json ());
2026-07-06 10:05:37 +08:00
// 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' })
}
})
// ─────────────────────────────────────────────────────────────────────────────
2026-06-03 14:00:29 +08:00
app . get ( '/health' , ( _req , res ) => {
2026-07-06 10:05:37 +08:00
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.' );
}
2026-06-03 14:00:29 +08:00
});
2026-07-06 10:05:37 +08:00
// 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' });
}
2026-06-03 14:00:29 +08:00
});
2026-07-06 10:05:37 +08:00
const server = app . listen ( PORT , () => {
2026-06-03 14:00:29 +08:00
console . log ( JSON . stringify ({ ts : new Date (). toISOString (), level : 'info' , service : 'app' , msg : `running on port ${ PORT } ` }));
});
2026-07-06 10:05:37 +08:00
// 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' ));