@@ -368,7 +368,11 @@ class RailwayMCPServer {
368368 if ( this . pgPool ) {
369369 await this . pgPool . query ( 'DELETE FROM admin_sessions WHERE expires_at <= NOW()' ) ;
370370 } else if ( this . db ) {
371- this . db . prepare ( 'DELETE FROM admin_sessions WHERE datetime(expires_at) <= datetime("now")' ) . run ( ) ;
371+ // Check if table exists before cleanup
372+ const tableExists = this . db . prepare ( `SELECT name FROM sqlite_master WHERE type='table' AND name='admin_sessions'` ) . get ( ) ;
373+ if ( tableExists ) {
374+ this . db . prepare ( 'DELETE FROM admin_sessions WHERE datetime(expires_at) <= datetime("now")' ) . run ( ) ;
375+ }
372376 }
373377 } catch ( e ) {
374378 console . warn ( 'Admin session cleanup failed:' , e ) ;
@@ -1226,6 +1230,67 @@ loadSessions();
12261230 }
12271231 } ) ;
12281232 }
1233+
1234+ // Admin login/logout routes
1235+ this . app . get ( '/admin/login' , ( _req , res ) => {
1236+ res . setHeader ( 'Content-Type' , 'text/html' ) ;
1237+ res . send ( `<!doctype html><html><head><meta charset="utf-8"/><title>Admin Login</title>
1238+ <style>body{font-family:system-ui;margin:40px} input{padding:8px;margin:4px} button{padding:8px}</style></head>
1239+ <body><h3>Admin Login</h3>
1240+ <p>Paste an admin API key to manage projects and members.</p>
1241+ <form method="POST" action="/admin/login">
1242+ <input type="password" name="apiKey" placeholder="sk-..." style="min-width:360px" required/>
1243+ <div><button type="submit">Login</button></div>
1244+ <p style="color:#666">Your key is validated server-side and not stored in the browser; a short-lived session cookie is created.</p>
1245+ </form>
1246+ </body></html>` ) ;
1247+ } ) ;
1248+
1249+ // Accept urlencoded form
1250+ this . app . post ( '/admin/login' , express . urlencoded ( { extended : false } ) , async ( req , res ) => {
1251+ try {
1252+ const apiKey = req . body ?. apiKey || '' ;
1253+ if ( ! apiKey ) return res . status ( 400 ) . send ( 'Missing API key' ) ;
1254+ const u = await this . validateApiKey ( apiKey ) ;
1255+ if ( ! u || ( u as any ) . role !== 'admin' ) return res . status ( 403 ) . send ( 'Not an admin API key' ) ;
1256+ // Create DB-backed admin session and sign JWT
1257+ const jti = Math . random ( ) . toString ( 36 ) . slice ( 2 ) + Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
1258+ const hours = parseInt ( process . env [ 'ADMIN_SESSION_HOURS' ] || '8' , 10 ) ;
1259+ const expMs = Date . now ( ) + hours * 3600 * 1000 ;
1260+ const expDateIso = new Date ( expMs ) . toISOString ( ) ;
1261+ const ua = req . headers [ 'user-agent' ] || '' ;
1262+ const ip = ( req . headers [ 'x-forwarded-for' ] as string ) || req . socket . remoteAddress || '' ;
1263+ if ( this . pgPool ) {
1264+ await this . pgPool . query ( 'INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES ($1, $2, $3, $4, $5)' , [ jti , ( u as any ) . id , expDateIso , ua , ip ] ) ;
1265+ } else {
1266+ this . db . prepare ( 'INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES (?, ?, ?, ?, ?)' ) . run ( jti , ( u as any ) . id , expDateIso , ua , ip ) ;
1267+ }
1268+ const token = jwt . sign ( { sub : ( u as any ) . id , role : 'admin' , jti } , process . env [ 'ADMIN_JWT_SECRET' ] || 'dev-admin-secret' , { expiresIn : hours + 'h' } ) ;
1269+ setJwtCookie ( res , token ) ;
1270+ res . redirect ( '/admin' ) ;
1271+ } catch ( e : any ) {
1272+ res . status ( 500 ) . send ( 'Login failed' ) ;
1273+ }
1274+ } ) ;
1275+
1276+ this . app . get ( '/admin/logout' , async ( req , res ) => {
1277+ const cookies = parseCookies ( req . headers . cookie ) ;
1278+ const t = cookies [ 'sm_admin_jwt' ] ;
1279+ if ( t ) {
1280+ const verified = verifyAdminJwt ( t ) ;
1281+ if ( verified ) {
1282+ try {
1283+ if ( this . pgPool ) {
1284+ await this . pgPool . query ( 'DELETE FROM admin_sessions WHERE id = $1' , [ verified . jti ] ) ;
1285+ } else {
1286+ this . db . prepare ( 'DELETE FROM admin_sessions WHERE id = ?' ) . run ( verified . jti ) ;
1287+ }
1288+ } catch { }
1289+ }
1290+ }
1291+ clearJwtCookie ( res ) ;
1292+ res . redirect ( '/admin/login' ) ;
1293+ } ) ;
12291294 }
12301295
12311296 private setupWebSocket ( ) : void {
@@ -1480,61 +1545,3 @@ process.on('SIGINT', () => {
14801545 console . log ( 'Shutting down...' ) ;
14811546 process . exit ( 0 ) ;
14821547} ) ;
1483- // Admin login/logout
1484- this . app . get ( '/admin/login' , ( _req , res ) => {
1485- res . setHeader ( 'Content-Type' , 'text/html' ) ;
1486- res . send ( `<!doctype html><html><head><meta charset="utf-8"/><title>Admin Login</title>
1487- <style>body{font-family:system-ui;margin:40px} input{padding:8px;margin:4px} button{padding:8px}</style></head>
1488- <body><h3>Admin Login</h3>
1489- <p>Paste an admin API key to manage projects and members.</p>
1490- <form method="POST" action="/admin/login">
1491- <input type="password" name="apiKey" placeholder="sk-..." style="min-width:360px" required/>
1492- <div><button type="submit">Login</button></div>
1493- <p style="color:#666">Your key is validated server-side and not stored in the browser; a short-lived session cookie is created.</p>
1494- </form>
1495- </body></html>` ) ;
1496- } ) ;
1497- // Accept urlencoded form
1498- this . app . post ( '/admin/login' , express . urlencoded ( { extended : false } ) , async ( req , res ) => {
1499- try {
1500- const apiKey = req . body ?. apiKey || '' ;
1501- if ( ! apiKey ) return res . status ( 400 ) . send ( 'Missing API key' ) ;
1502- const u = await this . validateApiKey ( apiKey ) ;
1503- if ( ! u || ( u as any ) . role !== 'admin' ) return res . status ( 403 ) . send ( 'Not an admin API key' ) ;
1504- // Create DB-backed admin session and sign JWT
1505- const jti = Math . random ( ) . toString ( 36 ) . slice ( 2 ) + Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
1506- const hours = parseInt ( process . env [ 'ADMIN_SESSION_HOURS' ] || '8' , 10 ) ;
1507- const expMs = Date . now ( ) + hours * 3600 * 1000 ;
1508- const expDateIso = new Date ( expMs ) . toISOString ( ) ;
1509- const ua = req . headers [ 'user-agent' ] || '' ;
1510- const ip = ( req . headers [ 'x-forwarded-for' ] as string ) || req . socket . remoteAddress || '' ;
1511- if ( this . pgPool ) {
1512- await this . pgPool . query ( 'INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES ($1, $2, $3, $4, $5)' , [ jti , ( u as any ) . id , expDateIso , ua , ip ] ) ;
1513- } else {
1514- this . db . prepare ( 'INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES (?, ?, ?, ?, ?)' ) . run ( jti , ( u as any ) . id , expDateIso , ua , ip ) ;
1515- }
1516- const token = jwt . sign ( { sub : ( u as any ) . id , role : 'admin' , jti } , process . env [ 'ADMIN_JWT_SECRET' ] || 'dev-admin-secret' , { expiresIn : hours + 'h' } ) ;
1517- setJwtCookie ( res , token ) ;
1518- res . redirect ( '/admin' ) ;
1519- } catch ( e : any ) {
1520- res . status ( 500 ) . send ( 'Login failed' ) ;
1521- }
1522- } ) ;
1523- this . app . get ( '/admin/logout' , async ( req , res ) => {
1524- const cookies = parseCookies ( req . headers . cookie ) ;
1525- const t = cookies [ 'sm_admin_jwt' ] ;
1526- if ( t ) {
1527- const verified = verifyAdminJwt ( t ) ;
1528- if ( verified ) {
1529- try {
1530- if ( this . pgPool ) {
1531- await this . pgPool . query ( 'DELETE FROM admin_sessions WHERE id = $1' , [ verified . jti ] ) ;
1532- } else {
1533- this . db . prepare ( 'DELETE FROM admin_sessions WHERE id = ?' ) . run ( verified . jti ) ;
1534- }
1535- } catch { }
1536- }
1537- }
1538- clearJwtCookie ( res ) ;
1539- res . redirect ( '/admin/login' ) ;
1540- } ) ;
0 commit comments