Skip to content

Commit c89142f

Browse files
committed
feat: add admin apis
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent b765ade commit c89142f

8 files changed

Lines changed: 476 additions & 0 deletions

File tree

backend/src/api/public/v1/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { membersRouter } from './members'
1717
import { organizationsRouter } from './organizations'
1818
import { packagesRouter } from './packages'
1919
import { batchGetStewardship } from './packages/batchGetStewardship'
20+
import { stewardshipsRouter } from './stewardships'
2021

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

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

4042
router.use(() => {
4143
throw new NotFoundError()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Request, Response } from 'express'
2+
import { z } from 'zod'
3+
4+
import { NotFoundError } from '@crowd/common'
5+
import { assignSteward } from '@crowd/data-access-layer'
6+
7+
import { getPackagesQx } from '@/db/packagesDb'
8+
import { ok } from '@/utils/api'
9+
import { validateOrThrow } from '@/utils/validation'
10+
11+
import { stewardshipIdParamsSchema } from './schemas'
12+
13+
const bodySchema = z.object({
14+
userId: z.string().trim().min(1),
15+
role: z.enum(['lead', 'co_steward']),
16+
})
17+
18+
export async function assignStewardHandler(req: Request, res: Response): Promise<void> {
19+
const { id } = validateOrThrow(stewardshipIdParamsSchema, req.params)
20+
const { userId, role } = validateOrThrow(bodySchema, req.body)
21+
22+
const qx = await getPackagesQx()
23+
const result = await assignSteward(qx, id, { userId, role, assignedBy: req.actor.id })
24+
25+
if (!result) {
26+
throw new NotFoundError(`Stewardship not found: ${id}`)
27+
}
28+
29+
ok(res, { stewardship: result.stewardship, stewards: result.stewards })
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Request, Response } from 'express'
2+
import { z } from 'zod'
3+
4+
import { NotFoundError } from '@crowd/common'
5+
import { ESCALATION_RESOLUTION_PATHS, escalateStewardship } from '@crowd/data-access-layer'
6+
7+
import { getPackagesQx } from '@/db/packagesDb'
8+
import { ok } from '@/utils/api'
9+
import { validateOrThrow } from '@/utils/validation'
10+
11+
import { stewardshipIdParamsSchema } from './schemas'
12+
13+
const bodySchema = z.object({
14+
resolutionPath: z.enum(ESCALATION_RESOLUTION_PATHS),
15+
notes: z.string().trim().min(1).optional(),
16+
})
17+
18+
export async function escalateHandler(req: Request, res: Response): Promise<void> {
19+
const { id } = validateOrThrow(stewardshipIdParamsSchema, req.params)
20+
const { resolutionPath, notes } = validateOrThrow(bodySchema, req.body)
21+
22+
const qx = await getPackagesQx()
23+
const stewardship = await escalateStewardship(qx, id, {
24+
resolutionPath,
25+
notes,
26+
actorUserId: req.actor.id,
27+
})
28+
29+
if (!stewardship) {
30+
throw new NotFoundError(`Stewardship not found: ${id}`)
31+
}
32+
33+
ok(res, { stewardship })
34+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Router } from 'express'
2+
3+
import { createRateLimiter } from '@/api/apiRateLimiter'
4+
// TODO: restore once write:stewardships is added to Auth0 staging tenant
5+
// import { requireScopes } from '@/api/public/middlewares/requireScopes'
6+
import { safeWrap } from '@/middlewares/errorMiddleware'
7+
8+
// import { SCOPES } from '@/security/scopes'
9+
import { assignStewardHandler } from './assignSteward'
10+
import { escalateHandler } from './escalate'
11+
import { openStewardship } from './openStewardship'
12+
import { updateStatusHandler } from './updateStatus'
13+
14+
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
15+
16+
export function stewardshipsRouter(): Router {
17+
const router = Router()
18+
19+
router.use(rateLimiter)
20+
21+
router.post(
22+
'/',
23+
// TODO: restore once write:stewardships is added to Auth0 staging tenant
24+
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
25+
safeWrap(openStewardship),
26+
)
27+
28+
router.put(
29+
'/:id/steward',
30+
// TODO: restore once write:stewardships is added to Auth0 staging tenant
31+
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
32+
safeWrap(assignStewardHandler),
33+
)
34+
35+
router.put(
36+
'/:id/escalate',
37+
// TODO: restore once write:stewardships is added to Auth0 staging tenant
38+
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
39+
safeWrap(escalateHandler),
40+
)
41+
42+
router.put(
43+
'/:id/status',
44+
// TODO: restore once write:stewardships is added to Auth0 staging tenant
45+
// requireScopes([SCOPES.WRITE_STEWARDSHIPS]),
46+
safeWrap(updateStatusHandler),
47+
)
48+
49+
return router
50+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Request, Response } from 'express'
2+
import { z } from 'zod'
3+
4+
import { NotFoundError } from '@crowd/common'
5+
import { openStewardshipByPurl } from '@crowd/data-access-layer'
6+
7+
import { getPackagesQx } from '@/db/packagesDb'
8+
import { ok } from '@/utils/api'
9+
import { validateOrThrow } from '@/utils/validation'
10+
11+
import { normalizePurl } from '../packages/purl'
12+
13+
const bodySchema = z.object({
14+
purl: z
15+
.string()
16+
.trim()
17+
.min(1)
18+
.refine((v) => v.startsWith('pkg:'), { message: 'purl must start with pkg:' }),
19+
})
20+
21+
export async function openStewardship(req: Request, res: Response): Promise<void> {
22+
const { purl: rawPurl } = validateOrThrow(bodySchema, req.body)
23+
const purl = normalizePurl(rawPurl)
24+
25+
const qx = await getPackagesQx()
26+
const stewardship = await openStewardshipByPurl(qx, purl, req.actor.id)
27+
28+
if (!stewardship) {
29+
throw new NotFoundError(`Package not found: ${rawPurl}`)
30+
}
31+
32+
ok(res, { stewardship })
33+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { z } from 'zod'
2+
3+
export const stewardshipIdParamsSchema = z.object({
4+
id: z.coerce.number().int().positive(),
5+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Request, Response } from 'express'
2+
import { z } from 'zod'
3+
4+
import { NotFoundError } from '@crowd/common'
5+
import {
6+
INACTIVE_REASONS,
7+
STEWARDSHIP_UPDATABLE_STATUSES,
8+
updateStewardshipStatus,
9+
} from '@crowd/data-access-layer'
10+
11+
import { getPackagesQx } from '@/db/packagesDb'
12+
import { ok } from '@/utils/api'
13+
import { validateOrThrow } from '@/utils/validation'
14+
15+
import { stewardshipIdParamsSchema } from './schemas'
16+
17+
const bodySchema = z
18+
.object({
19+
status: z.enum(STEWARDSHIP_UPDATABLE_STATUSES),
20+
inactiveReason: z.enum(INACTIVE_REASONS).optional(),
21+
notes: z.string().trim().min(1).optional(),
22+
})
23+
.refine((d) => d.status !== 'inactive' || !!d.inactiveReason, {
24+
message: 'inactiveReason is required when status is inactive',
25+
path: ['inactiveReason'],
26+
})
27+
28+
export async function updateStatusHandler(req: Request, res: Response): Promise<void> {
29+
const { id } = validateOrThrow(stewardshipIdParamsSchema, req.params)
30+
const { status, inactiveReason, notes } = validateOrThrow(bodySchema, req.body)
31+
32+
const qx = await getPackagesQx()
33+
const stewardship = await updateStewardshipStatus(qx, id, {
34+
status,
35+
inactiveReason,
36+
notes,
37+
actorUserId: req.actor.id,
38+
})
39+
40+
if (!stewardship) {
41+
throw new NotFoundError(`Stewardship not found: ${id}`)
42+
}
43+
44+
ok(res, { stewardship })
45+
}

0 commit comments

Comments
 (0)