128 lines
4.0 KiB
TypeScript
128 lines
4.0 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 files (images, css, tools, etc)
|
|
app.use('/images/*', serveStatic({ root: new URL('./', import.meta.url).pathname }));
|
|
app.use('/tools/*', 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)');
|