Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/api.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 39df94f92b2a33f852481fe1954dabed841524bb |
| Branch | w0-aedt-compliance |
| Path or archive member | frontend/src/lib/api.ts |
| Original SHA-256 | 1a6fad54cd2d5b4136a5f69c94783364cea48c5ad8795a3e231fd20dd29e34ca |
| Git blob SHA | 79cd2c93403a754457287b1297f7b3a07ab4c65c |
| Reading-copy SHA-256 | 1a6fad54cd2d5b4136a5f69c94783364cea48c5ad8795a3e231fd20dd29e34ca |
Recovered source transcript
Download complete source text ↓154 lines · 6.2 KB
import type {
AdverseImpactReport,
CandidateIntelligence,
CandidateListView,
CommandCenterView,
ExternalCandidate,
FederalAward,
OpportunityFilters,
OpportunityFitView,
RankedCandidateView,
RequirementListView,
SamOpportunity,
ScreenView,
SlateResult,
WageBaseline,
} from './types'
// opportunityQuery turns the filter object into a query string. Multi valued filters are repeated
// params (?naics=a&naics=b); the backend also accepts comma lists.
function opportunityQuery(f: OpportunityFilters): string {
const p = new URLSearchParams()
f.naics?.forEach((v) => p.append('naics', v))
f.set_aside?.forEach((v) => p.append('set_aside', v))
f.state?.forEach((v) => p.append('state', v))
f.department?.forEach((v) => p.append('department', v))
f.sub_agency?.forEach((v) => p.append('sub_agency', v))
if (f.q) p.set('q', f.q)
if (f.active_only === false) p.set('active_only', 'false')
if (f.limit) p.set('limit', String(f.limit))
const s = p.toString()
return s ? `?${s}` : ''
}
// Dev tenant. In production the org comes from a validated session; here we send the dev header the
// backend's dev resolver expects (see cmd/api devResolveOrg).
const ORG_ID =
(import.meta.env.VITE_ORG_ID as string | undefined) ?? '11111111-1111-1111-1111-111111111111'
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
// csrfToken is the signed double submit token issued by GET /auth/csrf. The companion __Host-mm_csrf
// cookie is set by that response; the session cookie rides along via credentials include. A full login
// page is a separate frontend auth pass; this is the transport wiring that makes authenticated and
// unsafe verb calls succeed against the session backend.
let csrfToken: string | null = null
async function ensureCsrf(): Promise<string> {
if (csrfToken) return csrfToken
const res = await fetch('/api/auth/csrf', {
credentials: 'include',
headers: { 'X-Org-ID': ORG_ID },
})
if (res.ok) {
const body = (await res.json()) as { csrf_token?: string }
csrfToken = body.csrf_token ?? null
}
return csrfToken ?? ''
}
async function doFetch(path: string, init: RequestInit, method: string): Promise<Response> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Org-ID': ORG_ID,
...((init.headers as Record<string, string>) ?? {}),
}
if (method !== 'GET' && method !== 'HEAD') {
headers['X-CSRF-Token'] = await ensureCsrf()
}
return fetch(`/api${path}`, { ...init, credentials: 'include', headers })
}
async function rawRequest(path: string, init?: RequestInit): Promise<Response> {
const method = (init?.method ?? 'GET').toUpperCase()
let res = await doFetch(path, init ?? {}, method)
// A stale or rotated CSRF token returns 403; refresh once and retry the unsafe verb.
if (res.status === 403 && method !== 'GET' && method !== 'HEAD') {
csrfToken = null
res = await doFetch(path, init ?? {}, method)
}
if (!res.ok) {
let detail = res.statusText
try {
const body = (await res.clone().json()) as { error?: string }
if (body.error) detail = body.error
} catch {
/* ignore non-JSON error bodies */
}
throw new ApiError(res.status, detail)
}
return res
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await rawRequest(path, init)
return (await res.json()) as T
}
async function requestText(path: string): Promise<string> {
const res = await rawRequest(path)
return res.text()
}
export const api = {
candidates: () => request<CandidateListView[]>('/candidates?limit=200'),
candidateIntelligence: (limit = 250) =>
request<CandidateIntelligence[]>(`/candidates/intelligence?limit=${limit}`),
requirements: () => request<RequirementListView[]>('/requirements?limit=200'),
screen: (candidate_id: string, requirement_id: string) =>
request<ScreenView>('/screen', {
method: 'POST',
body: JSON.stringify({ candidate_id, requirement_id }),
}),
generateSlate: (requirement_id: string, name: string) =>
request<SlateResult>('/slates/generate', {
method: 'POST',
body: JSON.stringify({ requirement_id, name }),
}),
opportunities: (candidateId: string) =>
request<OpportunityFitView[]>(`/candidates/${candidateId}/opportunities?limit=10`),
rankCandidates: (requirementId: string) =>
request<RankedCandidateView[]>(`/requirements/${requirementId}/candidates`),
commandCenter: () => request<CommandCenterView>('/command-center'),
// Cached SAM.gov opportunities (the requirement feed). The frontend NEVER calls SAM live.
samOpportunities: (f: OpportunityFilters = {}) =>
request<SamOpportunity[]>(`/opportunities${opportunityQuery(f)}`),
samOpportunity: (noticeId: string) =>
request<SamOpportunity>(`/opportunities/${encodeURIComponent(noticeId)}`),
// USASpending awards (cache only): incumbents by NAICS and Galapagos own past performance.
incumbents: (naics: string) =>
request<FederalAward[]>(`/awards?naics=${encodeURIComponent(naics)}&limit=6`),
galapagosAwards: () => request<FederalAward[]>('/awards/galapagos?limit=8'),
// BLS OEWS regional wage baseline (cache only).
wages: () => request<WageBaseline[]>('/wages'),
// Externally sourced candidates (People Data Labs) plus the delete path.
externalCandidates: () => request<ExternalCandidate[]>('/sourcing/candidates'),
deleteExternalCandidate: (id: string) =>
request<{ status: string }>(`/sourcing/candidates/${encodeURIComponent(id)}`, { method: 'DELETE' }),
// AEDT: the human in the loop decision on a slate member (approved or rejected).
decideSlateMember: (slateId: string, candidateId: string, decision: 'approved' | 'rejected') =>
request<{ status: string }>(
`/slates/${encodeURIComponent(slateId)}/members/${encodeURIComponent(candidateId)}/decision`,
{ method: 'PATCH', body: JSON.stringify({ decision }) },
),
// Compliance: the adverse impact report (compliance role only) and its publication ready export.
adverseImpact: () => request<AdverseImpactReport>('/compliance/adverse-impact'),
adverseImpactExport: () => requestText('/compliance/adverse-impact/export'),
}