149 lines
4.0 KiB
TypeScript
149 lines
4.0 KiB
TypeScript
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 type { HonoEnv } from './types/hono.ts'
|
|
|
|
const app = new Hono<HonoEnv>()
|
|
|
|
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.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)
|