kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: team crud
rodzic
d02e70ea74
commit
83b8d9e31c
|
@ -4,6 +4,14 @@ 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 { registerV1TeamsCreateTeam } from './teams/create-team'
|
||||||
|
import { registerV1TeamsDeleteTeam } from './teams/delete-team'
|
||||||
|
import { registerV1TeamsGetTeam } from './teams/get-team'
|
||||||
|
import { registerV1TeamsListTeams } from './teams/list-teams'
|
||||||
|
import { registerV1TeamsMembersCreateTeamMember } from './teams/members/create-team-member'
|
||||||
|
import { registerV1TeamsMembersDeleteTeamMember } from './teams/members/delete-team-member'
|
||||||
|
import { registerV1TeamsMembersUpdateTeamMember } from './teams/members/update-team-member'
|
||||||
|
import { registerV1TeamsUpdateTeam } from './teams/update-team'
|
||||||
import { registerV1UsersGetUser } from './users/get-user'
|
import { registerV1UsersGetUser } from './users/get-user'
|
||||||
import { registerV1UsersUpdateUser } from './users/update-user'
|
import { registerV1UsersUpdateUser } from './users/update-user'
|
||||||
|
|
||||||
|
@ -14,10 +22,23 @@ const pri = new OpenAPIHono<AuthenticatedEnv>()
|
||||||
|
|
||||||
registerHealthCheck(pub)
|
registerHealthCheck(pub)
|
||||||
|
|
||||||
// users crud
|
// Users crud
|
||||||
registerV1UsersGetUser(pri)
|
registerV1UsersGetUser(pri)
|
||||||
registerV1UsersUpdateUser(pri)
|
registerV1UsersUpdateUser(pri)
|
||||||
|
|
||||||
|
// Teams crud
|
||||||
|
registerV1TeamsCreateTeam(pri)
|
||||||
|
registerV1TeamsListTeams(pri)
|
||||||
|
registerV1TeamsGetTeam(pri)
|
||||||
|
registerV1TeamsDeleteTeam(pri)
|
||||||
|
registerV1TeamsUpdateTeam(pri)
|
||||||
|
|
||||||
|
// Team members crud
|
||||||
|
registerV1TeamsMembersCreateTeamMember(pri)
|
||||||
|
registerV1TeamsMembersUpdateTeamMember(pri)
|
||||||
|
registerV1TeamsMembersDeleteTeamMember(pri)
|
||||||
|
|
||||||
|
// Setup routes and middleware
|
||||||
apiV1.route('/', pub)
|
apiV1.route('/', pub)
|
||||||
apiV1.use(middleware.authenticate)
|
apiV1.use(middleware.authenticate)
|
||||||
apiV1.use(middleware.team)
|
apiV1.use(middleware.team)
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, schema } from '@/db'
|
||||||
|
import { ensureUniqueTeamSlug } from '@/lib/ensure-unique-team-slug'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Creates a team.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'createTeam',
|
||||||
|
method: 'post',
|
||||||
|
path: 'teams',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamInsertSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The created team',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsCreateTeam(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const user = c.get('user')
|
||||||
|
const body = c.req.valid('json')
|
||||||
|
|
||||||
|
await ensureUniqueTeamSlug(body.slug)
|
||||||
|
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
const [team] = await tx
|
||||||
|
.insert(schema.teams)
|
||||||
|
.values({
|
||||||
|
...body,
|
||||||
|
ownerId: user.id
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
assert(team, 404, `Failed to create team "${body.slug}"`)
|
||||||
|
|
||||||
|
const [teamMember] = await tx.insert(schema.teamMembers).values({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
teamSlug: team.slug,
|
||||||
|
role: 'admin',
|
||||||
|
confirmed: true
|
||||||
|
})
|
||||||
|
assert(
|
||||||
|
teamMember,
|
||||||
|
404,
|
||||||
|
`Failed to create team member owner for team "${body.slug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamSelectSchema, team))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamAdmin } from '@/lib/acl-team-admin'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Deletes a team by slug.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'deleteTeam',
|
||||||
|
method: 'delete',
|
||||||
|
path: 'teams/{team}',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The team that was deleted',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsDeleteTeam(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug } = c.req.valid('param')
|
||||||
|
await aclTeamAdmin(c, { teamSlug })
|
||||||
|
|
||||||
|
const [team] = await db
|
||||||
|
.delete(schema.teams)
|
||||||
|
.where(eq(schema.teams.slug, teamSlug))
|
||||||
|
.returning()
|
||||||
|
assert(team, 404, `Team not found "${teamSlug}"`)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamSelectSchema, team))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Gets a team by slug.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'getTeam',
|
||||||
|
method: 'get',
|
||||||
|
path: 'teams/{team}',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'A team object',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsGetTeam(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug } = c.req.valid('param')
|
||||||
|
await aclTeamMember(c, { teamSlug })
|
||||||
|
|
||||||
|
const team = await db.query.teams.findFirst({
|
||||||
|
where: eq(schema.teams.slug, teamSlug)
|
||||||
|
})
|
||||||
|
assert(team, 404, `Team not found "${teamSlug}"`)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamSelectSchema, team))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, eq, paginationSchema, schema } from '@/db'
|
||||||
|
import { parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Lists all teams the authenticated user belongs to.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'listTeams',
|
||||||
|
method: 'get',
|
||||||
|
path: 'teams',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
query: paginationSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'A list of teams',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.array(schema.teamSelectSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsListTeams(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const {
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
sort = 'desc',
|
||||||
|
sortBy = 'createdAt'
|
||||||
|
} = c.req.valid('query')
|
||||||
|
|
||||||
|
// schema.teamMembers._.columns
|
||||||
|
|
||||||
|
const teamMembers = await db.query.teamMembers.findMany({
|
||||||
|
where: eq(schema.teamMembers.userId, c.get('user').id),
|
||||||
|
with: {
|
||||||
|
team: true
|
||||||
|
},
|
||||||
|
orderBy: (teamMembers, { asc, desc }) => [
|
||||||
|
sort === 'desc' ? desc(teamMembers[sortBy]) : asc(teamMembers[sortBy])
|
||||||
|
],
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(z.array(schema.teamSelectSchema), teamMembers))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { and, db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamAdmin } from '@/lib/acl-team-admin'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from '../schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Creates a team member.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'createTeamMember',
|
||||||
|
method: 'post',
|
||||||
|
path: 'teams/{team}/members',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema,
|
||||||
|
body: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamMemberInsertSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The created team member',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamMemberSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsMembersCreateTeamMember(
|
||||||
|
app: OpenAPIHono<AuthenticatedEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug } = c.req.valid('param')
|
||||||
|
const body = c.req.valid('json')
|
||||||
|
await aclTeamAdmin(c, { teamSlug })
|
||||||
|
|
||||||
|
const team = await db.query.teams.findFirst({
|
||||||
|
where: eq(schema.teams.slug, teamSlug)
|
||||||
|
})
|
||||||
|
assert(team, 404, `Team not found "${teamSlug}"`)
|
||||||
|
|
||||||
|
const existingTeamMember = await db.query.teamMembers.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.teamMembers.teamSlug, teamSlug),
|
||||||
|
eq(schema.teamMembers.userId, body.userId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
assert(
|
||||||
|
existingTeamMember,
|
||||||
|
409,
|
||||||
|
`User "${body.userId}" is already a member of team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const [teamMember] = await db.insert(schema.teamMembers).values({
|
||||||
|
...body,
|
||||||
|
teamSlug,
|
||||||
|
teamId: team.id
|
||||||
|
})
|
||||||
|
assert(
|
||||||
|
teamMember,
|
||||||
|
400,
|
||||||
|
`Failed to create team member "${body.userId}"for team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: send team invite email
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamMemberSelectSchema, teamMember))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { and, db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamAdmin } from '@/lib/acl-team-admin'
|
||||||
|
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from '../schemas'
|
||||||
|
import { TeamMemberUserIdParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Deletes a team member.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'deleteTeamMember',
|
||||||
|
method: 'delete',
|
||||||
|
path: 'teams/{team}/members/{userId}',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema.merge(TeamMemberUserIdParamsSchema)
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The deleted team member',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamMemberSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsMembersDeleteTeamMember(
|
||||||
|
app: OpenAPIHono<AuthenticatedEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug, userId } = c.req.valid('param')
|
||||||
|
|
||||||
|
await aclTeamAdmin(c, { teamSlug })
|
||||||
|
await aclTeamMember(c, { teamSlug, userId })
|
||||||
|
|
||||||
|
const [teamMember] = await db
|
||||||
|
.delete(schema.teamMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.teamMembers.teamSlug, teamSlug),
|
||||||
|
eq(schema.teamMembers.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning()
|
||||||
|
assert(
|
||||||
|
teamMember,
|
||||||
|
404,
|
||||||
|
`Team member "${userId}" for team "${teamSlug}" not found`
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamMemberSelectSchema, teamMember))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import { teamSlugSchema } from '@/db'
|
||||||
|
|
||||||
|
export const TeamSlugParamsSchema = z.object({
|
||||||
|
team: teamSlugSchema.openapi({
|
||||||
|
param: {
|
||||||
|
description: 'Team slug',
|
||||||
|
name: 'team',
|
||||||
|
in: 'path'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const TeamMemberUserIdParamsSchema = z.object({
|
||||||
|
userId: z.string().openapi({
|
||||||
|
param: {
|
||||||
|
description: 'Team member user id',
|
||||||
|
name: 'userId',
|
||||||
|
in: 'path'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { and, db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamAdmin } from '@/lib/acl-team-admin'
|
||||||
|
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from '../schemas'
|
||||||
|
import { TeamMemberUserIdParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Updates a team member.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'updateTeamMember',
|
||||||
|
method: 'put',
|
||||||
|
path: 'teams/{team}/members/{userId}',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema.merge(TeamMemberUserIdParamsSchema),
|
||||||
|
body: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamMemberUpdateSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The updated team member',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamMemberSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsMembersUpdateTeamMember(
|
||||||
|
app: OpenAPIHono<AuthenticatedEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug, userId } = c.req.valid('param')
|
||||||
|
const body = c.req.valid('json')
|
||||||
|
|
||||||
|
await aclTeamAdmin(c, { teamSlug })
|
||||||
|
await aclTeamMember(c, { teamSlug, userId })
|
||||||
|
|
||||||
|
const [teamMember] = await db
|
||||||
|
.update(schema.teamMembers)
|
||||||
|
.set(body)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.teamMembers.teamSlug, teamSlug),
|
||||||
|
eq(schema.teamMembers.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning()
|
||||||
|
assert(
|
||||||
|
teamMember,
|
||||||
|
400,
|
||||||
|
`Failed to update team member "${userId}" for team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamMemberSelectSchema, teamMember))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import { teamSlugSchema } from '@/db'
|
||||||
|
|
||||||
|
export const TeamSlugParamsSchema = z.object({
|
||||||
|
team: teamSlugSchema.openapi({
|
||||||
|
param: {
|
||||||
|
description: 'Team slug',
|
||||||
|
name: 'team',
|
||||||
|
in: 'path'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
|
import { db, eq, schema } from '@/db'
|
||||||
|
import { aclTeamAdmin } from '@/lib/acl-team-admin'
|
||||||
|
import { assert, parseZodSchema } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { TeamSlugParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Updates a team.',
|
||||||
|
tags: ['teams'],
|
||||||
|
operationId: 'updateTeam',
|
||||||
|
method: 'put',
|
||||||
|
path: 'teams/{team}',
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
request: {
|
||||||
|
params: TeamSlugParamsSchema,
|
||||||
|
body: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamUpdateSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'The updated team',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: schema.teamSelectSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1TeamsUpdateTeam(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { team: teamSlug } = c.req.valid('param')
|
||||||
|
const body = c.req.valid('json')
|
||||||
|
await aclTeamAdmin(c, { teamSlug })
|
||||||
|
|
||||||
|
const [team] = await db
|
||||||
|
.update(schema.teams)
|
||||||
|
.set(body)
|
||||||
|
.where(eq(schema.teams.slug, teamSlug))
|
||||||
|
.returning()
|
||||||
|
assert(team, 404, `Failed to update team "${teamSlug}"`)
|
||||||
|
|
||||||
|
return c.json(parseZodSchema(schema.teamSelectSchema, team))
|
||||||
|
})
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ const ParamsSchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
|
description: 'Gets a user by ID.',
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
operationId: 'getUser',
|
operationId: 'getUser',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
@ -40,12 +41,12 @@ const route = createRoute({
|
||||||
export function registerV1UsersGetUser(app: OpenAPIHono<AuthenticatedEnv>) {
|
export function registerV1UsersGetUser(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
||||||
const { userId } = c.req.valid('param')
|
const { userId } = c.req.valid('param')
|
||||||
|
await acl(c, { userId }, { label: 'User' })
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(schema.users.id, userId)
|
where: eq(schema.users.id, userId)
|
||||||
})
|
})
|
||||||
assert(user, 404, `User not found "${userId}"`)
|
assert(user, 404, `User not found "${userId}"`)
|
||||||
acl(c, user, { label: 'User', userField: 'id' })
|
|
||||||
|
|
||||||
return c.json(parseZodSchema(schema.userSelectSchema, user))
|
return c.json(parseZodSchema(schema.userSelectSchema, user))
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ const ParamsSchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
|
description: 'Updates a user',
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
operationId: 'updateUser',
|
operationId: 'updateUser',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
|
@ -48,6 +49,7 @@ const route = createRoute({
|
||||||
export function registerV1UsersUpdateUser(app: OpenAPIHono<AuthenticatedEnv>) {
|
export function registerV1UsersUpdateUser(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
||||||
const { userId } = c.req.valid('param')
|
const { userId } = c.req.valid('param')
|
||||||
|
await acl(c, { userId }, { label: 'User' })
|
||||||
const body = c.req.valid('json')
|
const body = c.req.valid('json')
|
||||||
|
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
|
@ -56,7 +58,6 @@ export function registerV1UsersUpdateUser(app: OpenAPIHono<AuthenticatedEnv>) {
|
||||||
.where(eq(schema.users.id, userId))
|
.where(eq(schema.users.id, userId))
|
||||||
.returning()
|
.returning()
|
||||||
assert(user, 404, `User not found "${userId}"`)
|
assert(user, 404, `User not found "${userId}"`)
|
||||||
acl(c, user, { label: 'User', userField: 'id' })
|
|
||||||
|
|
||||||
return c.json(parseZodSchema(schema.userSelectSchema, user))
|
return c.json(parseZodSchema(schema.userSelectSchema, user))
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,7 +19,9 @@ export {
|
||||||
arrayContained,
|
arrayContained,
|
||||||
arrayContains,
|
arrayContains,
|
||||||
arrayOverlaps,
|
arrayOverlaps,
|
||||||
|
asc,
|
||||||
between,
|
between,
|
||||||
|
desc,
|
||||||
eq,
|
eq,
|
||||||
exists,
|
exists,
|
||||||
gt,
|
gt,
|
||||||
|
|
|
@ -3,13 +3,16 @@ import {
|
||||||
boolean,
|
boolean,
|
||||||
index,
|
index,
|
||||||
pgTable,
|
pgTable,
|
||||||
primaryKey
|
primaryKey,
|
||||||
|
text
|
||||||
} from '@fisch0920/drizzle-orm/pg-core'
|
} from '@fisch0920/drizzle-orm/pg-core'
|
||||||
|
|
||||||
import { teams } from './team'
|
import { teams } from './team'
|
||||||
import { users } from './user'
|
import { users } from './user'
|
||||||
import {
|
import {
|
||||||
|
createInsertSchema,
|
||||||
createSelectSchema,
|
createSelectSchema,
|
||||||
|
createUpdateSchema,
|
||||||
cuid,
|
cuid,
|
||||||
teamMemberRoleEnum,
|
teamMemberRoleEnum,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -24,6 +27,9 @@ export const teamMembers = pgTable(
|
||||||
userId: cuid()
|
userId: cuid()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
teamSlug: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => teams.slug, { onDelete: 'cascade' }),
|
||||||
teamId: cuid()
|
teamId: cuid()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => teams.id, { onDelete: 'cascade' }),
|
.references(() => teams.id, { onDelete: 'cascade' }),
|
||||||
|
@ -36,6 +42,7 @@ export const teamMembers = pgTable(
|
||||||
primaryKey({ columns: [table.userId, table.teamId] }),
|
primaryKey({ columns: [table.userId, table.teamId] }),
|
||||||
index('team_member_user_idx').on(table.userId),
|
index('team_member_user_idx').on(table.userId),
|
||||||
index('team_member_team_idx').on(table.teamId),
|
index('team_member_team_idx').on(table.teamId),
|
||||||
|
index('team_member_slug_idx').on(table.teamSlug),
|
||||||
index('team_member_createdAt_idx').on(table.createdAt),
|
index('team_member_createdAt_idx').on(table.createdAt),
|
||||||
index('team_member_updatedAt_idx').on(table.updatedAt)
|
index('team_member_updatedAt_idx').on(table.updatedAt)
|
||||||
]
|
]
|
||||||
|
@ -52,5 +59,14 @@ export const teamMembersRelations = relations(teamMembers, ({ one }) => ({
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const teamMemberInsertSchema = createInsertSchema(teamMembers).pick({
|
||||||
|
userId: true,
|
||||||
|
role: true
|
||||||
|
})
|
||||||
|
|
||||||
export const teamMemberSelectSchema =
|
export const teamMemberSelectSchema =
|
||||||
createSelectSchema(teamMembers).openapi('TeamMember')
|
createSelectSchema(teamMembers).openapi('TeamMember')
|
||||||
|
|
||||||
|
export const teamMemberUpdateSchema = createUpdateSchema(teamMembers).pick({
|
||||||
|
role: true
|
||||||
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { validators } from '@agentic/validators'
|
||||||
import { relations } from '@fisch0920/drizzle-orm'
|
import { relations } from '@fisch0920/drizzle-orm'
|
||||||
import {
|
import {
|
||||||
index,
|
index,
|
||||||
|
@ -44,7 +45,18 @@ export const teamsRelations = relations(teams, ({ one, many }) => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const teamInsertSchema = createInsertSchema(teams, {
|
export const teamInsertSchema = createInsertSchema(teams, {
|
||||||
slug: (schema) => schema.min(3).max(20) // TODO
|
slug: (schema) =>
|
||||||
})
|
schema.refine((slug) => validators.team(slug), {
|
||||||
|
message: 'Invalid team slug'
|
||||||
|
})
|
||||||
|
}).omit({ id: true, createdAt: true, updatedAt: true, ownerId: true })
|
||||||
|
|
||||||
export const teamSelectSchema = createSelectSchema(teams).openapi('Team')
|
export const teamSelectSchema = createSelectSchema(teams).openapi('Team')
|
||||||
export const teamUpdateSchema = createUpdateSchema(teams).omit({ slug: true })
|
|
||||||
|
export const teamUpdateSchema = createUpdateSchema(teams).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
ownerId: true,
|
||||||
|
slug: true
|
||||||
|
})
|
||||||
|
|
|
@ -111,3 +111,116 @@ export const optionalCuid = optional('cuid')
|
||||||
export const optionalStripeId = optional('stripeId')
|
export const optionalStripeId = optional('stripeId')
|
||||||
export const optionalProjectId = optional('projectId')
|
export const optionalProjectId = optional('projectId')
|
||||||
export const optionalDeploymentId = optional('deploymentId')
|
export const optionalDeploymentId = optional('deploymentId')
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
|
||||||
|
type MaybePromise<T> = Promise<T> | T;
|
||||||
|
type RequestTypes = {
|
||||||
|
body?: ZodRequestBody;
|
||||||
|
params?: ZodType;
|
||||||
|
query?: ZodType;
|
||||||
|
cookies?: ZodType;
|
||||||
|
headers?: ZodType | ZodType[];
|
||||||
|
};
|
||||||
|
type InputTypeBase<R extends RouteConfig, Part extends string, Type extends keyof ValidationTargets> = R['request'] extends RequestTypes ? RequestPart<R, Part> extends ZodType ? {
|
||||||
|
in: {
|
||||||
|
[K in Type]: HasUndefined<ValidationTargets[K]> extends true ? {
|
||||||
|
[K2 in keyof z.input<RequestPart<R, Part>>]?: z.input<RequestPart<R, Part>>[K2];
|
||||||
|
} : {
|
||||||
|
[K2 in keyof z.input<RequestPart<R, Part>>]: z.input<RequestPart<R, Part>>[K2];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
out: {
|
||||||
|
[K in Type]: z.output<RequestPart<R, Part>>;
|
||||||
|
};
|
||||||
|
} : {} : {};
|
||||||
|
|
||||||
|
type InputTypeParam<R extends RouteConfig> = InputTypeBase<R, 'params', 'param'>;
|
||||||
|
type InputTypeQuery<R extends RouteConfig> = InputTypeBase<R, 'query', 'query'>;
|
||||||
|
type InputTypeHeader<R extends RouteConfig> = InputTypeBase<R, 'headers', 'header'>;
|
||||||
|
type InputTypeCookie<R extends RouteConfig> = InputTypeBase<R, 'cookies', 'cookie'>;
|
||||||
|
|
||||||
|
type o: <R extends RouteConfig, I extends Input = InputTypeParam<R> & InputTypeQuery<R> & InputTypeHeader<R> & InputTypeCookie<R> & InputTypeForm<R> & InputTypeJson<R>, P extends string = ConvertPathType<R["path"]>>({ middleware: routeMiddleware, hide, ...route }: R, handler: Handler<R["middleware"] extends MiddlewareHandler[] | MiddlewareHandler ? RouteMiddlewareParam<R>["env"] & E : E, P, I, R extends {
|
||||||
|
responses: {
|
||||||
|
[statusCode: number]: {
|
||||||
|
content: {
|
||||||
|
[mediaType: string]: ZodMediaTypeObject;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} ? MaybePromise<RouteConfigToTypedResponse<R>> : MaybePromise<RouteConfigToTypedResponse<R>> | MaybePromise<Response>>) => OpenAPIHono<E, S & ToSchema<R["method"], MergePath<BasePath, P>, I, RouteConfigToTypedResponse<R>>, BasePath>;
|
||||||
|
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
import type { SetOptional, SetRequired, Simplify } from 'type-fest'
|
||||||
|
import type { AnyZodObject } from 'zod'
|
||||||
|
import { createRoute, type RouteConfig } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
export type CreateOpenAPIHonoRouteOpts<
|
||||||
|
TAuthenticated extends boolean = true,
|
||||||
|
TMethod extends RouteConfig['method'] = RouteConfig['method']
|
||||||
|
> = Simplify<
|
||||||
|
{
|
||||||
|
authenticated: TAuthenticated
|
||||||
|
paramsSchema?: AnyZodObject
|
||||||
|
bodySchema?: AnyZodObject
|
||||||
|
responseSchema: AnyZodObject
|
||||||
|
method: TMethod
|
||||||
|
} & SetRequired<
|
||||||
|
Omit<
|
||||||
|
SetOptional<Parameters<typeof createRoute>[0], 'responses'>,
|
||||||
|
'request' | 'responses' | 'security'
|
||||||
|
>,
|
||||||
|
'path' | 'operationId' | 'tags' | 'description'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
export function createOpenAPIHonoRoute<TAuthenticated extends boolean>(
|
||||||
|
opts: CreateOpenAPIHonoRouteOpts<TAuthenticated>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
authenticated,
|
||||||
|
paramsSchema,
|
||||||
|
bodySchema,
|
||||||
|
responseSchema,
|
||||||
|
...createRouteOpts
|
||||||
|
} = opts
|
||||||
|
|
||||||
|
return createRoute({
|
||||||
|
...createRouteOpts,
|
||||||
|
security: authenticated
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema!,
|
||||||
|
body: bodySchema
|
||||||
|
? {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: responseSchema.shape.description,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: responseSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// ...openApiErrorResponses
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -21,3 +21,32 @@ export const deploymentIdSchema = z
|
||||||
.refine((id) => validators.deployment(id), {
|
.refine((id) => validators.deployment(id), {
|
||||||
message: 'Invalid deployment id'
|
message: 'Invalid deployment id'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const teamSlugSchema = z
|
||||||
|
.string()
|
||||||
|
.refine((slug) => validators.team(slug), {
|
||||||
|
message: 'Invalid team slug'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const paginationSchema = z.object({
|
||||||
|
offset: z.number().int().nonnegative().default(0).optional(),
|
||||||
|
limit: z.number().int().positive().max(100).default(10).optional(),
|
||||||
|
sort: z.enum(['asc', 'desc']).default('desc').optional(),
|
||||||
|
sortBy: z.enum(['createdAt', 'updatedAt']).default('createdAt').optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
// import type { PgTable, TableConfig } from '@fisch0920/drizzle-orm/pg-core'
|
||||||
|
// import type { AnyZodObject } from 'zod'
|
||||||
|
//
|
||||||
|
// export function createWhereFilterSchema<
|
||||||
|
// TTableConfig extends TableConfig,
|
||||||
|
// TTable extends PgTable<TTableConfig>,
|
||||||
|
// T extends AnyZodObject
|
||||||
|
// >(table: TTable, schema: T) {
|
||||||
|
// return z.object({
|
||||||
|
// where: z.record(
|
||||||
|
// z.enum(Object.keys(table._.columns) as [string, ...string[]]),
|
||||||
|
// z.string()
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { AuthenticatedContext } from './types'
|
||||||
|
import { assert } from './utils'
|
||||||
|
|
||||||
|
export async function aclAdmin(ctx: AuthenticatedContext) {
|
||||||
|
const user = ctx.get('user')
|
||||||
|
assert(user, 401, 'Authentication required')
|
||||||
|
assert(user.role === 'admin', 403, 'Access denied')
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { and, db, eq, schema, type TeamMember } from '@/db'
|
||||||
|
|
||||||
|
import type { AuthenticatedContext } from './types'
|
||||||
|
import { assert } from './utils'
|
||||||
|
|
||||||
|
export async function aclTeamAdmin(
|
||||||
|
ctx: AuthenticatedContext,
|
||||||
|
{
|
||||||
|
teamSlug,
|
||||||
|
teamMember
|
||||||
|
}: {
|
||||||
|
teamSlug: string
|
||||||
|
teamMember?: TeamMember
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const user = ctx.get('user')
|
||||||
|
assert(user, 401, 'Authentication required')
|
||||||
|
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
// TODO: Allow admins to access all team resources
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teamMember) {
|
||||||
|
teamMember = await db.query.teamMembers.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.teamMembers.teamSlug, teamSlug),
|
||||||
|
eq(schema.teamMembers.userId, user.id)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
teamMember.role === 'admin',
|
||||||
|
403,
|
||||||
|
`User does not have "admin" role for team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
teamMember.userId === user.id,
|
||||||
|
403,
|
||||||
|
`User does not have access to team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
teamMember.confirmed,
|
||||||
|
403,
|
||||||
|
`User has not confirmed their invitation to team "${teamSlug}"`
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,24 +1,50 @@
|
||||||
import type { TeamMemberWithTeam } from '@/db'
|
import { and, db, eq, schema, type TeamMember } from '@/db'
|
||||||
|
|
||||||
import type { AuthenticatedContext } from './types'
|
import type { AuthenticatedContext } from './types'
|
||||||
import { assert } from './utils'
|
import { assert } from './utils'
|
||||||
|
|
||||||
export async function aclTeamMember(
|
export async function aclTeamMember(
|
||||||
ctx: AuthenticatedContext,
|
ctx: AuthenticatedContext,
|
||||||
teamMember: TeamMemberWithTeam
|
{
|
||||||
|
teamSlug,
|
||||||
|
teamMember,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
teamSlug: string
|
||||||
|
teamMember?: TeamMember
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
const user = ctx.get('user')
|
const user = ctx.get('user')
|
||||||
assert(user, 401, 'Authentication required')
|
assert(user, 401, 'Authentication required')
|
||||||
|
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
// TODO: Allow admins to access all team resources
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId ??= user.id
|
||||||
|
|
||||||
|
if (!teamMember) {
|
||||||
|
teamMember = await db.query.teamMembers.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.teamMembers.teamSlug, teamSlug),
|
||||||
|
eq(schema.teamMembers.userId, userId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
teamMember.userId === user.id,
|
teamMember.userId === userId,
|
||||||
403,
|
403,
|
||||||
`User does not have access to team "${teamMember.team.slug}"`
|
`User does not have access to team "${teamSlug}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
teamMember.confirmed,
|
teamMember.confirmed,
|
||||||
403,
|
403,
|
||||||
`User has not confirmed their invitation to team "${teamMember.team.slug}"`
|
`User has not confirmed their invitation to team "${teamSlug}"`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { AuthenticatedContext } from './types'
|
import type { AuthenticatedContext } from './types'
|
||||||
import { assert } from './utils'
|
import { assert } from './utils'
|
||||||
|
|
||||||
export function acl<
|
export async function acl<
|
||||||
TModel extends Record<string, unknown> & { id: string },
|
TModel extends Record<string, unknown>,
|
||||||
TUserField extends keyof TModel = 'user',
|
TUserField extends keyof TModel = 'user',
|
||||||
TTeamField extends keyof TModel = 'team'
|
TTeamField extends keyof TModel = 'team'
|
||||||
>(
|
>(
|
||||||
|
@ -10,8 +10,8 @@ export function acl<
|
||||||
model: TModel,
|
model: TModel,
|
||||||
{
|
{
|
||||||
label,
|
label,
|
||||||
userField = 'user' as TUserField,
|
userField = 'userId' as TUserField,
|
||||||
teamField = 'team' as TTeamField
|
teamField = 'teamId' as TTeamField
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
userField?: TUserField
|
userField?: TUserField
|
||||||
|
@ -34,6 +34,6 @@ export function acl<
|
||||||
assert(
|
assert(
|
||||||
isAuthUserOwner || isAuthUserAdmin || hasTeamAccess,
|
isAuthUserOwner || isAuthUserAdmin || hasTeamAccess,
|
||||||
403,
|
403,
|
||||||
`User does not have access to ${label} "${model.id}"`
|
`User does not have access to ${label} "${model.id ?? userFieldValue}"`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { db, eq, schema } from '@/db'
|
||||||
|
|
||||||
|
import { assert } from './utils'
|
||||||
|
|
||||||
|
export async function ensureUniqueTeamSlug(slug: string) {
|
||||||
|
slug = slug.toLocaleLowerCase()
|
||||||
|
|
||||||
|
const [existingTeam, existingUser] = await Promise.all([
|
||||||
|
db.query.teams.findFirst({
|
||||||
|
where: eq(schema.teams.slug, slug)
|
||||||
|
}),
|
||||||
|
|
||||||
|
db.query.users.findFirst({
|
||||||
|
where: eq(schema.users.username, slug)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
assert(
|
||||||
|
!existingUser && !existingTeam,
|
||||||
|
409,
|
||||||
|
`Team slug [${slug}] is not available`
|
||||||
|
)
|
||||||
|
}
|
|
@ -24,6 +24,10 @@ export function username(value: string): boolean {
|
||||||
return !!value && usernameRe.test(value)
|
return !!value && usernameRe.test(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function team(value: string): boolean {
|
||||||
|
return username(value)
|
||||||
|
}
|
||||||
|
|
||||||
export function password(value: string): boolean {
|
export function password(value: string): boolean {
|
||||||
return !!value && passwordRe.test(value)
|
return !!value && passwordRe.test(value)
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue