140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { serveStatic } from 'hono/bun';
|
|
import { cors } from 'hono/cors';
|
|
import PocketBase from 'pocketbase';
|
|
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
// Load env from local .env
|
|
const envPath = join(import.meta.dir, '.env');
|
|
try {
|
|
const envContent = readFileSync(envPath, 'utf-8');
|
|
envContent.split('\n').forEach(line => {
|
|
const trimmed = line.trim();
|
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
const [key, ...valueParts] = trimmed.split('=');
|
|
if (key && valueParts.length > 0) {
|
|
process.env[key.trim()] = valueParts.join('=').trim();
|
|
}
|
|
}
|
|
});
|
|
console.log('✓ Loaded env from .env');
|
|
} catch (e) {
|
|
console.warn('⚠ Could not load .env:', (e as Error).message);
|
|
}
|
|
|
|
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 || 5500);
|
|
|
|
// 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)');
|