pull/715/head
Travis Fischer 2025-04-24 06:23:36 +07:00
rodzic f259cee1b9
commit 10ee357667
13 zmienionych plików z 148 dodań i 45 usunięć

Wyświetl plik

@ -34,6 +34,7 @@
"test:unit": "vitest run" "test:unit": "vitest run"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.14.1",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@workos-inc/node": "^7.47.0", "@workos-inc/node": "^7.47.0",

Wyświetl plik

@ -0,0 +1,5 @@
import type { Context } from 'hono'
export async function healthCheck(c: Context) {
return c.json({ status: 'ok' })
}

Wyświetl plik

@ -0,0 +1,19 @@
import { Hono } from 'hono'
import type { AuthenticatedEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { healthCheck } from './health-check'
export const apiV1 = new Hono()
const pub = new Hono()
const pri = new Hono<AuthenticatedEnv>()
pub.get('/health', healthCheck)
apiV1.route('', pub)
apiV1.use(middleware.authenticate)
apiV1.use(middleware.team)
apiV1.use(middleware.me)
apiV1.route('', pri)

Wyświetl plik

@ -7,7 +7,8 @@ export const envSchema = z.object({
.enum(['development', 'test', 'production']) .enum(['development', 'test', 'production'])
.default('development'), .default('development'),
DATABASE_URL: z.string().url(), DATABASE_URL: z.string().url(),
JWT_SECRET: z.string() JWT_SECRET: z.string(),
PORT: z.number().default(3000)
}) })
// eslint-disable-next-line no-process-env // eslint-disable-next-line no-process-env

Wyświetl plik

@ -1,11 +1,13 @@
import type { ContentfulStatusCode } from 'hono/utils/http-status'
export class HttpError extends Error { export class HttpError extends Error {
readonly statusCode: number readonly statusCode: ContentfulStatusCode
constructor({ constructor({
statusCode = 500, statusCode = 500,
message message
}: { }: {
statusCode?: number statusCode?: ContentfulStatusCode
message: string message: string
}) { }) {
super(message) super(message)

Wyświetl plik

@ -13,18 +13,19 @@ const jwtMiddleware = jwt({
}) })
export const authenticate = createMiddleware<AuthenticatedEnv>( export const authenticate = createMiddleware<AuthenticatedEnv>(
async (ctx, next) => { async function authenticateMiddleware(ctx, next) {
console.log(`[${ctx.req.method}] ${ctx.req.url}`) console.log(`[${ctx.req.method}] ${ctx.req.url}`)
await jwtMiddleware(ctx, async () => { await jwtMiddleware(ctx, async () => {
const payload = ctx.get('jwtPayload') const payload = ctx.get('jwtPayload')
if (payload.type === 'user') { assert(payload, 401, 'Unauthorized')
assert(payload.type === 'user', 401, 'Unauthorized')
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: eq(schema.users.id, payload.userId) where: eq(schema.users.id, payload.userId)
}) })
assert(user, 401, 'Unauthorized') assert(user, 401, 'Unauthorized')
ctx.set('user', user) ctx.set('user', user)
}
await next() await next()
}) })

Wyświetl plik

@ -0,0 +1,36 @@
import type { ContentfulStatusCode } from 'hono/utils/http-status'
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'
import type { AuthenticatedEnv } from '@/lib/types'
import { HttpError } from '../errors'
export const errorHandler = createMiddleware<AuthenticatedEnv>(
async function errorHandlerMiddleware(ctx, next) {
try {
await next()
if (!ctx.res.status) {
throw new HttpError({ statusCode: 404, message: 'Not Found' })
}
} catch (err) {
let message = 'Internal Server Error'
let status: ContentfulStatusCode = 500
if (err instanceof HTTPException) {
message = err.message
status = err.status
} else if (err instanceof HttpError) {
message = err.message
status = err.statusCode
}
if (status >= 500) {
console.error('http error', status, message)
}
ctx.json({ error: message }, status)
}
}
)

Wyświetl plik

@ -1,3 +1,5 @@
export * from './authenticate' export * from './authenticate'
export * from './error-handler'
export * from './me' export * from './me'
export * from './response-time'
export * from './team' export * from './team'

Wyświetl plik

@ -2,13 +2,16 @@ import { createMiddleware } from 'hono/factory'
import type { AuthenticatedEnv } from '@/lib/types' import type { AuthenticatedEnv } from '@/lib/types'
export const me = createMiddleware<AuthenticatedEnv>(async (ctx, next) => { export const me = createMiddleware<AuthenticatedEnv>(
async function meMiddleware(ctx, next) {
const user = ctx.get('user') const user = ctx.get('user')
const regex = /^\/(me)(\/|$)/ const regex = /^\/(me)(\/|$)/
if (user && regex.test(ctx.req.path)) { if (user && regex.test(ctx.req.path)) {
// TODO: redirect instead?
ctx.req.path = ctx.req.path.replace(regex, `/users/${user.id}$2`) ctx.req.path = ctx.req.path.replace(regex, `/users/${user.id}$2`)
} }
await next() await next()
}) }
)

Wyświetl plik

@ -0,0 +1,12 @@
import { createMiddleware } from 'hono/factory'
import type { AuthenticatedEnv } from '@/lib/types'
export const responseTime = createMiddleware<AuthenticatedEnv>(
async function responseTimeMiddleware(ctx, next) {
const start = Date.now()
await next()
const duration = Date.now() - start
ctx.res.headers.set('X-Response-Time', `${duration}ms`)
}
)

Wyświetl plik

@ -7,7 +7,8 @@ import { db, schema } from '@/db'
import { aclTeamMember } from '../acl-team-member' import { aclTeamMember } from '../acl-team-member'
import { assert } from '../utils' import { assert } from '../utils'
export const team = createMiddleware<AuthenticatedEnv>(async (ctx, next) => { export const team = createMiddleware<AuthenticatedEnv>(
async function teamMiddleware(ctx, next) {
const teamId = ctx.req.query('teamId') const teamId = ctx.req.query('teamId')
const user = ctx.get('user') const user = ctx.get('user')
@ -27,4 +28,5 @@ export const team = createMiddleware<AuthenticatedEnv>(async (ctx, next) => {
} }
await next() await next()
}) }
)

Wyświetl plik

@ -1,16 +1,22 @@
import { serve } from '@hono/node-server'
import { Hono } from 'hono' import { Hono } from 'hono'
import { compress } from 'hono/compress'
import { cors } from 'hono/cors'
import { apiV1 } from '@/api-v1'
import { env } from '@/lib/env'
import * as middleware from '@/lib/middleware' import * as middleware from '@/lib/middleware'
import type { AuthenticatedEnv } from './lib/types'
export const app = new Hono() export const app = new Hono()
const pub = new Hono() app.use(compress())
const pri = new Hono<AuthenticatedEnv>() app.use(middleware.responseTime)
app.use(middleware.errorHandler)
app.use(cors())
app.route('/', pub) app.route('/v1', apiV1)
app.use('*', middleware.authenticate)
app.use('*', middleware.team) serve({
app.use('*', middleware.me) fetch: app.fetch,
app.route('/', pri) port: env.PORT
})

Wyświetl plik

@ -116,6 +116,9 @@ importers:
apps/api: apps/api:
dependencies: dependencies:
'@hono/node-server':
specifier: ^1.14.1
version: 1.14.1(hono@4.7.7)
'@hono/zod-validator': '@hono/zod-validator':
specifier: ^0.4.3 specifier: ^0.4.3
version: 0.4.3(hono@4.7.7)(zod@3.24.3) version: 0.4.3(hono@4.7.7)(zod@3.24.3)
@ -507,6 +510,12 @@ packages:
prettier: '>= 3' prettier: '>= 3'
typescript: '>= 5' typescript: '>= 5'
'@hono/node-server@1.14.1':
resolution: {integrity: sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@hono/zod-validator@0.4.3': '@hono/zod-validator@0.4.3':
resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==} resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==}
peerDependencies: peerDependencies:
@ -3151,6 +3160,10 @@ snapshots:
- supports-color - supports-color
- vitest - vitest
'@hono/node-server@1.14.1(hono@4.7.7)':
dependencies:
hono: 4.7.7
'@hono/zod-validator@0.4.3(hono@4.7.7)(zod@3.24.3)': '@hono/zod-validator@0.4.3(hono@4.7.7)(zod@3.24.3)':
dependencies: dependencies:
hono: 4.7.7 hono: 4.7.7