Files
Prism-Notes/server.ts
T

124 lines
3.8 KiB
TypeScript

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 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)');