kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
f259cee1b9
commit
10ee357667
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import type { Context } from 'hono'
|
||||
|
||||
export async function healthCheck(c: Context) {
|
||||
return c.json({ status: 'ok' })
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
|
@ -1,3 +1,5 @@
|
|||
export * from './authenticate'
|
||||
export * from './error-handler'
|
||||
export * from './me'
|
||||
export * from './response-time'
|
||||
export * from './team'
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue