mirror of
https://github.com/temetro/temetro.git
synced 2026-08-18 22:26:19 +00:00
0d2494d67a
Expose temetro's own records as a read-only FHIR R4 server at /fhir, authenticated with per-clinic API keys (tmf_… bearer tokens, SHA-256 hashed, shown once). Serves Patient, Observation (labs + vitals), AllergyIntolerance, Condition, MedicationRequest, Encounter and Appointment as text-only CodeableConcepts (temetro stores free-text clinical values); CapabilityStatement at /fhir/metadata (unauth). Searchset Bundles with _count/_offset pagination and self/next/prev links; every request is org-scoped and written to the activity log. Keys are created/revoked under Settings → Integrations (owner/admin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.5 KiB
TypeScript
50 lines
1.5 KiB
TypeScript
import type { NextFunction, Request, Response } from "express";
|
|
|
|
import { resolveKey } from "../services/fhir-server/keys.js";
|
|
import {
|
|
FHIR_CONTENT_TYPE,
|
|
operationOutcome,
|
|
} from "../services/fhir-server/outcome.js";
|
|
|
|
// Bearer-token auth for the read-only FHIR server. Unlike the rest of the API
|
|
// (Better Auth session cookies), the `/fhir` endpoints authenticate with a
|
|
// per-clinic API key: `Authorization: Bearer tmf_<secret>`. On success the
|
|
// caller's organization is attached to `req.organizationId` and every downstream
|
|
// query is scoped to it. Failures return a FHIR OperationOutcome, not our
|
|
// standard error JSON.
|
|
export async function requireFhirKey(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
): Promise<void> {
|
|
const header = req.headers.authorization ?? "";
|
|
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
const secret = match?.[1]?.trim();
|
|
|
|
const unauthorized = (diagnostics: string) => {
|
|
res
|
|
.status(401)
|
|
.type(FHIR_CONTENT_TYPE)
|
|
.set("WWW-Authenticate", "Bearer")
|
|
.json(operationOutcome("error", "login", diagnostics));
|
|
};
|
|
|
|
if (!secret) {
|
|
unauthorized("Missing bearer token. Send Authorization: Bearer tmf_…");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resolved = await resolveKey(secret);
|
|
if (!resolved) {
|
|
unauthorized("Invalid or revoked API key.");
|
|
return;
|
|
}
|
|
req.organizationId = resolved.orgId;
|
|
req.fhirKey = { id: resolved.keyId, name: resolved.keyName };
|
|
next();
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|