pull/715/head
Travis Fischer 2025-05-21 00:06:39 +07:00
rodzic 93a46249f0
commit bd5c4a8dac
12 zmienionych plików z 337 dodań i 92 usunięć

Wyświetl plik

@ -10,17 +10,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugParamsSchema } from './schemas'
import { teamIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Deletes a team by slug.',
tags: ['teams'],
operationId: 'deleteTeam',
method: 'delete',
path: 'teams/{team}',
path: 'teams/{teamId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugParamsSchema
params: teamIdParamsSchema
},
responses: {
200: {
@ -38,14 +38,14 @@ const route = createRoute({
export function registerV1TeamsDeleteTeam(app: OpenAPIHono<AuthenticatedEnv>) {
return app.openapi(route, async (c) => {
const { team: teamSlug } = c.req.valid('param')
await aclTeamAdmin(c, { teamSlug })
const { teamId } = c.req.valid('param')
await aclTeamAdmin(c, { teamId })
const [team] = await db
.delete(schema.teams)
.where(eq(schema.teams.slug, teamSlug))
.where(eq(schema.teams.id, teamId))
.returning()
assert(team, 404, `Team not found "${teamSlug}"`)
assert(team, 404, `Team not found "${teamId}"`)
return c.json(parseZodSchema(schema.teamSelectSchema, team))
})

Wyświetl plik

@ -10,17 +10,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugParamsSchema } from './schemas'
import { teamIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Gets a team by slug.',
tags: ['teams'],
operationId: 'getTeam',
method: 'get',
path: 'teams/{team}',
path: 'teams/{teamId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugParamsSchema
params: teamIdParamsSchema
},
responses: {
200: {
@ -38,13 +38,13 @@ const route = createRoute({
export function registerV1TeamsGetTeam(app: OpenAPIHono<AuthenticatedEnv>) {
return app.openapi(route, async (c) => {
const { team: teamSlug } = c.req.valid('param')
await aclTeamMember(c, { teamSlug })
const { teamId } = c.req.valid('param')
await aclTeamMember(c, { teamId })
const team = await db.query.teams.findFirst({
where: eq(schema.teams.slug, teamSlug)
where: eq(schema.teams.id, teamId)
})
assert(team, 404, `Team not found "${teamSlug}"`)
assert(team, 404, `Team not found "${teamId}"`)
return c.json(parseZodSchema(schema.teamSelectSchema, team))
})

Wyświetl plik

@ -11,17 +11,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugParamsSchema } from '../schemas'
import { teamIdParamsSchema } from '../schemas'
const route = createRoute({
description: 'Creates a team member.',
tags: ['teams'],
operationId: 'createTeamMember',
method: 'post',
path: 'teams/{team}/members',
path: 'teams/{teamId}/members',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugParamsSchema,
params: teamIdParamsSchema,
body: {
required: true,
content: {
@ -50,36 +50,36 @@ export function registerV1TeamsMembersCreateTeamMember(
app: OpenAPIHono<AuthenticatedEnv>
) {
return app.openapi(route, async (c) => {
const { team: teamSlug } = c.req.valid('param')
const { teamId } = c.req.valid('param')
const body = c.req.valid('json')
await aclTeamAdmin(c, { teamSlug })
await aclTeamAdmin(c, { teamId })
const team = await db.query.teams.findFirst({
where: eq(schema.teams.slug, teamSlug)
where: eq(schema.teams.id, teamId)
})
assert(team, 404, `Team not found "${teamSlug}"`)
assert(team, 404, `Team not found "${teamId}"`)
const existingTeamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamSlug, teamSlug),
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, body.userId)
)
})
assert(
existingTeamMember,
409,
`User "${body.userId}" is already a member of team "${teamSlug}"`
`User "${body.userId}" is already a member of team "${teamId}"`
)
const [teamMember] = await db.insert(schema.teamMembers).values({
...body,
teamSlug,
teamId: team.id
teamId,
teamSlug: team.slug
})
assert(
teamMember,
500,
`Failed to create team member "${body.userId}"for team "${teamSlug}"`
`Failed to create team member "${body.userId}"for team "${teamId}"`
)
// TODO: send team invite email

Wyświetl plik

@ -11,17 +11,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugTeamMemberUserIdParamsSchema } from './schemas'
import { teamIdTeamMemberUserIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Deletes a team member.',
tags: ['teams'],
operationId: 'deleteTeamMember',
method: 'delete',
path: 'teams/{team}/members/{userId}',
path: 'teams/{teamId}/members/{userId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugTeamMemberUserIdParamsSchema
params: teamIdTeamMemberUserIdParamsSchema
},
responses: {
200: {
@ -41,16 +41,16 @@ export function registerV1TeamsMembersDeleteTeamMember(
app: OpenAPIHono<AuthenticatedEnv>
) {
return app.openapi(route, async (c) => {
const { team: teamSlug, userId } = c.req.valid('param')
const { teamId, userId } = c.req.valid('param')
await aclTeamAdmin(c, { teamSlug })
await aclTeamMember(c, { teamSlug, userId })
await aclTeamAdmin(c, { teamId })
await aclTeamMember(c, { teamId, userId })
const [teamMember] = await db
.delete(schema.teamMembers)
.where(
and(
eq(schema.teamMembers.teamSlug, teamSlug),
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, userId)
)
)
@ -58,7 +58,7 @@ export function registerV1TeamsMembersDeleteTeamMember(
assert(
teamMember,
400,
`Failed to update team member "${userId}" for team "${teamSlug}"`
`Failed to update team member "${userId}" for team "${teamId}"`
)
return c.json(parseZodSchema(schema.teamMemberSelectSchema, teamMember))

Wyświetl plik

@ -2,10 +2,11 @@ import { z } from '@hono/zod-openapi'
import { userIdSchema } from '@/db'
import { teamSlugParamsSchema } from '../schemas'
import { teamIdParamsSchema } from '../schemas'
export const teamIdTeamMemberUserIdParamsSchema = z.object({
...teamIdParamsSchema.shape,
export const teamSlugTeamMemberUserIdParamsSchema = z.object({
...teamSlugParamsSchema.shape,
userId: userIdSchema.openapi({
param: {
description: 'Team member user ID',

Wyświetl plik

@ -11,17 +11,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugTeamMemberUserIdParamsSchema } from './schemas'
import { teamIdTeamMemberUserIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Updates a team member.',
tags: ['teams'],
operationId: 'updateTeamMember',
method: 'post',
path: 'teams/{team}/members/{userId}',
path: 'teams/{teamId}/members/{userId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugTeamMemberUserIdParamsSchema,
params: teamIdTeamMemberUserIdParamsSchema,
body: {
required: true,
content: {
@ -49,18 +49,18 @@ export function registerV1TeamsMembersUpdateTeamMember(
app: OpenAPIHono<AuthenticatedEnv>
) {
return app.openapi(route, async (c) => {
const { team: teamSlug, userId } = c.req.valid('param')
const { teamId, userId } = c.req.valid('param')
const body = c.req.valid('json')
await aclTeamAdmin(c, { teamSlug })
await aclTeamMember(c, { teamSlug, userId })
await aclTeamAdmin(c, { teamId })
await aclTeamMember(c, { teamId, userId })
const [teamMember] = await db
.update(schema.teamMembers)
.set(body)
.where(
and(
eq(schema.teamMembers.teamSlug, teamSlug),
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, userId)
)
)
@ -68,7 +68,7 @@ export function registerV1TeamsMembersUpdateTeamMember(
assert(
teamMember,
400,
`Failed to update team member "${userId}" for team "${teamSlug}"`
`Failed to update team member "${userId}" for team "${teamId}"`
)
return c.json(parseZodSchema(schema.teamMemberSelectSchema, teamMember))

Wyświetl plik

@ -1,12 +1,12 @@
import { z } from '@hono/zod-openapi'
import { teamSlugSchema } from '@/db'
import { teamIdSchema } from '@/db'
export const teamSlugParamsSchema = z.object({
team: teamSlugSchema.openapi({
export const teamIdParamsSchema = z.object({
teamId: teamIdSchema.openapi({
param: {
description: 'Team slug',
name: 'team',
description: 'Team ID',
name: 'teamId',
in: 'path'
}
})

Wyświetl plik

@ -10,17 +10,17 @@ import {
openapiErrorResponses
} from '@/lib/openapi-utils'
import { teamSlugParamsSchema } from './schemas'
import { teamIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Updates a team.',
tags: ['teams'],
operationId: 'updateTeam',
method: 'post',
path: 'teams/{team}',
path: 'teams/{teamId}',
security: openapiAuthenticatedSecuritySchemas,
request: {
params: teamSlugParamsSchema,
params: teamIdParamsSchema,
body: {
required: true,
content: {
@ -46,16 +46,16 @@ const route = createRoute({
export function registerV1TeamsUpdateTeam(app: OpenAPIHono<AuthenticatedEnv>) {
return app.openapi(route, async (c) => {
const { team: teamSlug } = c.req.valid('param')
const { teamId } = c.req.valid('param')
const body = c.req.valid('json')
await aclTeamAdmin(c, { teamSlug })
await aclTeamAdmin(c, { teamId })
const [team] = await db
.update(schema.teams)
.set(body)
.where(eq(schema.teams.slug, teamSlug))
.where(eq(schema.teams.id, teamId))
.returning()
assert(team, 404, `Team not found "${teamSlug}"`)
assert(team, 404, `Team not found "${teamId}"`)
return c.json(parseZodSchema(schema.teamSelectSchema, team))
})

Wyświetl plik

@ -8,13 +8,27 @@ import { ensureAuthUser } from './ensure-auth-user'
export async function aclTeamAdmin(
ctx: AuthenticatedContext,
{
teamId,
teamSlug,
teamMember
}: {
teamSlug: string
teamId?: string
teamSlug?: string
teamMember?: TeamMember
}
} & (
| {
teamId: string
teamSlug?: never
}
| {
teamId?: never
teamSlug: string
}
)
) {
const teamLabel = teamId ?? teamSlug
assert(teamLabel, 500, 'Either teamSlug or teamId must be provided')
const user = await ensureAuthUser(ctx)
if (user.role === 'admin') {
@ -25,29 +39,31 @@ export async function aclTeamAdmin(
if (!teamMember) {
teamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamSlug, teamSlug),
teamId
? eq(schema.teamMembers.teamId, teamId)
: eq(schema.teamMembers.teamSlug, teamSlug!),
eq(schema.teamMembers.userId, user.id)
)
})
}
assert(teamMember, 403, `User does not have access to team "${teamSlug}"`)
assert(teamMember, 403, `User does not have access to team "${teamLabel}"`)
assert(
teamMember.role === 'admin',
403,
`User does not have "admin" role for team "${teamSlug}"`
`User does not have "admin" role for team "${teamLabel}"`
)
assert(
teamMember.userId === user.id,
403,
`User does not have access to team "${teamSlug}"`
`User does not have access to team "${teamLabel}"`
)
assert(
teamMember.confirmed,
403,
`User has not confirmed their invitation to team "${teamSlug}"`
`User has not confirmed their invitation to team "${teamLabel}"`
)
}

Wyświetl plik

@ -8,13 +8,13 @@ import { ensureAuthUser } from './ensure-auth-user'
export async function aclTeamMember(
ctx: AuthenticatedContext,
{
teamSlug,
teamId,
teamSlug,
teamMember,
userId
}: {
teamSlug?: string
teamId?: string
teamSlug?: string
teamMember?: RawTeamMember
userId?: string
} & (
@ -23,8 +23,10 @@ export async function aclTeamMember(
| { teamMember: RawTeamMember }
)
) {
const teamLabel = teamId ?? teamSlug
assert(teamLabel, 500, 'Either teamSlug or teamId must be provided')
const user = await ensureAuthUser(ctx)
assert(teamSlug || teamId, 500, 'Either teamSlug or teamId must be provided')
if (user.role === 'admin') {
// TODO: Allow admins to access all team resources
@ -36,15 +38,15 @@ export async function aclTeamMember(
if (!teamMember) {
teamMember = await db.query.teamMembers.findFirst({
where: and(
teamSlug
? eq(schema.teamMembers.teamSlug, teamSlug)
: eq(schema.teamMembers.teamId, teamId!),
teamId
? eq(schema.teamMembers.teamId, teamId)
: eq(schema.teamMembers.teamSlug, teamSlug!),
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 "${teamLabel}"`)
if (!ctx.get('teamMember')) {
ctx.set('teamMember', teamMember)
}
@ -52,12 +54,12 @@ export async function aclTeamMember(
assert(
teamMember.userId === userId,
403,
`User does not have access to team "${teamSlug}"`
`User does not have access to team "${teamLabel}"`
)
assert(
teamMember.confirmed,
403,
`User has not confirmed their invitation to team "${teamSlug}"`
`User has not confirmed their invitation to team "${teamLabel}"`
)
}

Wyświetl plik

@ -5,23 +5,33 @@ import type { operations } from './openapi'
import { getEnv } from './utils'
export class AgenticApiClient {
ky: KyInstance
static readonly DEFAULT_API_BASE_URL = 'https://api.agentic.so'
public readonly ky: KyInstance
public readonly apiBaseUrl: string
constructor({
apiKey = getEnv('AGENTIC_API_KEY'),
apiBaseUrl = AgenticApiClient.DEFAULT_API_BASE_URL,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
ky?: KyInstance
}) {
this.ky = ky.extend({ headers: { Authorization: `Bearer ${apiKey}` } })
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: apiBaseUrl,
headers: { Authorization: `Bearer ${apiKey}` }
})
}
async getUser({
userId,
...searchParams
}: OperationParameters<'getUser'>): Promise<OperationResponse<'getUser'>> {
return this.ky.get(`/v1/users/${userId}`, { searchParams }).json()
return this.ky.get(`v1/users/${userId}`, { searchParams }).json()
}
async updateUser(
@ -29,7 +39,223 @@ export class AgenticApiClient {
{ userId, ...searchParams }: OperationParameters<'updateUser'>
): Promise<OperationResponse<'updateUser'>> {
return this.ky
.post(`/v1/users/${userId}`, { json: user, searchParams })
.post(`v1/users/${userId}`, { json: user, searchParams })
.json()
}
async listTeams({
...searchParams
}: OperationParameters<'listTeams'>): Promise<
OperationResponse<'listTeams'>
> {
return this.ky.get('v1/teams', { searchParams }).json()
}
async createTeam(
team: OperationBody<'createTeam'>,
{ ...searchParams }: OperationParameters<'createTeam'>
): Promise<OperationResponse<'createTeam'>> {
return this.ky.post('v1/teams', { json: team, searchParams }).json()
}
async getTeam({
teamId,
...searchParams
}: OperationParameters<'getTeam'>): Promise<OperationResponse<'getTeam'>> {
return this.ky.get(`v1/teams/${teamId}`, { searchParams }).json()
}
async updateTeam(
team: OperationBody<'updateTeam'>,
{ teamId, ...searchParams }: OperationParameters<'updateTeam'>
): Promise<OperationResponse<'updateTeam'>> {
return this.ky
.post(`v1/teams/${teamId}`, { json: team, searchParams })
.json()
}
async deleteTeam({
teamId,
...searchParams
}: OperationParameters<'deleteTeam'>): Promise<
OperationResponse<'deleteTeam'>
> {
return this.ky.delete(`v1/teams/${teamId}`, { searchParams }).json()
}
async createTeamMember(
member: OperationBody<'createTeamMember'>,
{ teamId, ...searchParams }: OperationParameters<'createTeamMember'>
): Promise<OperationResponse<'createTeamMember'>> {
return this.ky
.post(`v1/teams/${teamId}/members`, { json: member, searchParams })
.json()
}
async updateTeamMember(
member: OperationBody<'updateTeamMember'>,
{ teamId, userId, ...searchParams }: OperationParameters<'updateTeamMember'>
): Promise<OperationResponse<'updateTeamMember'>> {
return this.ky
.post(`v1/teams/${teamId}/members/${userId}`, {
json: member,
searchParams
})
.json()
}
async deleteTeamMember({
teamId,
userId,
...searchParams
}: OperationParameters<'deleteTeamMember'>): Promise<
OperationResponse<'deleteTeamMember'>
> {
return this.ky
.delete(`v1/teams/${teamId}/members/${userId}`, { searchParams })
.json()
}
async listProjects({
...searchParams
}: OperationParameters<'listProjects'>): Promise<
OperationResponse<'listProjects'>
> {
return this.ky.get('v1/projects', { searchParams }).json()
}
async createProject(
project: OperationBody<'createProject'>,
{ ...searchParams }: OperationParameters<'createProject'>
): Promise<OperationResponse<'createProject'>> {
return this.ky.post('v1/projects', { json: project, searchParams }).json()
}
async getProject({
projectId,
...searchParams
}: OperationParameters<'getProject'>): Promise<
OperationResponse<'getProject'>
> {
return this.ky.get(`v1/projects/${projectId}`, { searchParams }).json()
}
async updateProject(
project: OperationBody<'updateProject'>,
{ projectId, ...searchParams }: OperationParameters<'updateProject'>
): Promise<OperationResponse<'updateProject'>> {
return this.ky
.post(`v1/projects/${projectId}`, { json: project, searchParams })
.json()
}
async getConsumer({
consumerId,
...searchParams
}: OperationParameters<'getConsumer'>): Promise<
OperationResponse<'getConsumer'>
> {
return this.ky.get(`v1/consumers/${consumerId}`, { searchParams }).json()
}
async updateConsumer(
consumer: OperationBody<'updateConsumer'>,
{ consumerId, ...searchParams }: OperationParameters<'updateConsumer'>
): Promise<OperationResponse<'updateConsumer'>> {
return this.ky
.post(`v1/consumers/${consumerId}`, { json: consumer, searchParams })
.json()
}
async createConsumer(
consumer: OperationBody<'createConsumer'>,
{ ...searchParams }: OperationParameters<'createConsumer'>
): Promise<OperationResponse<'createConsumer'>> {
return this.ky.post('v1/consumers', { json: consumer, searchParams }).json()
}
async refreshConsumerToken({
consumerId,
...searchParams
}: OperationParameters<'refreshConsumerToken'>): Promise<
OperationResponse<'refreshConsumerToken'>
> {
return this.ky
.post(`v1/consumers/${consumerId}/refresh-token`, { searchParams })
.json()
}
async listConsumers({
projectId,
...searchParams
}: OperationParameters<'listConsumers'>): Promise<
OperationResponse<'listConsumers'>
> {
return this.ky
.get(`v1/projects/${projectId}/consumers`, { searchParams })
.json()
}
async getdeployment({
deploymentId,
...searchParams
}: OperationParameters<'getdeployment'>): Promise<
OperationResponse<'getdeployment'>
> {
return this.ky
.get(`v1/deployments/${deploymentId}`, { searchParams })
.json()
}
async updateDeployment(
deployment: OperationBody<'updateDeployment'>,
{ deploymentId, ...searchParams }: OperationParameters<'updateDeployment'>
): Promise<OperationResponse<'updateDeployment'>> {
return this.ky
.post(`v1/deployments/${deploymentId}`, {
json: deployment,
searchParams
})
.json()
}
async listDeployments({
...searchParams
}: OperationParameters<'listDeployments'>): Promise<
OperationResponse<'listDeployments'>
> {
return this.ky.get('v1/deployments', { searchParams }).json()
}
async createDeployment(
deployment: OperationBody<'createDeployment'>,
{ ...searchParams }: OperationParameters<'createDeployment'>
): Promise<OperationResponse<'createDeployment'>> {
return this.ky
.post('v1/deployments', { json: deployment, searchParams })
.json()
}
async publishDeployment(
deployment: OperationBody<'publishDeployment'>,
{ deploymentId, ...searchParams }: OperationParameters<'publishDeployment'>
): Promise<OperationResponse<'publishDeployment'>> {
return this.ky
.post(`v1/deployments/${deploymentId}/publish`, {
json: deployment,
searchParams
})
.json()
}
async adminGetConsumerByToken({
token,
...searchParams
}: OperationParameters<'adminGetConsumerByToken'>): Promise<
OperationResponse<'adminGetConsumerByToken'>
> {
return this.ky
.get(`v1/admin/consumers/tokens/${token}`, { searchParams })
.json()
}
}

Wyświetl plik

@ -77,7 +77,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/teams/{team}": {
"/v1/teams/{teamId}": {
parameters: {
query?: never;
header?: never;
@ -96,7 +96,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/teams/{team}/members": {
"/v1/teams/{teamId}/members": {
parameters: {
query?: never;
header?: never;
@ -113,7 +113,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/teams/{team}/members/{userId}": {
"/v1/teams/{teamId}/members/{userId}": {
parameters: {
query?: never;
header?: never;
@ -706,8 +706,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
};
cookie?: never;
};
@ -733,8 +733,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
};
cookie?: never;
};
@ -768,8 +768,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
};
cookie?: never;
};
@ -795,8 +795,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
};
cookie?: never;
};
@ -832,8 +832,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
/** @description Team member user ID */
userId: string;
};
@ -868,8 +868,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description Team slug */
team: string;
/** @description Team ID */
teamId: string;
/** @description Team member user ID */
userId: string;
};