File size: 3,628 Bytes
e3b7b69
 
489d61b
5e0dec1
0dbdd4f
489d61b
e481638
 
 
0d611fc
e481638
 
e3b7b69
e481638
 
e3b7b69
 
 
 
 
 
 
 
 
d917c4e
e3b7b69
 
 
 
 
d917c4e
5e0dec1
 
e481638
5e0dec1
 
 
 
 
b419e9c
 
0dbdd4f
e3b7b69
 
 
 
0d611fc
07c37a1
e3b7b69
489d61b
 
 
 
 
666d9c1
489d61b
 
 
 
 
 
e3b7b69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import 'dotenv/config'
import express from 'express'
import type { Request, Response, NextFunction } from 'express'
import path from 'path'
import { auth } from './middleware/auth.js'
import { logger } from './logger.js'
import { authRouter } from './services/auth/auth.routes'
import { chatRouter } from './services/chat/chat.routes'
import { expenseRouter } from './services/expense/router'
import { workspaceMembersRouter } from './services/expense/workspace-members/workspace-members.routes'
import { slipQueueRouter } from './services/expense/slip-queue/slip-queue.routes'
import { publicRouter } from './services/expense/public/public.routes'
import { healthRouter } from './health/health.routes'
import { runMigrations } from './services/expense/db/migrate.js'
import { seedDefaults } from './services/expense/db/seed.js'

const app = express()

app.use(express.json({ limit: '50mb' }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.raw({ limit: '50mb', type: ['image/*', 'application/octet-stream'] }))

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-internal-key')
  if (req.method === 'OPTIONS') { res.sendStatus(204); return }
  next()
})

app.use(authRouter)        // /auth/login is public

// Serve built dashboard — no auth needed for static assets or SPA routes
const dashboardDist = path.join(__dirname, '../../web/dist')
app.use(express.static(dashboardDist))
app.get(/^\/(?!v1\/|auth\/|health)/, (_req, res) => {
  res.sendFile(path.join(dashboardDist, 'index.html'))
})

app.use('/v1/public', publicRouter)   // token-scoped, no auth — the token itself is the access control

app.use(auth)              // all API routes require auth (internal-secret bypass, else JWT via auth-client)

app.use(healthRouter)
app.use('/v1', chatRouter)
app.use('/v1/expense', expenseRouter)
app.use('/v1/workspaces', workspaceMembersRouter)
app.use('/v1/slip-queue', slipQueueRouter)

app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
  logger.error('unhandled_route_error', {
    method: req.method,
    path: req.path,
    error: err instanceof Error ? err.message : String(err),
    cause: err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined,
    stack: err instanceof Error ? err.stack : undefined,
  })
  if (res.headersSent) return
  res.status(500).json({ error: 'Internal server error' })
})

const PORT = parseInt(process.env['PORT'] ?? '7860', 10)
const FOUR_MINUTES = 4 * 60 * 1000

async function keepAlive(): Promise<void> {
  const services = [
    `http://localhost:${PORT}/health`,
    `${process.env['OLLAMA_HOST'] ?? ''}/api/tags`,
  ]
  for (const url of services) {
    try {
      const pingRes = await fetch(url, {
        headers: { 'x-internal-key': process.env['INTERNAL_SECRET'] ?? '' },
        signal: AbortSignal.timeout(5000),
      })
      console.log('keepalive_ping', { url, status: pingRes.status })
    } catch (err: unknown) {
      console.warn('keepalive_failed', { url, error: err instanceof Error ? err.message : String(err) })
    }
  }
}

setInterval(() => void keepAlive(), FOUR_MINUTES)
void keepAlive()

async function start(): Promise<void> {
  await runMigrations()
  await seedDefaults()
  app.listen(PORT, '0.0.0.0', () => console.log(`Proxy running on port ${PORT}`))
}

start().catch((err: unknown) => {
  console.error('Failed to start server:', err)
  process.exit(1)
})