Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | 2x 2x 2x 11x 13x 13x 13x 13x 10x 10x 10x 5x 4x 4x 4x 3x 4x 4x 3x 3x 10x 10x 10x 10x 10x 10x 10x 10x 10x 13x 9x 1x 3x 4x 4x 4x 2x 2x 2x 2x 9x 9x 9x 9x 9x 9x 3x 6x 5x 1x 4x 4x 4x 1x 3x 3x 9x 3x 3x 3x 1x 1x | import { NextRequest, NextResponse } from "next/server";
import { edcClient } from "@/lib/edc";
import { requireAuth, isAuthError } from "@/lib/auth-guard";
import { promises as fs } from "fs";
import path from "path";
export const dynamic = "force-dynamic";
/**
* Approved fictional participants — display names by DID slug.
* Slug = last path segment of the participant's DID
* (e.g. "did:web:identityhub%3A7083:alpha-klinik" → "alpha-klinik").
* Used as fallback when the CFM Tenant Manager is unavailable.
*/
const SLUG_DISPLAY_NAMES: Record<
string,
{ displayName: string; org: string; role: string }
> = {
"alpha-klinik": {
displayName: "AlphaKlinik Berlin",
org: "AlphaKlinik Berlin",
role: "DATA_HOLDER",
},
lmc: {
displayName: "Limburg Medical Centre",
org: "Limburg Medical Centre",
role: "DATA_HOLDER",
},
pharmaco: {
displayName: "PharmaCo Research AG",
org: "PharmaCo Research AG",
role: "DATA_USER",
},
medreg: { displayName: "MedReg DE", org: "MedReg DE", role: "HDAB" },
irs: {
displayName: "Institut de Recherche Santé",
org: "Institut de Recherche Santé",
role: "HDAB",
},
};
/** Extract DID slug from a participantId DID string. */
function didSlug(did: string): string {
return decodeURIComponent(did).split(":").pop()?.toLowerCase() ?? "";
}
interface EdcParticipant {
"@id": string;
participantId?: string;
identity?: string;
[key: string]: unknown;
}
interface CfmTenant {
id: string;
properties?: {
displayName?: string;
organization?: string;
role?: string;
participantDid?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
/**
* GET /api/participants — List all participant contexts (EDC-V Management API)
* enriched with human-readable display names from CFM Tenant Manager.
*/
export async function GET() {
const auth = await requireAuth();
Iif (isAuthError(auth)) return auth;
try {
const participants = await edcClient.management<EdcParticipant[]>(
"/v5alpha/participants",
);
// Attempt to fetch CFM tenants for display names — non-blocking
const tenantMap: Record<string, string> = {};
try {
const tenants = await edcClient.tenant<CfmTenant[]>("/v1alpha1/tenants");
if (Array.isArray(tenants)) {
for (const t of tenants) {
const dn = t.properties?.displayName;
if (!dn) continue;
// Match by DID slug stored in properties
const did = t.properties?.participantDid ?? "";
const slug = did ? didSlug(did) : "";
if (slug) tenantMap[slug] = dn;
// Also index by lowercase displayName slug for looser matching
const dnSlug = dn
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
tenantMap[dnSlug] = dn;
}
}
} catch {
// CFM may be down — fall back to static map below
}
// Enrich each participant with a displayName — skip stale CREATED contexts
const enriched = (Array.isArray(participants) ? participants : [])
.filter((p) => {
// Only show ACTIVATED participants in the UI
const state = (p as Record<string, unknown>).state as
| string
| undefined;
return state === "ACTIVATED";
})
.map((p) => {
const did = p.participantId ?? p.identity ?? "";
const slug = did ? didSlug(did) : "";
const staticEntry = SLUG_DISPLAY_NAMES[slug];
const displayName =
tenantMap[slug] ||
staticEntry?.displayName ||
slug ||
p["@id"].slice(0, 12);
const role = staticEntry?.role ?? "";
return {
...p,
participantId: p["@id"],
displayName,
role,
slug,
identity: did,
};
});
if (enriched.length > 0) {
return NextResponse.json(enriched);
}
// EDC-V returned no ACTIVATED participants — fall through to mock data
console.warn("EDC-V has no ACTIVATED participants — serving mock data");
} catch (err) {
console.error("Failed to list participants:", err);
}
// Fall back to bundled mock data so the UI works offline / pre-seed
try {
const mockPath = path.join(
process.cwd(),
"public",
"mock",
"participants.json",
);
const raw = await fs.readFile(mockPath, "utf-8");
const mock = JSON.parse(raw);
console.warn("Serving mock participants");
return NextResponse.json(mock);
} catch {
// Mock file not available either
}
return NextResponse.json(
{ error: "Failed to list participants" },
{ status: 502 },
);
}
/**
* POST /api/participants — Create a new tenant + participant context.
*
* Body: { displayName, organization, role, ehdsParticipantType }
*
* Steps:
* 1. Create tenant in CFM TenantManager
* 2. Create participant profile (triggers provisioning via CFM agents)
*/
export async function POST(req: NextRequest) {
const auth = await requireAuth();
Iif (isAuthError(auth)) return auth;
try {
const body = await req.json();
const { displayName, organization, role, ehdsParticipantType } = body;
if (!displayName || !role) {
return NextResponse.json(
{ error: "displayName and role are required" },
{ status: 400 },
);
}
// 1. Get the cell ID (there is typically one cell in dev)
const cells = await edcClient.tenant<{ id: string }[]>("/v1alpha1/cells");
if (!cells || cells.length === 0) {
return NextResponse.json(
{ error: "No cells found in TenantManager" },
{ status: 503 },
);
}
const cellId = cells[0].id;
// 2. Get the dataspace profile ID
const profiles = await edcClient.tenant<{ id: string }[]>(
"/v1alpha1/dataspace-profiles",
);
if (!profiles || profiles.length === 0) {
return NextResponse.json(
{ error: "No dataspace profiles found" },
{ status: 503 },
);
}
const profileId = profiles[0].id;
// 3. Create tenant
const tenantPayload = {
properties: {
displayName,
organization: organization || displayName,
role,
ehdsParticipantType: ehdsParticipantType || role,
},
};
const tenant = await edcClient.tenant<{ id: string }>(
"/v1alpha1/tenants",
"POST",
tenantPayload,
);
// 4. Create participant profile for this tenant (triggers DID + key provisioning)
const participantPayload = {
cellId,
dataspaceProfileId: profileId,
};
const participant = await edcClient.tenant<{ id: string }>(
`/v1alpha1/tenants/${tenant.id}/participant-profiles`,
"POST",
participantPayload,
);
return NextResponse.json(
{
tenantId: tenant.id,
participantId: participant?.id,
displayName,
role,
status: "provisioning",
},
{ status: 201 },
);
} catch (err) {
console.error("Failed to create participant:", err);
return NextResponse.json(
{ error: "Failed to create participant" },
{ status: 502 },
);
}
}
|