Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 119 lines 3.1 kB view raw
1import { Hono } from 'hono'; 2import { logger } from 'hono/logger'; 3import { serveStatic } from '@hono/node-server/serve-static'; 4import { readFileSync, existsSync } from 'node:fs'; 5import { join } from 'node:path'; 6import type { InstanceInfo } from './config.js'; 7import { 8 generateSecret, 9 authMiddleware, 10 handleVerify, 11 handleLogout, 12} from './auth.js'; 13 14const APP_NAMES = ['docs', 'sheets', 'forms', 'slides', 'diagrams', 'calendar']; 15 16export interface ServerConfig { 17 instanceInfo: InstanceInfo; 18 distPath: string; 19 cookieSecret?: Buffer; 20 version?: string; 21} 22 23export function createApp(config: ServerConfig): Hono { 24 const { instanceInfo, distPath, version = 'dev' } = config; 25 const secret = config.cookieSecret ?? generateSecret(); 26 const startTime = Date.now(); 27 28 const appHtml: Record<string, string> = {}; 29 for (const name of APP_NAMES) { 30 const htmlPath = join(distPath, name, 'index.html'); 31 if (existsSync(htmlPath)) { 32 appHtml[name] = readFileSync(htmlPath, 'utf-8'); 33 } 34 } 35 36 const landingPath = join(distPath, 'index.html'); 37 const landingHtml = existsSync(landingPath) 38 ? readFileSync(landingPath, 'utf-8') 39 : '<html><body>Atmosphere Office</body></html>'; 40 41 const app = new Hono(); 42 43 app.use('*', logger()); 44 45 app.use('*', async (c, next) => { 46 const start = performance.now(); 47 await next(); 48 const ms = (performance.now() - start).toFixed(1); 49 c.res.headers.set('X-Response-Time', `${ms}ms`); 50 c.res.headers.set('X-Version', version); 51 }); 52 53 app.get('/health', (c) => 54 c.json({ 55 status: 'ok', 56 version, 57 uptime: Math.floor((Date.now() - startTime) / 1000), 58 flavor: instanceInfo.flavor, 59 accessMode: instanceInfo.accessControl?.mode ?? 'open', 60 }), 61 ); 62 63 app.get('/instance-info.json', (c) => { 64 c.header('Cache-Control', 'no-cache'); 65 return c.json(instanceInfo); 66 }); 67 68 app.post('/api/auth/verify', async (c) => { 69 try { 70 const body = await c.req.json(); 71 return handleVerify(c, body.did, secret, instanceInfo); 72 } catch { 73 return c.json({ error: 'Invalid request body' }, 400); 74 } 75 }); 76 77 app.post('/api/auth/logout', (c) => handleLogout(c)); 78 79 const needsAuth = 80 instanceInfo.accessControl?.mode === 'allowlist' && 81 instanceInfo.flavor !== 'self-hosted'; 82 83 for (const name of APP_NAMES) { 84 const html = appHtml[name]; 85 if (!html) continue; 86 87 const serve = (c: import('hono').Context) => c.html(html); 88 89 if (needsAuth) { 90 const mw = authMiddleware(secret, instanceInfo); 91 app.get(`/${name}`, mw, serve); 92 app.get(`/${name}/*`, mw, serve); 93 } else { 94 app.get(`/${name}`, serve); 95 app.get(`/${name}/*`, serve); 96 } 97 } 98 99 app.use( 100 '/assets/*', 101 async (c, next) => { 102 c.header('Cache-Control', 'public, max-age=31536000, immutable'); 103 await next(); 104 }, 105 serveStatic({ root: distPath }), 106 ); 107 108 app.use( 109 '/*', 110 serveStatic({ 111 root: distPath, 112 onNotFound: () => {}, 113 }), 114 ); 115 116 app.get('*', (c) => c.html(landingHtml)); 117 118 return app; 119}