Skip to content

Commit 8865044

Browse files
committed
feat: add actor interface
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 43395bb commit 8865044

12 files changed

Lines changed: 293 additions & 80 deletions

File tree

backend/src/api/public/v1/ossprey/activityFeed.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ export async function activityFeedHandler(req: Request, res: Response): Promise<
2525
packagePurl: r.packagePurl,
2626
packageName: r.packageName,
2727
packageEcosystem: r.packageEcosystem,
28-
actorUserId: r.actorUserId,
29-
actorName: r.actorUserId, // TODO: resolve display name from crowd.dev users/members table by actorUserId
28+
actor: r.actor,
3029
actorType: r.actorType,
3130
activityType: r.activityType,
3231
content: r.content,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { z } from 'zod'
2+
3+
export const actorInputSchema = z.object({
4+
userId: z.string().trim().min(1, { message: 'actor.userId is required and must not be empty' }),
5+
username: z.string().trim().min(1).optional().nullable(),
6+
displayName: z.string().trim().min(1).optional().nullable(),
7+
avatarUrl: z.string().url().optional().nullable(),
8+
})

backend/src/api/public/v1/stewardships/assignSteward.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { NotFoundError } from '@crowd/common'
4+
import { BadRequestError, NotFoundError } from '@crowd/common'
55
import { assignSteward } from '@crowd/data-access-layer'
66

77
import { getPackagesQx } from '@/db/packagesDb'
88
import { ok } from '@/utils/api'
99
import { validateOrThrow } from '@/utils/validation'
1010

11+
import { actorInputSchema } from './actorSchema'
12+
1113
const paramsSchema = z.object({
1214
id: z.coerce.number().int().positive(),
1315
})
@@ -20,6 +22,7 @@ const bodySchema = z
2022
role: z.enum(['lead', 'co_steward']),
2123
note: z.string().trim().min(1).optional(),
2224
moveToAssessing: z.boolean().optional().default(false),
25+
actor: actorInputSchema,
2326
})
2427
.refine((d) => (d.username == null) === (d.displayName == null), {
2528
message: 'username and displayName must both be provided or both be absent',
@@ -28,11 +31,15 @@ const bodySchema = z
2831

2932
export async function assignStewardHandler(req: Request, res: Response): Promise<void> {
3033
const { id } = validateOrThrow(paramsSchema, req.params)
31-
const { userId, username, displayName, role, note, moveToAssessing } = validateOrThrow(
34+
const { userId, username, displayName, role, note, moveToAssessing, actor } = validateOrThrow(
3235
bodySchema,
3336
req.body,
3437
)
3538

39+
if (actor.userId !== req.actor.id) {
40+
throw new BadRequestError('actor.userId must match the authenticated user id')
41+
}
42+
3643
const qx = await getPackagesQx()
3744
const result = await assignSteward(qx, id, {
3845
userId,
@@ -41,6 +48,9 @@ export async function assignStewardHandler(req: Request, res: Response): Promise
4148
role,
4249
note,
4350
assignedBy: req.actor.id,
51+
actorUsername: actor.username ?? null,
52+
actorDisplayName: actor.displayName ?? null,
53+
actorAvatarUrl: actor.avatarUrl ?? null,
4454
moveToAssessing,
4555
})
4656

backend/src/api/public/v1/stewardships/escalate.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,41 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { NotFoundError } from '@crowd/common'
4+
import { BadRequestError, NotFoundError } from '@crowd/common'
55
import { ESCALATION_RESOLUTION_PATHS, escalateStewardship } from '@crowd/data-access-layer'
66

77
import { getPackagesQx } from '@/db/packagesDb'
88
import { ok } from '@/utils/api'
99
import { validateOrThrow } from '@/utils/validation'
1010

11+
import { actorInputSchema } from './actorSchema'
12+
1113
const paramsSchema = z.object({
1214
id: z.coerce.number().int().positive(),
1315
})
1416

1517
const bodySchema = z.object({
1618
resolutionPath: z.enum(ESCALATION_RESOLUTION_PATHS),
1719
notes: z.string().trim().min(1).optional(),
20+
actor: actorInputSchema,
1821
})
1922

2023
export async function escalateHandler(req: Request, res: Response): Promise<void> {
2124
const { id } = validateOrThrow(paramsSchema, req.params)
22-
const { resolutionPath, notes } = validateOrThrow(bodySchema, req.body)
25+
const { resolutionPath, notes, actor } = validateOrThrow(bodySchema, req.body)
26+
27+
if (actor.userId !== req.actor.id) {
28+
throw new BadRequestError('actor.userId must match the authenticated user id')
29+
}
2330

2431
const qx = await getPackagesQx()
2532
const stewardship = await escalateStewardship(qx, id, {
2633
resolutionPath,
2734
notes,
2835
actorUserId: req.actor.id,
36+
actorUsername: actor.username ?? null,
37+
actorDisplayName: actor.displayName ?? null,
38+
actorAvatarUrl: actor.avatarUrl ?? null,
2939
})
3040

3141
if (!stewardship) {

backend/src/api/public/v1/stewardships/getMyActivity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export async function getMyActivityHandler(req: Request, res: Response): Promise
6363
stewardshipStatus: r.stewardshipStatus,
6464
activityType: r.activityType,
6565
description: r.content,
66+
actor: r.actor,
6667
createdAt: r.createdAt,
6768
suggestedAction: SUGGESTED_ACTIONS[r.stewardshipStatus] ?? null,
6869
})),

backend/src/api/public/v1/stewardships/getMyPackages.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import {
5-
computeHealthBand,
6-
listMyPackages,
7-
translateActivityContent,
8-
} from '@crowd/data-access-layer'
4+
import { computeHealthBand, listMyPackages } from '@crowd/data-access-layer'
95

106
import { getPackagesQx } from '@/db/packagesDb'
117
import { ok } from '@/utils/api'
@@ -42,11 +38,7 @@ export async function getMyPackagesHandler(req: Request, res: Response): Promise
4238
healthBand: computeHealthBand(r.scorecardScore),
4339
openVulns: r.openVulns,
4440
vulnSeverity: r.maxVulnSeverity,
45-
lastActivityDescription: translateActivityContent(
46-
r.lastActivityContent,
47-
r.lastActivityType,
48-
r.lastActivityMetadata,
49-
),
41+
lastActivityDescription: r.lastActivityDescription,
5042
lastActivityAt: r.lastActivityAt ? r.lastActivityAt.toISOString() : null,
5143
stewardshipId: r.stewardshipId,
5244
stewardshipStatus: r.stewardshipStatus,

backend/src/api/public/v1/stewardships/openStewardship.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { NotFoundError } from '@crowd/common'
4+
import { BadRequestError, NotFoundError } from '@crowd/common'
55
import { openStewardshipByPurl } from '@crowd/data-access-layer'
66

77
import { getPackagesQx } from '@/db/packagesDb'
@@ -10,15 +10,29 @@ import { validateOrThrow } from '@/utils/validation'
1010

1111
import { purlFieldSchema } from '../packages/purl'
1212

13+
import { actorInputSchema } from './actorSchema'
14+
1315
const bodySchema = z.object({
1416
purl: purlFieldSchema,
17+
actor: actorInputSchema,
1518
})
1619

1720
export async function openStewardship(req: Request, res: Response): Promise<void> {
18-
const { purl } = validateOrThrow(bodySchema, req.body)
21+
const { purl, actor } = validateOrThrow(bodySchema, req.body)
22+
23+
if (actor.userId !== req.actor.id) {
24+
throw new BadRequestError('actor.userId must match the authenticated user id')
25+
}
1926

2027
const qx = await getPackagesQx()
21-
const stewardship = await openStewardshipByPurl(qx, purl, req.actor.id)
28+
const stewardship = await openStewardshipByPurl(
29+
qx,
30+
purl,
31+
req.actor.id,
32+
actor.username ?? null,
33+
actor.displayName ?? null,
34+
actor.avatarUrl ?? null,
35+
)
2236

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

backend/src/api/public/v1/stewardships/openapi.yaml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,66 @@ components:
4545
type: string
4646
example: Stewardship not found.
4747

48+
ActivityActor:
49+
type: object
50+
required: [userId]
51+
description: Profile of the actor who performed an activity. Stored as a snapshot on the activity log.
52+
properties:
53+
userId:
54+
type: string
55+
description: Auth0 sub of the actor.
56+
example: auth0|abc123
57+
username:
58+
type:
59+
- string
60+
- 'null'
61+
description: LFX username of the actor.
62+
example: gaspergrom
63+
displayName:
64+
type:
65+
- string
66+
- 'null'
67+
description: Full display name of the actor.
68+
example: Gašper Grom
69+
avatarUrl:
70+
type:
71+
- string
72+
- 'null'
73+
format: uri
74+
description: Avatar URL of the actor.
75+
example: 'https://avatars.githubusercontent.com/u/12345'
76+
77+
ActorInput:
78+
type: object
79+
required: [userId]
80+
description: >
81+
Profile of the actor performing this action. Stored as a snapshot on the activity log.
82+
`userId` is required. All other fields are optional and can be null.
83+
properties:
84+
userId:
85+
type: string
86+
description: Auth0 sub of the actor. Must match the authenticated user's token sub.
87+
example: auth0|abc123
88+
username:
89+
type:
90+
- string
91+
- 'null'
92+
description: LFX username of the actor.
93+
example: gaspergrom
94+
displayName:
95+
type:
96+
- string
97+
- 'null'
98+
description: Full display name of the actor.
99+
example: Gašper Grom
100+
avatarUrl:
101+
type:
102+
- string
103+
- 'null'
104+
format: uri
105+
description: Avatar URL of the actor.
106+
example: 'https://avatars.githubusercontent.com/u/12345'
107+
48108
StewardshipStatus:
49109
type: string
50110
enum:
@@ -187,8 +247,14 @@ paths:
187247
type: string
188248
description: Package URL (must start with `pkg:`).
189249
example: pkg:npm/lodash
250+
actor:
251+
$ref: '#/components/schemas/ActorInput'
190252
example:
191253
purl: pkg:npm/lodash
254+
actor:
255+
username: gaspergrom
256+
displayName: Gašper Grom
257+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
192258
responses:
193259
'200':
194260
description: Stewardship opened (or already open).
@@ -288,10 +354,16 @@ paths:
288354
If true, atomically transitions the stewardship status to `assessing`
289355
in the same transaction as the assignment. Use for the "Assign & move to
290356
Assessing" action to avoid a second round-trip.
357+
actor:
358+
$ref: '#/components/schemas/ActorInput'
291359
example:
292360
userId: abc123
293361
role: lead
294362
moveToAssessing: true
363+
actor:
364+
username: gaspergrom
365+
displayName: Gašper Grom
366+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
295367
responses:
296368
'200':
297369
description: Steward assigned.
@@ -386,9 +458,15 @@ paths:
386458
minLength: 1
387459
description: Optional free-text notes for the activity log.
388460
example: Contacted maintainer, no response after 30 days.
461+
actor:
462+
$ref: '#/components/schemas/ActorInput'
389463
example:
390464
resolutionPath: right_of_first_refusal
391465
notes: Contacted maintainer, no response after 30 days.
466+
actor:
467+
username: gaspergrom
468+
displayName: Gašper Grom
469+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
392470
responses:
393471
'200':
394472
description: Stewardship escalated.
@@ -485,22 +563,36 @@ paths:
485563
type: string
486564
minLength: 1
487565
description: Optional free-text notes for the activity log.
566+
actor:
567+
$ref: '#/components/schemas/ActorInput'
488568
examples:
489569
set_active:
490570
summary: Transition to active
491571
value:
492572
status: active
573+
actor:
574+
username: gaspergrom
575+
displayName: Gašper Grom
576+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
493577
set_inactive:
494578
summary: Transition to inactive (inactiveReason required)
495579
value:
496580
status: inactive
497581
inactiveReason: stepped_down
498582
notes: Steward stepped down voluntarily.
583+
actor:
584+
username: gaspergrom
585+
displayName: Gašper Grom
586+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
499587
set_blocked:
500588
summary: Transition to blocked
501589
value:
502590
status: blocked
503591
notes: Waiting on upstream maintainer response.
592+
actor:
593+
username: gaspergrom
594+
displayName: Gašper Grom
595+
avatarUrl: 'https://avatars.githubusercontent.com/u/12345'
504596
responses:
505597
'200':
506598
description: Status updated.

backend/src/api/public/v1/stewardships/updateStatus.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { NotFoundError } from '@crowd/common'
4+
import { BadRequestError, NotFoundError } from '@crowd/common'
55
import {
66
INACTIVE_REASONS,
77
STEWARDSHIP_UPDATABLE_STATUSES,
@@ -12,6 +12,8 @@ import { getPackagesQx } from '@/db/packagesDb'
1212
import { ok } from '@/utils/api'
1313
import { validateOrThrow } from '@/utils/validation'
1414

15+
import { actorInputSchema } from './actorSchema'
16+
1517
const paramsSchema = z.object({
1618
id: z.coerce.number().int().positive(),
1719
})
@@ -21,6 +23,7 @@ const bodySchema = z
2123
status: z.enum(STEWARDSHIP_UPDATABLE_STATUSES),
2224
inactiveReason: z.enum(INACTIVE_REASONS).optional(),
2325
notes: z.string().trim().min(1).optional(),
26+
actor: actorInputSchema,
2427
})
2528
.refine((d) => d.status !== 'inactive' || !!d.inactiveReason, {
2629
message: 'inactiveReason is required when status is inactive',
@@ -29,14 +32,21 @@ const bodySchema = z
2932

3033
export async function updateStatusHandler(req: Request, res: Response): Promise<void> {
3134
const { id } = validateOrThrow(paramsSchema, req.params)
32-
const { status, inactiveReason, notes } = validateOrThrow(bodySchema, req.body)
35+
const { status, inactiveReason, notes, actor } = validateOrThrow(bodySchema, req.body)
36+
37+
if (actor.userId !== req.actor.id) {
38+
throw new BadRequestError('actor.userId must match the authenticated user id')
39+
}
3340

3441
const qx = await getPackagesQx()
3542
const stewardship = await updateStewardshipStatus(qx, id, {
3643
status,
3744
inactiveReason,
3845
notes,
3946
actorUserId: req.actor.id,
47+
actorUsername: actor.username ?? null,
48+
actorDisplayName: actor.displayName ?? null,
49+
actorAvatarUrl: actor.avatarUrl ?? null,
4050
})
4151

4252
if (!stewardship) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ALTER TABLE stewardship_activity
2+
ADD COLUMN IF NOT EXISTS actor_username TEXT,
3+
ADD COLUMN IF NOT EXISTS actor_display_name TEXT,
4+
ADD COLUMN IF NOT EXISTS actor_avatar_url TEXT;

0 commit comments

Comments
 (0)