Skip to content

Commit 04f8416

Browse files
committed
Merge branch 'main' into chore/claude-code-setup
2 parents 88991f7 + e76f62c commit 04f8416

40 files changed

Lines changed: 2358 additions & 81 deletions

backend/.env.dist.composed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CROWD_S3_HOST="s3"
1111
# Db settings
1212
CROWD_DB_READ_HOST="db"
1313
CROWD_DB_WRITE_HOST="db"
14+
INSIGHTS_DB_WRITE_HOST="db"
1415

1516
# Product DB settings
1617
PRODUCT_DB_HOST=product

backend/.env.dist.local

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ CROWD_DB_USERNAME=postgres
3838
CROWD_DB_PASSWORD=example
3939
CROWD_DB_DATABASE=crowd-web
4040

41+
INSIGHTS_DB_WRITE_HOST=localhost
42+
INSIGHTS_DB_USERNAME=postgres
43+
INSIGHTS_DB_PASSWORD=example
44+
INSIGHTS_DB_DATABASE=insights
45+
INSIGHTS_DB_PORT=5432
46+
INSIGHTS_DB_POOL_MAX=10
47+
INSIGHTS_DB_SSLMODE=disable
48+
4149
# Product DB settings
4250
PRODUCT_DB_HOST=localhost
4351
PRODUCT_DB_PORT=5433

backend/src/api/public/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { AUTH0_CONFIG } from '../../conf'
44

55
import { errorHandler } from './middlewares/errorHandler'
66
import { oauth2Middleware } from './middlewares/oauth2Middleware'
7+
import { staticApiKeyMiddleware } from './middlewares/staticApiKeyMiddleware'
78
import { v1Router } from './v1'
9+
import { devStatsRouter } from './v1/dev-stats'
810

911
export function publicRouter(): Router {
1012
const router = Router()
1113

14+
router.use('/v1/dev-stats', staticApiKeyMiddleware(), devStatsRouter())
1215
router.use('/v1', oauth2Middleware(AUTH0_CONFIG), v1Router())
1316
router.use(errorHandler)
1417

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import crypto from 'crypto'
2+
import type { NextFunction, Request, RequestHandler, Response } from 'express'
3+
4+
import { UnauthorizedError } from '@crowd/common'
5+
import { findApiKeyByHash, optionsQx, touchApiKeyLastUsed } from '@crowd/data-access-layer'
6+
7+
export function staticApiKeyMiddleware(): RequestHandler {
8+
return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {
9+
try {
10+
const authHeader = req.headers.authorization
11+
12+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
13+
next(new UnauthorizedError('Missing or invalid Authorization header'))
14+
return
15+
}
16+
17+
const providedKey = authHeader.slice('Bearer '.length)
18+
const keyHash = crypto.createHash('sha256').update(providedKey).digest('hex')
19+
20+
const qx = optionsQx(req)
21+
const apiKey = await findApiKeyByHash(qx, keyHash)
22+
23+
if (!apiKey) {
24+
next(new UnauthorizedError('Invalid API key'))
25+
return
26+
}
27+
28+
if (apiKey.revokedAt) {
29+
next(new UnauthorizedError('API key has been revoked'))
30+
return
31+
}
32+
33+
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
34+
next(new UnauthorizedError('API key has expired'))
35+
return
36+
}
37+
38+
// fire and forget — don't block the request
39+
touchApiKeyLastUsed(qx, apiKey.id).catch(() => {})
40+
41+
req.actor = { id: apiKey.name, type: 'service', scopes: apiKey.scopes }
42+
43+
next()
44+
} catch (err) {
45+
next(err)
46+
}
47+
}
48+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Router } from 'express'
2+
3+
import { createRateLimiter } from '@/api/apiRateLimiter'
4+
import { requireScopes } from '@/api/public/middlewares/requireScopes'
5+
import { SCOPES } from '@/security/scopes'
6+
7+
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
8+
9+
export function devStatsRouter(): Router {
10+
const router = Router()
11+
12+
router.use(rateLimiter)
13+
14+
router.post('/affiliations', requireScopes([SCOPES.READ_AFFILIATIONS]), (_req, res) => {
15+
res.json({ status: 'ok' })
16+
})
17+
18+
return router
19+
}

backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export async function deleteMemberWorkExperience(req: Request, res: Response): P
4646

4747
await qx.tx(async (tx) => {
4848
await deleteMemberOrganizations(tx, memberId, [workExperienceId])
49-
5049
const commonMemberService = new CommonMemberService(tx, req.temporal, req.log)
5150
await commonMemberService.startAffiliationRecalculation(memberId, [
5251
memberOrg.organizationId,

backend/src/database/migrations/U1773938832__add-api-keys-tale.sql

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CREATE TABLE "apiKeys" (
2+
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3+
"name" TEXT NOT NULL,
4+
"keyHash" TEXT NOT NULL UNIQUE,
5+
"keyPrefix" TEXT NOT NULL,
6+
"scopes" TEXT[] NOT NULL DEFAULT '{}',
7+
"expiresAt" TIMESTAMPTZ,
8+
"lastUsedAt" TIMESTAMPTZ,
9+
"createdById" TEXT,
10+
"revokedAt" TIMESTAMPTZ,
11+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
12+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
13+
);

backend/src/security/scopes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const SCOPES = {
99
WRITE_WORK_EXPERIENCES: 'write:work-experiences',
1010
READ_PROJECT_AFFILIATIONS: 'read:project-affiliations',
1111
WRITE_PROJECT_AFFILIATIONS: 'write:project-affiliations',
12+
READ_AFFILIATIONS: 'read:affiliations',
1213
} as const
1314

1415
export type Scope = (typeof SCOPES)[keyof typeof SCOPES]

scripts/services/docker/Dockerfile.git_integration

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ COPY ./services/apps/git_integration/src/crowdgit/services/software_value/ ./
2222
# Build the binary
2323
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -o software-value ./
2424

25+
# Go builder stage 2: build the vulnerability-scanner binary
26+
FROM golang:1.25-alpine AS go-vuln-builder
27+
28+
WORKDIR /go/src/vulnerability-scanner
29+
30+
# Copy module files first for dependency caching
31+
COPY ./services/apps/git_integration/src/crowdgit/services/vulnerability_scanner/go.mod ./
32+
COPY ./services/apps/git_integration/src/crowdgit/services/vulnerability_scanner/go.sum ./
33+
34+
# Copy source code
35+
COPY ./services/apps/git_integration/src/crowdgit/services/vulnerability_scanner/ ./
36+
37+
# Download dependencies and build, using cache mounts to avoid re-downloading on every build
38+
RUN --mount=type=cache,target=/go/pkg/mod \
39+
--mount=type=cache,target=/root/.cache/go-build \
40+
go mod download && CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -o vulnerability-scanner ./
41+
2542
# Builder stage: install build dependencies, uv, and dependencies
2643
FROM base AS builder
2744

@@ -84,13 +101,17 @@ COPY --from=builder /usr/crowd/app /usr/crowd/app
84101
COPY --from=go-builder /go/src/software-value/software-value /usr/local/bin/software-value
85102
COPY --from=go-builder /go/bin/scc /usr/local/bin/scc
86103

104+
# Copy vulnerability-scanner binary from go-vuln-builder stage
105+
COPY --from=go-vuln-builder /go/src/vulnerability-scanner/vulnerability-scanner /usr/local/bin/vulnerability-scanner
106+
87107
# Add virtual environment bin to PATH
88108
ENV PATH="/usr/crowd/app/.venv/bin:$PATH"
89109

90110
# Make runner script and binaries executable
91111
RUN chmod +x ./src/runner.sh \
92112
&& chmod +x /usr/local/bin/software-value \
93-
&& chmod +x /usr/local/bin/scc
113+
&& chmod +x /usr/local/bin/scc \
114+
&& chmod +x /usr/local/bin/vulnerability-scanner
94115

95116
EXPOSE 8085
96117

0 commit comments

Comments
 (0)