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"
},
"dependencies": {
"@hono/node-server": "^1.14.1",
"@hono/zod-validator": "^0.4.3",
"@paralleldrive/cuid2": "^2.2.2",
"@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'])
.default('development'),
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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -2,13 +2,16 @@ import { createMiddleware } from 'hono/factory'
import type { AuthenticatedEnv } from '@/lib/types'
export const me = createMiddleware<AuthenticatedEnv>(async (ctx, next) => {
const user = ctx.get('user')
const regex = /^\/(me)(\/|$)/
export const me = createMiddleware<AuthenticatedEnv>(
async function meMiddleware(ctx, next) {
const user = ctx.get('user')
const regex = /^\/(me)(\/|$)/
if (user && regex.test(ctx.req.path)) {
ctx.req.path = ctx.req.path.replace(regex, `/users/${user.id}$2`)
if (user && regex.test(ctx.req.path)) {
// TODO: redirect instead?
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,24 +7,26 @@ import { db, schema } from '@/db'
import { aclTeamMember } from '../acl-team-member'
import { assert } from '../utils'
export const team = createMiddleware<AuthenticatedEnv>(async (ctx, next) => {
const teamId = ctx.req.query('teamId')
const user = ctx.get('user')
export const team = createMiddleware<AuthenticatedEnv>(
async function teamMiddleware(ctx, next) {
const teamId = ctx.req.query('teamId')
const user = ctx.get('user')
if (teamId && user) {
const teamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, user.id)
),
with: { team: true }
})
assert(teamMember, 401, 'Unauthorized')
if (teamId && user) {
const teamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, user.id)
),
with: { team: true }
})
assert(teamMember, 401, 'Unauthorized')
await aclTeamMember(ctx, teamMember)
await aclTeamMember(ctx, teamMember)
ctx.set('teamMember', teamMember)
ctx.set('teamMember', teamMember)
}
await next()
}
await next()
})
)

Wyświetl plik

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

Wyświetl plik

@ -116,6 +116,9 @@ importers:
apps/api:
dependencies:
'@hono/node-server':
specifier: ^1.14.1
version: 1.14.1(hono@4.7.7)
'@hono/zod-validator':
specifier: ^0.4.3
version: 0.4.3(hono@4.7.7)(zod@3.24.3)
@ -507,6 +510,12 @@ packages:
prettier: '>= 3'
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':
resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==}
peerDependencies:
@ -3151,6 +3160,10 @@ snapshots:
- supports-color
- 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)':
dependencies:
hono: 4.7.7