Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/api/public/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { membersRouter } from './members'
import { organizationsRouter } from './organizations'
import { packagesRouter } from './packages'
import { batchGetStewardship } from './packages/batchGetStewardship'
import { stewardshipsRouter } from './stewardships'

const packagesRateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })

Expand All @@ -36,6 +37,7 @@ export function v1Router(): Router {
safeWrap(batchGetStewardship),
)
router.use('/packages', oauth2Middleware(AUTH0_CONFIG), packagesRouter())
router.use('/stewardships', oauth2Middleware(AUTH0_CONFIG), stewardshipsRouter())

router.use(() => {
throw new NotFoundError()
Expand Down
18 changes: 14 additions & 4 deletions backend/src/api/public/v1/packages/getPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { Request, Response } from 'express'
import { z } from 'zod'

import { NotFoundError } from '@crowd/common'
import { getAdvisoriesByPackageId, getPackageDetailByPurl } from '@crowd/data-access-layer'
import {
getAdvisoriesByPackageId,
getPackageDetailByPurl,
getStewardshipSummary,
} from '@crowd/data-access-layer'

import { getPackagesQx } from '@/db/packagesDb'
import { ok } from '@/utils/api'
Expand Down Expand Up @@ -30,7 +34,10 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
throw new NotFoundError()
}

const advisories = await getAdvisoriesByPackageId(qx, pkg.id)
const [advisories, stewardshipSummary] = await Promise.all([
getAdvisoriesByPackageId(qx, pkg.id),
pkg.stewardshipId ? getStewardshipSummary(qx, Number(pkg.stewardshipId)) : null,
])

ok(res, {
purl: pkg.purl,
Expand Down Expand Up @@ -83,9 +90,12 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
},
},
stewardship: {
id: pkg.stewardshipId ?? null,
status: (pkg.stewardshipStatus ?? 'unassigned') as StewardshipStatus,
stewards: null,
lastActivityAt: null,
stewards: stewardshipSummary?.stewards ?? null,
lastActivityAt: stewardshipSummary?.lastActivityAt ?? null,
Comment thread
ulemons marked this conversation as resolved.
resolutionPath: pkg.stewardshipResolutionPath ?? null,
statusNote: pkg.stewardshipStatusNote ?? null,
},
history: {},
})
Expand Down
1 change: 1 addition & 0 deletions backend/src/api/public/v1/packages/listPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
lifecycle: null,
maintainerBusFactor: r.maintainerCount,
openVulns: r.openVulns,
stewardshipId: r.stewardshipId ?? null,
stewardship: (r.stewardshipStatus ?? 'unassigned') as StewardshipStatus,
stewards: null,
}))
Expand Down
38 changes: 31 additions & 7 deletions backend/src/api/public/v1/packages/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,17 @@ components:

Steward:
type: object
required: [userId, name, role, assignedAt]
required: [userId, role, assignedAt]
properties:
userId:
type: string
description: LFID.
example: jdoe
description: Auth0 sub of the assigned steward.
example: abc123
name:
type: string
type:
- string
- 'null'
description: Display name of the steward. Null if not available.
example: Jonathan R.
role:
type: string
Expand Down Expand Up @@ -194,6 +197,12 @@ components:
oneOf:
- $ref: '#/components/schemas/OpenVulns'
- type: 'null'
stewardshipId:
type:
- string
- 'null'
description: Stewardship ID. Required to call mutation endpoints (assign/escalate/status). Null if no stewardship row exists.
example: '42'
stewardship:
$ref: '#/components/schemas/StewardshipStatus'
stewards:
Expand Down Expand Up @@ -418,12 +427,18 @@ components:
- 'null'
stewardship:
type: object
description: Stewardship state. In v1 always unassigned with no stewards or activity.
description: Stewardship state.
properties:
id:
type:
- string
- 'null'
description: Stewardship ID. Required to call mutation endpoints (assign/escalate/status).
example: '42'
status:
$ref: '#/components/schemas/StewardshipStatus'
stewards:
description: Assigned stewards or null. Null in v1.
description: Assigned stewards or null.
oneOf:
- type: array
items:
Expand All @@ -434,7 +449,16 @@ components:
- string
- 'null'
format: date-time
description: Null in v1.
resolutionPath:
type:
- string
- 'null'
description: Set on `escalated` status. Null for all other statuses.
statusNote:
type:
- string
- 'null'
description: Free-text note for the current status.
history:
type: object
description: Package history data. Empty in v1.
Expand Down
38 changes: 38 additions & 0 deletions backend/src/api/public/v1/stewardships/assignSteward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Request, Response } from 'express'
import { z } from 'zod'

import { NotFoundError } from '@crowd/common'
import { assignSteward } from '@crowd/data-access-layer'

import { getPackagesQx } from '@/db/packagesDb'
import { ok } from '@/utils/api'
import { validateOrThrow } from '@/utils/validation'

const paramsSchema = z.object({
id: z.coerce.number().int().positive(),
})

const bodySchema = z.object({
userId: z.string().trim().min(1),
role: z.enum(['lead', 'co_steward']),
moveToAssessing: z.boolean().optional().default(false),
})

export async function assignStewardHandler(req: Request, res: Response): Promise<void> {
const { id } = validateOrThrow(paramsSchema, req.params)
const { userId, role, moveToAssessing } = validateOrThrow(bodySchema, req.body)

const qx = await getPackagesQx()
const result = await assignSteward(qx, id, {
userId,
role,
assignedBy: req.actor.id,
moveToAssessing,
})

if (!result) {
throw new NotFoundError(`Stewardship not found: ${id}`)
}

ok(res, { stewardship: result.stewardship, stewards: result.stewards })
}
36 changes: 36 additions & 0 deletions backend/src/api/public/v1/stewardships/escalate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Request, Response } from 'express'
import { z } from 'zod'

import { NotFoundError } from '@crowd/common'
import { ESCALATION_RESOLUTION_PATHS, escalateStewardship } from '@crowd/data-access-layer'

import { getPackagesQx } from '@/db/packagesDb'
import { ok } from '@/utils/api'
import { validateOrThrow } from '@/utils/validation'

const paramsSchema = z.object({
id: z.coerce.number().int().positive(),
})

const bodySchema = z.object({
resolutionPath: z.enum(ESCALATION_RESOLUTION_PATHS),
notes: z.string().trim().min(1).optional(),
})

export async function escalateHandler(req: Request, res: Response): Promise<void> {
const { id } = validateOrThrow(paramsSchema, req.params)
const { resolutionPath, notes } = validateOrThrow(bodySchema, req.body)

const qx = await getPackagesQx()
const stewardship = await escalateStewardship(qx, id, {
resolutionPath,
notes,
actorUserId: req.actor.id,
})

if (!stewardship) {
throw new NotFoundError(`Stewardship not found: ${id}`)
}

ok(res, { stewardship })
}
50 changes: 50 additions & 0 deletions backend/src/api/public/v1/stewardships/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Router } from 'express'

import { createRateLimiter } from '@/api/apiRateLimiter'
// TODO: restore once write:stewardships is added to Auth0 staging tenant
// import { requireScopes } from '@/api/public/middlewares/requireScopes'
import { safeWrap } from '@/middlewares/errorMiddleware'

Comment thread
ulemons marked this conversation as resolved.
// import { SCOPES } from '@/security/scopes'
import { assignStewardHandler } from './assignSteward'
import { escalateHandler } from './escalate'
import { openStewardship } from './openStewardship'
import { updateStatusHandler } from './updateStatus'

const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })

export function stewardshipsRouter(): Router {
const router = Router()

router.use(rateLimiter)
Comment on lines +1 to +19

router.post(
'/',
// TODO: restore once write:stewardships is added to Auth0 staging tenant
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
safeWrap(openStewardship),
Comment on lines +21 to +25
)
Comment thread
ulemons marked this conversation as resolved.

router.put(
'/:id/steward',
// TODO: restore once write:stewardships is added to Auth0 staging tenant
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
safeWrap(assignStewardHandler),
)

router.put(
'/:id/escalate',
// TODO: restore once write:stewardships is added to Auth0 staging tenant
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
safeWrap(escalateHandler),
)

router.put(
'/:id/status',
// TODO: restore once write:stewardships is added to Auth0 staging tenant
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
safeWrap(updateStatusHandler),
)

return router
}
33 changes: 33 additions & 0 deletions backend/src/api/public/v1/stewardships/openStewardship.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Request, Response } from 'express'
import { z } from 'zod'

import { NotFoundError } from '@crowd/common'
import { openStewardshipByPurl } from '@crowd/data-access-layer'

import { getPackagesQx } from '@/db/packagesDb'
import { ok } from '@/utils/api'
import { validateOrThrow } from '@/utils/validation'

import { normalizePurl } from '../packages/purl'

const bodySchema = z.object({
purl: z
.string()
.trim()
.min(1)
.refine((v) => v.startsWith('pkg:'), { message: 'purl must start with pkg:' })
.transform(normalizePurl),
})

export async function openStewardship(req: Request, res: Response): Promise<void> {
const { purl } = validateOrThrow(bodySchema, req.body)

const qx = await getPackagesQx()
const stewardship = await openStewardshipByPurl(qx, purl, req.actor.id)

if (!stewardship) {
throw new NotFoundError(`Package not found: ${purl}`)
}

ok(res, { stewardship })
}
Loading
Loading