pull/715/head
Travis Fischer 2025-05-01 11:23:53 +07:00
rodzic 612519ec68
commit d1dfd32567
7 zmienionych plików z 110 dodań i 22 usunięć

Wyświetl plik

@ -4,6 +4,7 @@ import type { AuthenticatedEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { registerHealthCheck } from './health-check'
import { registerV1ProjectsCreateProject } from './projects/create-project'
import { registerV1ProjectsGetProject } from './projects/get-project'
import { registerV1ProjectsListProjects } from './projects/list-projects'
import { registerV1TeamsCreateTeam } from './teams/create-team'
@ -44,9 +45,22 @@ registerV1TeamsMembersUpdateTeamMember(pri)
registerV1TeamsMembersDeleteTeamMember(pri)
// Projects crud
registerV1ProjectsCreateProject(pri)
registerV1ProjectsGetProject(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
apiV1.route('/', pub)
apiV1.use(middleware.authenticate)

Wyświetl plik

@ -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))
})
}

Wyświetl plik

@ -41,7 +41,10 @@ export function registerV1ProjectsGetProject(
const project = await db.query.projects.findFirst({
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}"`)
await acl(c, project, { label: 'Project' })

Wyświetl plik

@ -10,8 +10,6 @@ import {
} from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import { getProviderToken } from '@/lib/auth/get-provider-token'
import { deployments, deploymentSelectSchema } from './deployment'
import { teams, teamSelectSchema } from './team'
import { type Webhook } from './types'
@ -40,7 +38,7 @@ export const projects = pgTable(
userId: cuid()
.notNull()
.references(() => users.id),
teamId: cuid().notNull(),
teamId: cuid(),
// Most recently published Deployment if one exists
lastPublishedDeploymentId: deploymentId(),
@ -57,7 +55,7 @@ export const projects = pgTable(
_secret: text(),
// Auth token used to access the saasify API on behalf of this project
_providerToken: text().notNull(),
// _providerToken: text().notNull(),
// TODO: Full-text search
_text: text().default('').notNull(),
@ -164,7 +162,7 @@ export const projectSelectSchema = createSelectSchema(projects, {
})
.omit({
_secret: true,
_providerToken: true,
// _providerToken: true,
_text: true,
_webhooks: true,
_stripeCouponIds: true,
@ -206,17 +204,16 @@ export const projectInsertSchema = createInsertSchema(projects, {
})
})
.pick({
id: true,
name: true,
userId: true
teamId: true
})
.strict()
.refine((data) => {
return {
...data,
_providerToken: getProviderToken(data)
}
})
// .refine((data) => {
// return {
// ...data,
// _providerToken: getProviderToken(data)
// }
// })
// TODO: narrow update schema
export const projectUpdateSchema = createUpdateSchema(projects).strict()

Wyświetl plik

@ -7,16 +7,19 @@ export async function aclTeamMember(
ctx: AuthenticatedContext,
{
teamSlug,
teamId,
teamMember,
userId
}: {
teamSlug: string
teamSlug?: string
teamId?: string
teamMember?: TeamMember
userId?: string
}
} & ({ teamSlug: string } | { teamId: string } | { teamMember: TeamMember })
) {
const user = ctx.get('user')
assert(user, 401, 'Authentication required')
assert(teamSlug || teamId, 500, 'Either teamSlug or teamId must be provided')
if (user.role === 'admin') {
// TODO: Allow admins to access all team resources
@ -28,13 +31,18 @@ export async function aclTeamMember(
if (!teamMember) {
teamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamSlug, teamSlug),
teamSlug
? eq(schema.teamMembers.teamSlug, teamSlug)
: eq(schema.teamMembers.teamId, teamId!),
eq(schema.teamMembers.userId, userId)
)
})
}
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
if (!ctx.get('teamMember')) {
ctx.set('teamMember', teamMember)
}
assert(
teamMember.userId === userId,

Wyświetl plik

@ -17,12 +17,11 @@ export const team = createMiddleware<AuthenticatedEnv>(
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)
}

Wyświetl plik

@ -1,10 +1,10 @@
import type { Context } from 'hono'
import type { TeamMemberWithTeam, User } from '@/db'
import type { TeamMember, User } from '@/db'
export type AuthenticatedEnvVariables = {
user: User
teamMember?: TeamMemberWithTeam
teamMember?: TeamMember
jwtPayload:
| {
type: 'user'