import 'dotenv/config' import { serve } from '@hono/node-server' import { Hono } from 'hono' import { createNodeWebSocket } from '@hono/node-ws' import { getConnInfo } from '@hono/node-server/conninfo' import { cors } from 'hono/cors' import { logger } from 'hono/logger' import { showRoutes } from 'hono/dev' import { auth } from './lib/auth.ts' import { createOcppHandler } from './ocpp/handler.ts' import { verifyOcppPassword } from './lib/ocpp-auth.ts' import { useDrizzle } from './lib/db.ts' import { chargePoint } from './db/schema.ts' import { eq } from 'drizzle-orm' import statsRoutes from './routes/stats.ts' import statsChartRoutes from './routes/stats-chart.ts' import chargePointRoutes from './routes/charge-points.ts' import transactionRoutes from './routes/transactions.ts' import idTagRoutes from './routes/id-tags.ts' import userRoutes from './routes/users.ts' import setupRoutes from './routes/setup.ts' import tariffRoutes from './routes/tariff.ts' import settingsRoutes from './routes/settings.ts' import type { HonoEnv } from './types/hono.ts' const app = new Hono() const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) app.use(logger()) app.use('*', async (c, next) => { const session = await auth.api.getSession({ headers: c.req.raw.headers }) if (!session) { c.set('user', null) c.set('session', null) await next() return } c.set('user', session.user) c.set('session', session.session) await next() }) app.use( '/api/*', cors({ origin: process.env.WEB_ORIGIN ?? '*', allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], exposeHeaders: ['Content-Length'], credentials: true, }), ) app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw)) // REST API routes app.route('/api/stats', statsRoutes) app.route('/api/stats/chart', statsChartRoutes) app.route('/api/charge-points', chargePointRoutes) app.route('/api/transactions', transactionRoutes) app.route('/api/id-tags', idTagRoutes) app.route('/api/users', userRoutes) app.route('/api/setup', setupRoutes) app.route('/api/tariff', tariffRoutes) app.route('/api/settings', settingsRoutes) app.get('/api', (c) => { const user = c.get('user') const session = c.get('session') const payload = { platform: 'Helios CSMS', message: 'ok', } if (user) { Object.assign(payload, { user }) } if (session) { Object.assign(payload, { session }) } return c.json(payload) }) app.get( '/ocpp/:chargePointId', async (c, next) => { const chargePointId = c.req.param('chargePointId') const authHeader = c.req.header('Authorization') if (!authHeader?.startsWith('Basic ')) { c.header('WWW-Authenticate', 'Basic realm="OCPP"') return c.json({ error: 'Unauthorized' }, 401) } let id: string, password: string try { const decoded = atob(authHeader.slice(6)) const colonIdx = decoded.indexOf(':') if (colonIdx === -1) throw new Error('Invalid format') id = decoded.slice(0, colonIdx) password = decoded.slice(colonIdx + 1) } catch { return c.json({ error: 'Invalid Authorization header' }, 400) } if (id !== chargePointId) { return c.json({ error: 'Unauthorized' }, 401) } const db = useDrizzle() const [cp] = await db .select({ passwordHash: chargePoint.passwordHash }) .from(chargePoint) .where(eq(chargePoint.chargePointIdentifier, chargePointId)) .limit(1) if (!cp?.passwordHash || !(await verifyOcppPassword(password, cp.passwordHash))) { return c.json({ error: 'Unauthorized' }, 401) } await next() }, upgradeWebSocket((c) => { const chargePointId = c.req.param('chargePointId') const connInfo = getConnInfo(c) return createOcppHandler(chargePointId, connInfo.remote.address) }), ) showRoutes(app, { verbose: true, }) const server = serve( { fetch: app.fetch, port: 3001, }, (info) => { console.log(`Server is running on http://localhost:${info.port}`) }, ) injectWebSocket(server)