import { config } from 'dotenv'; import { Hono } from 'hono'; import { serveStatic } from 'hono/bun'; import { cors } from 'hono/cors'; import PocketBase from 'pocketbase'; import { ConfidentialClientApplication } from '@azure/msal-node'; // Load secrets from absolute path (not in project directory) config({ path: '/home/admin/secrets/.env' }); const app = new Hono(); // Enable CORS app.use('/*', cors({ origin: '*', credentials: true, })); // PocketBase client const pb = new PocketBase(process.env.PB_DB || 'http://127.0.0.1:8090'); // Use user's PocketBase token function setUserPocketBaseAuth(token: string) { pb.authStore.save(token, null); } // MSAL config for Graph token (client credentials) const msalConfig = { auth: { clientId: process.env.CLIENT_ID || '', authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`, clientSecret: process.env.CLIENT_SECRET || '', }, }; const cca = new ConfidentialClientApplication(msalConfig); // Simple in-memory cache for Graph token let graphTokenCache: { token: string; expiresOn: number } | null = null; async function getGraphToken(): Promise<{ token: string; expiresOn: number }> { const now = Date.now(); if (graphTokenCache && graphTokenCache.expiresOn - 60000 > now) { return graphTokenCache; } const result = await cca.acquireTokenByClientCredential({ scopes: ['https://graph.microsoft.com/.default'], }); if (!result || !result.accessToken) { throw new Error('Failed to acquire Graph token'); } const expiresOn = result.expiresOn ? new Date(result.expiresOn).getTime() : now + 55 * 60 * 1000; graphTokenCache = { token: result.accessToken, expiresOn }; return graphTokenCache; } // Health check app.get('/health', (c) => { return c.json({ ok: true, pbDB: process.env.PB_DB }); }); // Graph token status (acquires if missing/expired) app.get('/api/graph/status', async (c) => { try { const tokenInfo = await getGraphToken(); return c.json({ active: true, expiresOnISO: new Date(tokenInfo.expiresOn).toISOString() }); } catch (e) { return c.json({ active: false, error: (e as Error)?.message || 'Failed to acquire token' }, 500); } }); // Submit endpoint (example) app.post('/api/submit', async (c) => { try { const body = await c.req.json(); const pbToken = body.pbToken; if (!pbToken) { return c.json({ success: false, message: 'Missing pbToken' }, 401); } // Validate user token setUserPocketBaseAuth(pbToken); try { await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh(); } catch (e) { console.error('auth_validation_failed', { message: (e as Error)?.message }); return c.json({ success: false, message: 'Invalid token' }, 401); } // Store in PocketBase collection (adjust fields as needed) const record = await pb .collection(process.env.PB_COLLECTION || 'Job_Info_TestEnv') .create({ ...body, submittedBy: pb.authStore.model?.email || 'unknown', submittedAt: new Date().toISOString(), }); console.log('submission_success', { recordId: record.id }); return c.json({ success: true, message: 'Feedback submitted successfully', recordId: record.id }); } catch (err) { console.error('submit_error', { message: (err as Error)?.message }); return c.json({ success: false, message: 'Internal error' }, 500); } }); // Serve static files (images, css, etc) app.use('/images/*', serveStatic({ root: new URL('./', import.meta.url).pathname })); // Serve static index.html app.get('/', async (c) => { const html = await Bun.file(new URL('./index.html', import.meta.url)).text(); return c.html(html); }); const PORT = Number(process.env.PORT || 3030); // Start Bun server manually to avoid Bun's auto startup banner const server = Bun.serve({ port: PORT, fetch: app.fetch, }); // Custom startup notes console.log(`Prism Notes server running at http://localhost:${PORT}`); console.log('Prism Notes API ready (health: /health)');