Skip to content

Commit 4abda17

Browse files
authored
Merge branch 'main' into docs/affiliations-api
2 parents d9e3718 + e885a25 commit 4abda17

37 files changed

Lines changed: 445 additions & 139 deletions

File tree

backend/.env.dist.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,5 @@ CROWD_GITHUB_IS_SNOWFLAKE_ENABLED=false
165165
CROWD_TINYBIRD_BASE_URL=http://localhost:7181/
166166

167167
# Auth0
168-
CROWD_AUTH0_ISSUER_BASE_URL=
168+
CROWD_AUTH0_ISSUER_BASE_URLS=
169169
CROWD_AUTH0_AUDIENCE=

backend/config/custom-environment-variables.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
"auth0": {
155155
"clientId": "CROWD_AUTH0_CLIENT_ID",
156156
"jwks": "CROWD_AUTH0_JWKS",
157-
"issuerBaseURL": "CROWD_AUTH0_ISSUER_BASE_URL",
157+
"issuerBaseURLs": "CROWD_AUTH0_ISSUER_BASE_URLS",
158158
"audience": "CROWD_AUTH0_AUDIENCE"
159159
},
160160
"sso": {

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import { UnauthorizedError } from '@crowd/common'
66
import type { Auth0Configuration } from '@/conf/configTypes'
77
import type { Auth0TokenPayload } from '@/types/api'
88

9+
function resolveIssuer(req: Request): string | undefined {
10+
const token = req.headers.authorization?.split(' ')[1]
11+
if (!token) return undefined
12+
try {
13+
const { iss } = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString())
14+
return typeof iss === 'string' ? iss : undefined
15+
} catch {
16+
return undefined
17+
}
18+
}
19+
920
function resolveActor(req: Request, _res: Response, next: NextFunction): void {
1021
const payload = (req.auth?.payload ?? {}) as Auth0TokenPayload
1122

@@ -26,11 +37,38 @@ function resolveActor(req: Request, _res: Response, next: NextFunction): void {
2637
}
2738

2839
export function oauth2Middleware(config: Auth0Configuration): RequestHandler[] {
29-
return [
30-
auth({
31-
issuerBaseURL: config.issuerBaseURL,
32-
audience: config.audience,
33-
}),
34-
resolveActor,
35-
]
40+
const issuers = config.issuerBaseURLs
41+
.split(',')
42+
.map((s) => s.trim())
43+
.filter(Boolean)
44+
45+
if (issuers.length === 0) {
46+
throw new Error('No auth0 issuers configured')
47+
}
48+
49+
const handlersByIssuer = new Map(
50+
issuers.map((issuerBaseURL) => [
51+
issuerBaseURL.replace(/\/$/, ''),
52+
auth({ issuerBaseURL, audience: config.audience }),
53+
]),
54+
)
55+
56+
const verifyJwt: RequestHandler = (req, res, next) => {
57+
const iss = resolveIssuer(req)
58+
if (!iss) {
59+
next(new UnauthorizedError('Missing or malformed bearer token'))
60+
return
61+
}
62+
63+
const handler = handlersByIssuer.get(iss.replace(/\/$/, ''))
64+
65+
if (!handler) {
66+
next(new UnauthorizedError('Unknown token issuer'))
67+
return
68+
}
69+
70+
handler(req, res, next)
71+
}
72+
73+
return [verifyJwt, resolveActor]
3674
}

backend/src/conf/configTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ApiConfiguration {
6868
export interface Auth0Configuration {
6969
clientId: string
7070
jwks: string
71-
issuerBaseURL: string
71+
issuerBaseURLs: string
7272
audience: string
7373
}
7474

backend/src/database/flyway_migrate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ flyway \
1010
-password="$PGPASSWORD" \
1111
-connectRetries=60 \
1212
-outOfOrder=true \
13+
-mixed=true \
1314
-placeholderReplacement=false \
1415
-schemas=public \
1516
-X \

backend/src/database/migrations/U1774609007__data-sink-worker-optimizations.sql

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Drop 4 unused activityRelations indexes (already dropped on prod 2026-03-27,
2+
-- see ACTIVITYRELATIONS_INDEX_CLEANUP.md — IF EXISTS guards for idempotency)
3+
alter table "activityRelations" drop constraint if exists "activityRelations_activityId_memberId_key";
4+
5+
drop index concurrently if exists "ix_activityRelations_memberId_segmentId_include";
6+
drop index concurrently if exists "ix_activityRelations_organizationId_segmentId_include";
7+
drop index concurrently if exists "ix_activityRelations_platform_username";
8+
9+
create index concurrently if not exists idx_osa_org_segment_membercount
10+
on "organizationSegmentsAgg" ("organizationId", "segmentId")
11+
include ("memberCount");

backend/src/product/flyway_migrate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ flyway \
1010
-password="$PGPASSWORD" \
1111
-connectRetries=60 \
1212
-outOfOrder=true \
13+
-mixed=true \
1314
-placeholderReplacement=false \
1415
-schemas=public \
1516
-X \

services/apps/data_sink_worker/src/service/activity.service.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
} from '@crowd/data-access-layer'
2929
import { IDbActivityRelation } from '@crowd/data-access-layer/src/activityRelations/types'
3030
import { DbStore, arePrimitivesDbEqual } from '@crowd/data-access-layer/src/database'
31-
import { getMemberNoMerge } from '@crowd/data-access-layer/src/member_merge'
3231
import {
3332
IActivityRelationCreateOrUpdateData,
3433
IDbActivity,
@@ -56,7 +55,7 @@ import {
5655
} from '@crowd/types'
5756

5857
import { IActivityUpdateData, ISentimentActivityInput } from './activity.data'
59-
import MemberService from './member.service'
58+
import MemberService, { mergeIfAllowed } from './member.service'
6059
import { IProcessActivityResult } from './types'
6160

6261
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -293,6 +292,28 @@ export default class ActivityService extends LoggerBase {
293292
}
294293
}
295294

295+
// When activity.username is set but differs from the member's platform identity value,
296+
// override it so the member lookup and the identity insert use the same key.
297+
// Example: git activities set activity.username to the author display name (e.g. "John Doe")
298+
// while the identity stores the email (e.g. "john.doe@example.com"). Without this correction
299+
// the lookup misses the existing member, creating an unnecessary orphan member.
300+
if (username && member) {
301+
const platformIdentity = member.identities.find(
302+
(i) =>
303+
i.platform === platform &&
304+
i.type === MemberIdentityType.USERNAME &&
305+
i.value &&
306+
i.verified,
307+
)
308+
if (platformIdentity && platformIdentity.value !== username) {
309+
this.log.debug(
310+
{ platform, originalUsername: username, correctedUsername: platformIdentity.value },
311+
'Overriding activity.username with member platform identity value',
312+
)
313+
activity.username = platformIdentity.value
314+
}
315+
}
316+
296317
member.identities = member.identities.filter((i) => i.value)
297318

298319
if (!username) {
@@ -1721,32 +1742,24 @@ export default class ActivityService extends LoggerBase {
17211742
const originalId = metadata.memberWithIdentity as string
17221743
const targetId = metadata.memberIdToUpdate as string
17231744

1724-
// but first check memberNoMerge table
1725-
const noMergeMemberIds = await getMemberNoMerge(this.pgQx, [originalId, targetId])
1726-
1727-
const noMerge = singleOrDefault(
1728-
noMergeMemberIds,
1729-
(m) =>
1730-
(m.memberId === originalId && m.noMergeId === targetId) ||
1731-
(m.memberId === targetId && m.noMergeId === originalId),
1732-
)
1733-
1734-
if (noMerge) {
1735-
metadata.noMerge = true
1736-
} else {
1737-
try {
1738-
await this.pgQx.tx(async (txPgQx) => {
1739-
const service = new CommonMemberService(txPgQx, this.temporal, this.log)
1740-
await service.merge(originalId, targetId)
1741-
})
1742-
1745+
try {
1746+
const merged = await mergeIfAllowed(
1747+
this.pgQx,
1748+
this.temporal,
1749+
this.log,
1750+
originalId,
1751+
targetId,
1752+
)
1753+
if (merged) {
17431754
return originalId
1744-
} catch (err) {
1745-
metadata.mergeError = {
1746-
errorMessage: err?.message ?? '<no error message>',
1747-
errorStack: err?.stack,
1748-
err,
1749-
}
1755+
} else {
1756+
metadata.noMerge = true
1757+
}
1758+
} catch (err) {
1759+
metadata.mergeError = {
1760+
errorMessage: err?.message ?? '<no error message>',
1761+
errorStack: err?.stack,
1762+
err,
17501763
}
17511764
}
17521765
}

0 commit comments

Comments
 (0)