import { Hono } from 'hono'; import { serveStatic } from 'hono/bun'; import { cors } from 'hono/cors'; import path from 'path'; import { fileURLToPath } from 'url'; import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import { AuthConfigManager, BackendAuth } from '../auth-module/backend'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = new Hono(); const PORT = process.env.PORT || 3005; // Initialize auth const backendAuth = new BackendAuth({ clientId: process.env.CLIENT_ID, tenantId: process.env.TENANT_ID, clientSecret: process.env.CLIENT_SECRET, }, process.env.PB_DB); // CORS Configuration - Allow requests from frontend domains app.use('*', cors({ origin: ['https://ji-test.ccllc.pro', 'http://localhost:3005', 'http://localhost:5173'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], credentials: true, })); // Serve bundled auth module JavaScript app.get('/auth-module.js', async (c) => { const filePath = path.join(__dirname, '../frontend/dist/auth.js'); if (existsSync(filePath)) { const content = await readFile(filePath); c.header('Content-Type', 'application/javascript'); return c.body(content); } return c.notFound(); }); // Serve frontend static files app.use('/', serveStatic({ root: path.join(__dirname, '../frontend') })); // API: Get frontend config app.get('/api/auth/config', (c) => { try { const config = AuthConfigManager.getFrontendConfig(); return c.json(config); } catch (error) { return c.json( { error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' }, 500 ); } }); // API: Get Graph token status app.get('/api/auth/graph/token', async (c) => { try { const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry(); if (!token) { return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500); } // Decode JWT to show token details (for verification) const parts = token.split('.'); let payload = {}; if (parts.length === 3) { try { const decoded = JSON.parse(Buffer.from(parts[1], 'base64').toString()); payload = { aud: decoded.aud, iss: decoded.iss, scp: decoded.scp, app_displayname: decoded.app_displayname, iat: new Date(decoded.iat * 1000).toISOString(), exp: new Date(decoded.exp * 1000).toISOString(), }; } catch (e) { // Silent fail - just show truncated token } } return c.json({ success: true, token: token.substring(0, 50) + '...', fullToken: token, // Include full token for testing expiresOn: expiresOnISO, isValid: backendAuth.graphTokenManager.isTokenValid(), tokenDetails: Object.keys(payload).length > 0 ? payload : 'Could not decode', }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' }, 500 ); } }); // API: Validate PocketBase token app.post('/api/auth/validate-pb-token', async (c) => { try { const body = await c.req.json(); const token = body.pbToken; if (!token) { return c.json({ valid: false, error: 'No token provided' }, 400); } const isValid = await backendAuth.pbValidator.validateUserToken(token); const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null; return c.json({ valid: isValid, user: user ? { id: user.id, email: user.email, name: user.name, } : null, }); } catch (error) { return c.json( { valid: false, error: error instanceof Error ? error.message : 'Validation failed' }, 500 ); } }); // API: Refresh Graph token (test cache) app.post('/api/auth/refresh-graph', async (c) => { try { backendAuth.graphTokenManager.clearCache(); const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry(); return c.json({ success: true, refreshed: true, expiresOn: expiresOnISO, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Refresh failed' }, 500 ); } }); // API: Compare PB and Graph tokens app.post('/api/auth/compare-tokens', async (c) => { try { const body = await c.req.json(); const pbToken = body.pbToken; // Get Graph token const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry(); if (!graphToken) { return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500); } // Decode both tokens to compare const decodeJWT = (token: string) => { try { const parts = token.split('.'); if (parts.length !== 3) return null; return JSON.parse(Buffer.from(parts[1], 'base64').toString()); } catch { return null; } }; const graphPayload = decodeJWT(graphToken); // Validate PB token to get actual user let authenticatedUser = 'Unknown'; if (pbToken) { try { const isValid = await backendAuth.pbValidator.validateUserToken(pbToken); if (isValid) { const userRecord = await backendAuth.pbValidator.getUserRecord(pbToken); authenticatedUser = userRecord?.email || userRecord?.id || 'Unknown'; } } catch { authenticatedUser = 'Token invalid or expired'; } } return c.json({ success: true, comparison: { graph_token: { issuer: graphPayload?.iss || 'N/A', audience: graphPayload?.aud || 'N/A', app: graphPayload?.app_displayname || 'N/A', scopes: graphPayload?.scp || 'N/A', issued_at: graphPayload?.iat ? new Date(graphPayload.iat * 1000).toISOString() : 'N/A', expires_at: graphPayload?.exp ? new Date(graphPayload.exp * 1000).toISOString() : 'N/A', type: 'Microsoft Graph Token (Backend/App-only)', source: '@azure/msal-node', purpose: 'Backend API calls to Microsoft Graph', }, pocketbase_token: { note: 'User token from PocketBase OAuth2', type: 'PocketBase User Token (Frontend/User)', source: 'PocketBase OAuth2 flow', purpose: 'User authentication and authorization', authenticated_user: authenticatedUser, }, are_different: true, explanation: 'These are completely different tokens from different systems: Graph token is for app-to-app Microsoft API access, PocketBase token is for user authentication in the application.', }, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' }, 500 ); } }); // Start server console.log(`Mode1 Auth Test Server running on port ${PORT}`); export default { port: PORT, fetch: app.fetch, };