kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
612519ec68
commit
d1dfd32567
|
@ -4,6 +4,7 @@ import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
import * as middleware from '@/lib/middleware'
|
import * as middleware from '@/lib/middleware'
|
||||||
|
|
||||||
import { registerHealthCheck } from './health-check'
|
import { registerHealthCheck } from './health-check'
|
||||||
|
import { registerV1ProjectsCreateProject } from './projects/create-project'
|
||||||
import { registerV1ProjectsGetProject } from './projects/get-project'
|
import { registerV1ProjectsGetProject } from './projects/get-project'
|
||||||
import { registerV1ProjectsListProjects } from './projects/list-projects'
|
import { registerV1ProjectsListProjects } from './projects/list-projects'
|
||||||
import { registerV1TeamsCreateTeam } from './teams/create-team'
|
import { registerV1TeamsCreateTeam } from './teams/create-team'
|
||||||
|
@ -44,9 +45,22 @@ registerV1TeamsMembersUpdateTeamMember(pri)
|
||||||
registerV1TeamsMembersDeleteTeamMember(pri)
|
registerV1TeamsMembersDeleteTeamMember(pri)
|
||||||
|
|
||||||
// Projects crud
|
// Projects crud
|
||||||
|
registerV1ProjectsCreateProject(pri)
|
||||||
registerV1ProjectsGetProject(pri)
|
registerV1ProjectsGetProject(pri)
|
||||||
registerV1ProjectsListProjects(pri)
|
registerV1ProjectsListProjects(pri)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// pub.get('/projects/alias/:alias(.+)', require('./projects').readByAlias)
|
||||||
|
// pri.get('/projects/provider/:project(.+)', require('./provider').read)
|
||||||
|
// pri.put('/projects/provider/:project(.+)', require('./provider').update)
|
||||||
|
// pri.put('/projects/connect/:project(.+)', require('./projects').connect)
|
||||||
|
// pub.get(
|
||||||
|
// '/projects/:project(.+)',
|
||||||
|
// middleware.authenticate({ passthrough: true }),
|
||||||
|
// require('./projects').read
|
||||||
|
// )
|
||||||
|
// pri.put('/projects/:project(.+)', require('./projects').update)
|
||||||
|
|
||||||
// Setup routes and middleware
|
// Setup routes and middleware
|
||||||
apiV1.route('/', pub)
|
apiV1.route('/', pub)
|
||||||
apiV1.use(middleware.authenticate)
|
apiV1.use(middleware.authenticate)
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, schema } from '@/db'
|
||||||
|
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Creates a new project.',
|
||||||
|
tags: ['projects'],
|
||||||
|
operationId: 'createProject',
|
||||||
|
method: 'post',
|
||||||
|
path: 'projects',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.projectInsertSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The created project',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.projectSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1ProjectsCreateProject(
|
||||||
|
app: OpenAPIHono<AuthenticatedEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const body = c.req.valid('json')
|
||||||
|
const user = c.get('user')
|
||||||
|
|
||||||
|
if (body.teamId) {
|
||||||
|
await aclTeamMember(c, { teamId: body.teamId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamMember = c.get('teamMember')
|
||||||
|
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||||
|
const id = `${namespace}/${body.name}`
|
||||||
|
|
||||||
|
const [project] = await db
|
||||||
|
.insert(schema.projects)
|
||||||
|
.values({
|
||||||
|
...body,
|
||||||
|
teamId: teamMember?.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
id
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
assert(project, 404, `Failed to create project "${body.name}"`)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.projectSelectSchema, project))
|
||||||
|
})
|
||||||
|
}
|
|
@ -41,7 +41,10 @@ export function registerV1ProjectsGetProject(
|
||||||
|
|
||||||
const project = await db.query.projects.findFirst({
|
const project = await db.query.projects.findFirst({
|
||||||
where: eq(schema.projects.id, projectId),
|
where: eq(schema.projects.id, projectId),
|
||||||
with: Object.fromEntries(populate.map((field) => [field, true]))
|
with: {
|
||||||
|
lastPublishedDeployment: true,
|
||||||
|
...Object.fromEntries(populate.map((field) => [field, true]))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
assert(project, 404, `Project not found "${projectId}"`)
|
assert(project, 404, `Project not found "${projectId}"`)
|
||||||
await acl(c, project, { label: 'Project' })
|
await acl(c, project, { label: 'Project' })
|
||||||
|
|
|
@ -10,8 +10,6 @@ import {
|
||||||
} from '@fisch0920/drizzle-orm/pg-core'
|
} from '@fisch0920/drizzle-orm/pg-core'
|
||||||
import { z } from '@hono/zod-openapi'
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import { getProviderToken } from '@/lib/auth/get-provider-token'
|
|
||||||
|
|
||||||
import { deployments, deploymentSelectSchema } from './deployment'
|
import { deployments, deploymentSelectSchema } from './deployment'
|
||||||
import { teams, teamSelectSchema } from './team'
|
import { teams, teamSelectSchema } from './team'
|
||||||
import { type Webhook } from './types'
|
import { type Webhook } from './types'
|
||||||
|
@ -40,7 +38,7 @@ export const projects = pgTable(
|
||||||
userId: cuid()
|
userId: cuid()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
teamId: cuid().notNull(),
|
teamId: cuid(),
|
||||||
|
|
||||||
// Most recently published Deployment if one exists
|
// Most recently published Deployment if one exists
|
||||||
lastPublishedDeploymentId: deploymentId(),
|
lastPublishedDeploymentId: deploymentId(),
|
||||||
|
@ -57,7 +55,7 @@ export const projects = pgTable(
|
||||||
_secret: text(),
|
_secret: text(),
|
||||||
|
|
||||||
// Auth token used to access the saasify API on behalf of this project
|
// Auth token used to access the saasify API on behalf of this project
|
||||||
_providerToken: text().notNull(),
|
// _providerToken: text().notNull(),
|
||||||
|
|
||||||
// TODO: Full-text search
|
// TODO: Full-text search
|
||||||
_text: text().default('').notNull(),
|
_text: text().default('').notNull(),
|
||||||
|
@ -164,7 +162,7 @@ export const projectSelectSchema = createSelectSchema(projects, {
|
||||||
})
|
})
|
||||||
.omit({
|
.omit({
|
||||||
_secret: true,
|
_secret: true,
|
||||||
_providerToken: true,
|
// _providerToken: true,
|
||||||
_text: true,
|
_text: true,
|
||||||
_webhooks: true,
|
_webhooks: true,
|
||||||
_stripeCouponIds: true,
|
_stripeCouponIds: true,
|
||||||
|
@ -206,17 +204,16 @@ export const projectInsertSchema = createInsertSchema(projects, {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.pick({
|
.pick({
|
||||||
id: true,
|
|
||||||
name: true,
|
name: true,
|
||||||
userId: true
|
teamId: true
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => {
|
// .refine((data) => {
|
||||||
return {
|
// return {
|
||||||
...data,
|
// ...data,
|
||||||
_providerToken: getProviderToken(data)
|
// _providerToken: getProviderToken(data)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
// TODO: narrow update schema
|
// TODO: narrow update schema
|
||||||
export const projectUpdateSchema = createUpdateSchema(projects).strict()
|
export const projectUpdateSchema = createUpdateSchema(projects).strict()
|
||||||
|
|
|
@ -7,16 +7,19 @@ export async function aclTeamMember(
|
||||||
ctx: AuthenticatedContext,
|
ctx: AuthenticatedContext,
|
||||||
{
|
{
|
||||||
teamSlug,
|
teamSlug,
|
||||||
|
teamId,
|
||||||
teamMember,
|
teamMember,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
teamSlug: string
|
teamSlug?: string
|
||||||
|
teamId?: string
|
||||||
teamMember?: TeamMember
|
teamMember?: TeamMember
|
||||||
userId?: string
|
userId?: string
|
||||||
}
|
} & ({ teamSlug: string } | { teamId: string } | { teamMember: TeamMember })
|
||||||
) {
|
) {
|
||||||
const user = ctx.get('user')
|
const user = ctx.get('user')
|
||||||
assert(user, 401, 'Authentication required')
|
assert(user, 401, 'Authentication required')
|
||||||
|
assert(teamSlug || teamId, 500, 'Either teamSlug or teamId must be provided')
|
||||||
|
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
// TODO: Allow admins to access all team resources
|
// TODO: Allow admins to access all team resources
|
||||||
|
@ -28,13 +31,18 @@ export async function aclTeamMember(
|
||||||
if (!teamMember) {
|
if (!teamMember) {
|
||||||
teamMember = await db.query.teamMembers.findFirst({
|
teamMember = await db.query.teamMembers.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(schema.teamMembers.teamSlug, teamSlug),
|
teamSlug
|
||||||
|
? eq(schema.teamMembers.teamSlug, teamSlug)
|
||||||
|
: eq(schema.teamMembers.teamId, teamId!),
|
||||||
eq(schema.teamMembers.userId, userId)
|
eq(schema.teamMembers.userId, userId)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
|
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
|
||||||
|
if (!ctx.get('teamMember')) {
|
||||||
|
ctx.set('teamMember', teamMember)
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
teamMember.userId === userId,
|
teamMember.userId === userId,
|
||||||
|
|
|
@ -17,12 +17,11 @@ export const team = createMiddleware<AuthenticatedEnv>(
|
||||||
where: and(
|
where: and(
|
||||||
eq(schema.teamMembers.teamId, teamId),
|
eq(schema.teamMembers.teamId, teamId),
|
||||||
eq(schema.teamMembers.userId, user.id)
|
eq(schema.teamMembers.userId, user.id)
|
||||||
),
|
)
|
||||||
with: { team: true }
|
|
||||||
})
|
})
|
||||||
assert(teamMember, 401, 'Unauthorized')
|
assert(teamMember, 401, 'Unauthorized')
|
||||||
|
|
||||||
await aclTeamMember(ctx, teamMember)
|
await aclTeamMember(ctx, { teamMember })
|
||||||
|
|
||||||
ctx.set('teamMember', teamMember)
|
ctx.set('teamMember', teamMember)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
|
|
||||||
import type { TeamMemberWithTeam, User } from '@/db'
|
import type { TeamMember, User } from '@/db'
|
||||||
|
|
||||||
export type AuthenticatedEnvVariables = {
|
export type AuthenticatedEnvVariables = {
|
||||||
user: User
|
user: User
|
||||||
teamMember?: TeamMemberWithTeam
|
teamMember?: TeamMember
|
||||||
jwtPayload:
|
jwtPayload:
|
||||||
| {
|
| {
|
||||||
type: 'user'
|
type: 'user'
|
||||||
|
|
Ładowanie…
Reference in New Issue