Skip to content

Commit 24de661

Browse files
skwowetcursoragentjoanagmaia
authored
feat: add public v1 endpoints (CM-967) (#3864)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent a2963db commit 24de661

60 files changed

Lines changed: 2528 additions & 997 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"uuid": "^9.0.0",
139139
"validator": "^13.7.0",
140140
"verify-github-webhook": "^1.0.1",
141-
"zlib-sync": "^0.1.8"
141+
"zlib-sync": "^0.1.8",
142+
"zod": "^4.3.6"
142143
},
143144
"private": true,
144145
"devDependencies": {
@@ -150,6 +151,7 @@
150151
"@types/bunyan-format": "^0.2.5",
151152
"@types/config": "^3.3.0",
152153
"@types/cron": "^2.0.0",
154+
"@types/express": "^4.17.17",
153155
"@types/html-to-text": "^8.1.1",
154156
"@types/node": "~18.0.4",
155157
"@types/sanitize-html": "^2.6.2",

backend/src/api/index.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ setImmediate(async () => {
147147

148148
app.use(bodyParser.urlencoded({ limit: '5mb', extended: true }))
149149

150+
app.use((req, res, next) => {
151+
// @ts-ignore
152+
req.userData = {
153+
ip: req.ip,
154+
userAgent: req.headers ? req.headers['user-agent'] : null,
155+
}
156+
157+
next()
158+
})
159+
150160
// Public API uses its own OAuth2 auth and error flow
151161
// Must be mounted before internal endpoints.
152162
app.use('/', publicRouter())
@@ -164,16 +174,6 @@ setImmediate(async () => {
164174
// to set the currentUser to the requests
165175
app.use(authMiddleware)
166176

167-
app.use((req, res, next) => {
168-
// @ts-ignore
169-
req.userData = {
170-
ip: req.ip,
171-
userAgent: req.headers ? req.headers['user-agent'] : null,
172-
}
173-
174-
next()
175-
})
176-
177177
app.use('/health', async (req: any, res) => {
178178
try {
179179
const seq = SequelizeRepository.getSequelize(req)
Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,27 @@
1-
import { CommonMemberService } from '@crowd/common_services'
1+
import { CommonMemberService, invalidateMemberQueryCache } from '@crowd/common_services'
22
import { optionsQx } from '@crowd/data-access-layer'
33

4-
import MemberService from '@/services/memberService'
5-
64
import Permissions from '../../security/permissions'
75
import track from '../../segment/track'
86
import PermissionChecker from '../../services/user/permissionChecker'
97

108
export default async (req, res) => {
119
new PermissionChecker(req).validateHas(Permissions.values.memberEdit)
1210

13-
const commonMemberService = new CommonMemberService(optionsQx(req), req.temporal, req.log)
14-
const memberService = new MemberService(req)
11+
const { memberId } = req.params
12+
const { memberToMerge } = req.body
13+
14+
const service = new CommonMemberService(optionsQx(req), req.temporal, req.log)
1515

16-
const payload = await commonMemberService.merge(req.params.memberId, req.body.memberToMerge, req)
16+
const payload = await service.merge(memberId, memberToMerge, req)
1717

18-
// Invalidate member query cache after merge
1918
try {
20-
await memberService.invalidateMemberQueryCache([req.params.memberId, req.body.memberToMerge])
21-
req.log.debug('Invalidated member query cache after merge')
19+
await invalidateMemberQueryCache(req.redis, [memberId, memberToMerge])
2220
} catch (error) {
23-
// Don't fail the merge if cache invalidation fails
24-
req.log.warn('Failed to invalidate member query cache after merge', { error })
21+
req.log.warn({ error }, 'Cache invalidation failed after member merge')
2522
}
2623

27-
track(
28-
'Merge members',
29-
{ memberId: req.params.memberId, memberToMergeId: req.body.memberToMerge },
30-
{ ...req },
31-
)
32-
33-
const status = payload.status || 200
24+
track('Merge members', { memberId, memberToMergeId: memberToMerge }, req)
3425

35-
await req.responseHandler.success(req, res, payload, status)
26+
return req.responseHandler.success(req, res, payload, payload.status ?? 200)
3627
}

backend/src/api/member/memberUnmerge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export default async (req, res) => {
77

88
const payload = await new MemberService(req).unmerge(req.params.memberId, req.body)
99

10-
await req.responseHandler.success(req, res, payload, 200)
10+
return req.responseHandler.success(req, res, payload)
1111
}

backend/src/api/public/middlewares/errorHandler.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from 'express-oauth2-jwt-bearer'
66

77
import { HttpError, InsufficientScopeError, InternalError, UnauthorizedError } from '@crowd/common'
8+
import { SlackChannel, SlackPersona, sendSlackNotification } from '@crowd/slack'
89

910
/**
1011
* Converts errors to structured JSON: `{ error: { code, message } }`.
@@ -33,6 +34,35 @@ export const errorHandler: ErrorRequestHandler = (
3334
return
3435
}
3536

37+
req.log.error(
38+
{ error, url: req.url, method: req.method, query: req.query, body: req.body },
39+
'Unhandled error in public API',
40+
)
41+
42+
sendSlackNotification(
43+
SlackChannel.ALERTS,
44+
SlackPersona.ERROR_REPORTER,
45+
`Public API Error 500: ${req.method} ${req.url}`,
46+
[
47+
{
48+
title: 'Request',
49+
text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\``,
50+
},
51+
{
52+
title: 'Error',
53+
text: `*Name:* \`${error?.name || 'Unknown'}\`\n*Message:* ${error?.message || 'No message'}`,
54+
},
55+
...(error?.stack
56+
? [
57+
{
58+
title: 'Stack Trace',
59+
text: `\`\`\`${error.stack.substring(0, 2700)}\`\`\``,
60+
},
61+
]
62+
: []),
63+
],
64+
)
65+
3666
const unknownError = new InternalError()
3767
res.status(unknownError.status).json(unknownError.toJSON())
3868
}

backend/src/api/public/middlewares/oauth2Middleware.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { auth } from 'express-oauth2-jwt-bearer'
44
import { UnauthorizedError } from '@crowd/common'
55

66
import type { Auth0Configuration } from '@/conf/configTypes'
7-
import type { ApiRequest, Auth0TokenPayload } from '@/types/api'
7+
import type { Auth0TokenPayload } from '@/types/api'
88

99
function resolveActor(req: Request, _res: Response, next: NextFunction): void {
1010
const payload = (req.auth?.payload ?? {}) as Auth0TokenPayload
@@ -18,11 +18,9 @@ function resolveActor(req: Request, _res: Response, next: NextFunction): void {
1818

1919
const id = rawId.replace(/@clients$/, '')
2020

21-
const authReq = req as ApiRequest
22-
2321
const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ').filter(Boolean) : []
2422

25-
authReq.actor = { id, type: 'service', scopes }
23+
req.actor = { id, type: 'service', scopes }
2624

2725
next()
2826
}

backend/src/api/public/middlewares/requireScopes.ts

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

33
import { InsufficientScopeError, UnauthorizedError } from '@crowd/common'
44

55
import { Scope } from '@/security/scopes'
6-
import type { ApiRequest } from '@/types/api'
76

87
export const requireScopes =
98
(required: Scope[], mode: 'all' | 'any' = 'all') =>
10-
(req: ApiRequest, _res: Response, next: NextFunction) => {
9+
(req: Request, _res: Response, next: NextFunction) => {
1110
if (!req.actor) {
1211
next(new UnauthorizedError())
1312
return

backend/src/api/public/v1/identities/getIdentities.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

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

Lines changed: 0 additions & 15 deletions
This file was deleted.

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Router } from 'express'
22

3-
import { identitiesRouter } from './identities'
3+
import { membersRouter } from './members'
4+
import { organizationsRouter } from './organizations'
45

56
export function v1Router(): Router {
67
const router = Router()
78

8-
router.use('/identities', identitiesRouter())
9+
router.use('/members', membersRouter())
10+
router.use('/organizations', organizationsRouter())
911

1012
return router
1113
}

0 commit comments

Comments
 (0)