mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 04:18:52 +00:00
93e1dc598c
Full audit of all ~100 backend API endpoints against frontend client functions and TypeScript interfaces. Fixes field name mismatches, missing client functions, phantom interface fields, type coercion for Go bool/int config fields, and issuer type ID alignment with backend domain constants. Backend: - issuer.go/target.go: GUI-created entities default enabled=true (Go bool zero value was overriding DB DEFAULT) Frontend types (types.ts): - Certificate: fingerprint→fingerprint_sha256, phantom fields made optional - CertificateVersion: fingerprint→fingerprint_sha256, chain_pem→pem_chain, removed phantom version/cert_pem fields - Job: error_message→last_error (matches Go json tag) Frontend client (client.ts): - Added getNotification(id) and getAuditEvent(id) for existing backend routes Frontend pages: - CertificateDetailPage: derives serial/fingerprint/issuedAt from latest CertificateVersion instead of empty Certificate fields - JobsPage/JobDetailPage: error_message→last_error - TargetsPage: reload_cmd→reload_command, validate_cmd→validate_command, added missing config fields per backend structs (validate_command for NGINX/Apache, hostname/winrm_timeout for IIS, private_key/passphrase/ cert_mode/key_mode for SSH, winrm_https/winrm_insecure for WinCertStore, create_keystore for JavaKeystore, mode for Dovecot), type coercion via buildConfigPayload() with BOOL_FIELDS/INT_FIELDS sets, IIS WinRM nesting - TargetDetailPage: added passphrase to sensitiveKeys redaction - issuerTypes.ts: type IDs aligned to backend constants (acme→ACME, local→GenericCA, stepca→StepCA, openssl→OpenSSL), backward compat aliases preserved, step-ca config fields updated to match backend struct Utilities (utils.ts): - formatDate/formatDateTime accept string|undefined|null Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
435 lines
17 KiB
TypeScript
435 lines
17 KiB
TypeScript
import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, PolicyViolation, Issuer, Target, CertificateProfile, Owner, Team, AgentGroup, PaginatedResponse, DashboardSummary, CertificateStatusCount, ExpirationBucket, JobTrendDataPoint, IssuanceRateDataPoint, MetricsResponse, DiscoveredCertificate, DiscoveryScan, DiscoverySummary, NetworkScanTarget } from './types';
|
|
|
|
const BASE = '/api/v1';
|
|
|
|
// API key stored in memory (not localStorage for security)
|
|
let apiKey: string | null = null;
|
|
|
|
export function setApiKey(key: string | null) {
|
|
apiKey = key;
|
|
}
|
|
|
|
export function getApiKey(): string | null {
|
|
return apiKey;
|
|
}
|
|
|
|
function authHeaders(): Record<string, string> {
|
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
if (apiKey) {
|
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(url, {
|
|
headers: { ...authHeaders(), ...init?.headers },
|
|
...init,
|
|
});
|
|
if (res.status === 401) {
|
|
// Trigger re-auth
|
|
const event = new CustomEvent('certctl:auth-required');
|
|
window.dispatchEvent(event);
|
|
throw new Error('Authentication required');
|
|
}
|
|
if (!res.ok) {
|
|
let errorMsg = res.statusText;
|
|
try {
|
|
const body = await res.json();
|
|
errorMsg = body.message || body.error || errorMsg;
|
|
} catch {
|
|
// Response body is not JSON, use status text
|
|
}
|
|
throw new Error(errorMsg || `HTTP ${res.status}`);
|
|
}
|
|
if (res.status === 204) return {} as T;
|
|
return res.json();
|
|
}
|
|
|
|
// Auth
|
|
export const getAuthInfo = () =>
|
|
fetch(`${BASE}/auth/info`, { headers: { 'Content-Type': 'application/json' } })
|
|
.then(r => r.json() as Promise<{ auth_type: string; required: boolean }>);
|
|
|
|
export const checkAuth = (key: string) =>
|
|
fetch(`${BASE}/auth/check`, {
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` },
|
|
}).then(r => {
|
|
if (!r.ok) throw new Error('Invalid API key');
|
|
return r.json() as Promise<{ status: string }>;
|
|
});
|
|
|
|
// Certificates
|
|
export const getCertificates = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Certificate>>(`${BASE}/certificates?${qs}`);
|
|
};
|
|
|
|
export const getCertificate = (id: string) =>
|
|
fetchJSON<Certificate>(`${BASE}/certificates/${id}`);
|
|
|
|
export const getCertificateVersions = (id: string) =>
|
|
fetchJSON<PaginatedResponse<CertificateVersion>>(`${BASE}/certificates/${id}/versions`);
|
|
|
|
export const createCertificate = (data: Partial<Certificate>) =>
|
|
fetchJSON<Certificate>(`${BASE}/certificates`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const triggerRenewal = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/certificates/${id}/renew`, { method: 'POST' });
|
|
|
|
export const updateCertificate = (id: string, data: Partial<Certificate>) =>
|
|
fetchJSON<Certificate>(`${BASE}/certificates/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const archiveCertificate = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/certificates/${id}`, { method: 'DELETE' });
|
|
|
|
export const triggerDeployment = (id: string, targetId: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/certificates/${id}/deploy`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ target_id: targetId }),
|
|
});
|
|
|
|
export const revokeCertificate = (id: string, reason: string) =>
|
|
fetchJSON<{ status: string }>(`${BASE}/certificates/${id}/revoke`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ reason }),
|
|
});
|
|
|
|
// Certificate Export
|
|
export const exportCertificatePEM = (id: string) =>
|
|
fetchJSON<{ cert_pem: string; chain_pem: string; full_pem: string }>(`${BASE}/certificates/${id}/export/pem`);
|
|
|
|
export const downloadCertificatePEM = (id: string) => {
|
|
const headers: Record<string, string> = {};
|
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
return fetch(`${BASE}/certificates/${id}/export/pem?download=true`, { headers })
|
|
.then(r => {
|
|
if (!r.ok) throw new Error('Export failed');
|
|
return r.blob();
|
|
});
|
|
};
|
|
|
|
export const exportCertificatePKCS12 = (id: string, password: string = '') => {
|
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
return fetch(`${BASE}/certificates/${id}/export/pkcs12`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ password }),
|
|
}).then(r => {
|
|
if (!r.ok) throw new Error('Export failed');
|
|
return r.blob();
|
|
});
|
|
};
|
|
|
|
// Certificate Deployments
|
|
export const getCertificateDeployments = (id: string, params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Job>>(`${BASE}/certificates/${id}/deployments?${qs}`);
|
|
};
|
|
|
|
// CRL / OCSP
|
|
export const getCRL = () =>
|
|
fetchJSON<{ version: number; entries: unknown[]; total: number; generated_at: string }>(`${BASE}/crl`);
|
|
|
|
export const getOCSPStatus = (issuerId: string, serial: string) => {
|
|
const headers: Record<string, string> = {};
|
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
return fetch(`${BASE}/ocsp/${issuerId}/${serial}`, { headers })
|
|
.then(r => {
|
|
if (!r.ok) throw new Error(`OCSP request failed: ${r.status}`);
|
|
return r.arrayBuffer();
|
|
});
|
|
};
|
|
|
|
// Agents
|
|
export const getAgents = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Agent>>(`${BASE}/agents?${qs}`);
|
|
};
|
|
|
|
export const getAgent = (id: string) =>
|
|
fetchJSON<Agent>(`${BASE}/agents/${id}`);
|
|
|
|
export const registerAgent = (data: Partial<Agent>) =>
|
|
fetchJSON<Agent>(`${BASE}/agents`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
// Jobs
|
|
export const getJobs = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Job>>(`${BASE}/jobs?${qs}`);
|
|
};
|
|
|
|
export const cancelJob = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/jobs/${id}/cancel`, { method: 'POST' });
|
|
|
|
// Notifications
|
|
export const getNotifications = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Notification>>(`${BASE}/notifications?${qs}`);
|
|
};
|
|
|
|
export const getNotification = (id: string) =>
|
|
fetchJSON<Notification>(`${BASE}/notifications/${id}`);
|
|
|
|
export const markNotificationRead = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/notifications/${id}/read`, { method: 'POST' });
|
|
|
|
// Audit
|
|
export const getAuditEvents = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<AuditEvent>>(`${BASE}/audit?${qs}`);
|
|
};
|
|
|
|
export const getAuditEvent = (id: string) =>
|
|
fetchJSON<AuditEvent>(`${BASE}/audit/${id}`);
|
|
|
|
// Policies
|
|
export const getPolicies = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<PolicyRule>>(`${BASE}/policies?${qs}`);
|
|
};
|
|
|
|
export const createPolicy = (data: Partial<PolicyRule>) =>
|
|
fetchJSON<PolicyRule>(`${BASE}/policies`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updatePolicy = (id: string, data: Partial<PolicyRule>) =>
|
|
fetchJSON<PolicyRule>(`${BASE}/policies/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const getPolicy = (id: string) =>
|
|
fetchJSON<PolicyRule>(`${BASE}/policies/${id}`);
|
|
|
|
export const deletePolicy = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/policies/${id}`, { method: 'DELETE' });
|
|
|
|
export const getPolicyViolations = (id: string) =>
|
|
fetchJSON<PaginatedResponse<PolicyViolation>>(`${BASE}/policies/${id}/violations`);
|
|
|
|
// Issuers
|
|
export const getIssuers = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Issuer>>(`${BASE}/issuers?${qs}`);
|
|
};
|
|
|
|
export const createIssuer = (data: Partial<Issuer>) =>
|
|
fetchJSON<Issuer>(`${BASE}/issuers`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const testIssuerConnection = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/issuers/${id}/test`, { method: 'POST' });
|
|
|
|
export const updateIssuer = (id: string, data: Partial<Issuer>) =>
|
|
fetchJSON<Issuer>(`${BASE}/issuers/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteIssuer = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/issuers/${id}`, { method: 'DELETE' });
|
|
|
|
// Targets
|
|
export const getTargets = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Target>>(`${BASE}/targets?${qs}`);
|
|
};
|
|
|
|
export const createTarget = (data: Partial<Target>) =>
|
|
fetchJSON<Target>(`${BASE}/targets`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateTarget = (id: string, data: Partial<Target>) =>
|
|
fetchJSON<Target>(`${BASE}/targets/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteTarget = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/targets/${id}`, { method: 'DELETE' });
|
|
|
|
export const testTargetConnection = (id: string) =>
|
|
fetchJSON<{ status: string; message: string }>(`${BASE}/targets/${id}/test`, { method: 'POST' });
|
|
|
|
// Profiles
|
|
export const getProfiles = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<CertificateProfile>>(`${BASE}/profiles?${qs}`);
|
|
};
|
|
|
|
export const getProfile = (id: string) =>
|
|
fetchJSON<CertificateProfile>(`${BASE}/profiles/${id}`);
|
|
|
|
export const createProfile = (data: Partial<CertificateProfile>) =>
|
|
fetchJSON<CertificateProfile>(`${BASE}/profiles`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateProfile = (id: string, data: Partial<CertificateProfile>) =>
|
|
fetchJSON<CertificateProfile>(`${BASE}/profiles/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteProfile = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/profiles/${id}`, { method: 'DELETE' });
|
|
|
|
// Owners
|
|
export const getOwners = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Owner>>(`${BASE}/owners?${qs}`);
|
|
};
|
|
|
|
export const getOwner = (id: string) =>
|
|
fetchJSON<Owner>(`${BASE}/owners/${id}`);
|
|
|
|
export const createOwner = (data: Partial<Owner>) =>
|
|
fetchJSON<Owner>(`${BASE}/owners`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateOwner = (id: string, data: Partial<Owner>) =>
|
|
fetchJSON<Owner>(`${BASE}/owners/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteOwner = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/owners/${id}`, { method: 'DELETE' });
|
|
|
|
// Teams
|
|
export const getTeams = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<Team>>(`${BASE}/teams?${qs}`);
|
|
};
|
|
|
|
export const getTeam = (id: string) =>
|
|
fetchJSON<Team>(`${BASE}/teams/${id}`);
|
|
|
|
export const createTeam = (data: Partial<Team>) =>
|
|
fetchJSON<Team>(`${BASE}/teams`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateTeam = (id: string, data: Partial<Team>) =>
|
|
fetchJSON<Team>(`${BASE}/teams/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteTeam = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/teams/${id}`, { method: 'DELETE' });
|
|
|
|
// Agent Groups
|
|
export const getAgentGroups = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<AgentGroup>>(`${BASE}/agent-groups?${qs}`);
|
|
};
|
|
|
|
export const getAgentGroup = (id: string) =>
|
|
fetchJSON<AgentGroup>(`${BASE}/agent-groups/${id}`);
|
|
|
|
export const createAgentGroup = (data: Partial<AgentGroup>) =>
|
|
fetchJSON<AgentGroup>(`${BASE}/agent-groups`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateAgentGroup = (id: string, data: Partial<AgentGroup>) =>
|
|
fetchJSON<AgentGroup>(`${BASE}/agent-groups/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteAgentGroup = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/agent-groups/${id}`, { method: 'DELETE' });
|
|
|
|
export const getAgentGroupMembers = (id: string) =>
|
|
fetchJSON<PaginatedResponse<Agent>>(`${BASE}/agent-groups/${id}/members`);
|
|
|
|
// Renewal Approvals
|
|
export const approveRenewal = (jobId: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/jobs/${jobId}/approve`, { method: 'POST' });
|
|
|
|
export const rejectRenewal = (jobId: string, reason: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/jobs/${jobId}/reject`, { method: 'POST', body: JSON.stringify({ reason }) });
|
|
|
|
// Discovery
|
|
export const getDiscoveredCertificates = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<DiscoveredCertificate>>(`${BASE}/discovered-certificates?${qs}`);
|
|
};
|
|
|
|
export const getDiscoveredCertificate = (id: string) =>
|
|
fetchJSON<DiscoveredCertificate>(`${BASE}/discovered-certificates/${id}`);
|
|
|
|
export const claimDiscoveredCertificate = (id: string, managedCertificateId: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/discovered-certificates/${id}/claim`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ managed_certificate_id: managedCertificateId }),
|
|
});
|
|
|
|
export const dismissDiscoveredCertificate = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/discovered-certificates/${id}/dismiss`, { method: 'POST' });
|
|
|
|
export const getDiscoveryScans = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<DiscoveryScan>>(`${BASE}/discovery-scans?${qs}`);
|
|
};
|
|
|
|
export const getDiscoverySummary = () =>
|
|
fetchJSON<DiscoverySummary>(`${BASE}/discovery-summary`);
|
|
|
|
// Network Scan Targets
|
|
export const getNetworkScanTargets = (params: Record<string, string> = {}) => {
|
|
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
|
return fetchJSON<PaginatedResponse<NetworkScanTarget>>(`${BASE}/network-scan-targets?${qs}`);
|
|
};
|
|
|
|
export const getNetworkScanTarget = (id: string) =>
|
|
fetchJSON<NetworkScanTarget>(`${BASE}/network-scan-targets/${id}`);
|
|
|
|
export const createNetworkScanTarget = (data: Partial<NetworkScanTarget>) =>
|
|
fetchJSON<NetworkScanTarget>(`${BASE}/network-scan-targets`, { method: 'POST', body: JSON.stringify(data) });
|
|
|
|
export const updateNetworkScanTarget = (id: string, data: Partial<NetworkScanTarget>) =>
|
|
fetchJSON<NetworkScanTarget>(`${BASE}/network-scan-targets/${id}`, { method: 'PUT', body: JSON.stringify(data) });
|
|
|
|
export const deleteNetworkScanTarget = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/network-scan-targets/${id}`, { method: 'DELETE' });
|
|
|
|
export const triggerNetworkScan = (id: string) =>
|
|
fetchJSON<{ message: string }>(`${BASE}/network-scan-targets/${id}/scan`, { method: 'POST' });
|
|
|
|
// Stats
|
|
export const getDashboardSummary = () =>
|
|
fetchJSON<DashboardSummary>(`${BASE}/stats/summary`);
|
|
|
|
export const getCertificatesByStatus = () =>
|
|
fetchJSON<CertificateStatusCount[]>(`${BASE}/stats/certificates-by-status`);
|
|
|
|
export const getExpirationTimeline = (days = 30) =>
|
|
fetchJSON<ExpirationBucket[]>(`${BASE}/stats/expiration-timeline?days=${days}`);
|
|
|
|
export const getJobTrends = (days = 30) =>
|
|
fetchJSON<JobTrendDataPoint[]>(`${BASE}/stats/job-trends?days=${days}`);
|
|
|
|
export const getIssuanceRate = (days = 30) =>
|
|
fetchJSON<IssuanceRateDataPoint[]>(`${BASE}/stats/issuance-rate?days=${days}`);
|
|
|
|
export const getMetrics = () =>
|
|
fetchJSON<MetricsResponse>(`${BASE}/metrics`);
|
|
|
|
// Digest
|
|
export const previewDigest = () => {
|
|
const headers: Record<string, string> = {};
|
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
return fetch(`${BASE}/digest/preview`, { headers })
|
|
.then(r => {
|
|
if (!r.ok) throw new Error(`Digest preview failed: ${r.status}`);
|
|
return r.text();
|
|
});
|
|
};
|
|
|
|
export const sendDigest = () =>
|
|
fetchJSON<{ message: string }>(`${BASE}/digest/send`, { method: 'POST' });
|
|
|
|
// Jobs (single)
|
|
export const getJob = (id: string) =>
|
|
fetchJSON<Job>(`${BASE}/jobs/${id}`);
|
|
|
|
// Job Verification
|
|
export const getJobVerification = (id: string) =>
|
|
fetchJSON<{ job_id: string; target_id: string; verified: boolean; actual_fingerprint: string; expected_fingerprint: string; verified_at: string; error?: string }>(`${BASE}/jobs/${id}/verification`);
|
|
|
|
// Issuers (single)
|
|
export const getIssuer = (id: string) =>
|
|
fetchJSON<Issuer>(`${BASE}/issuers/${id}`);
|
|
|
|
// Targets (single)
|
|
export const getTarget = (id: string) =>
|
|
fetchJSON<Target>(`${BASE}/targets/${id}`);
|
|
|
|
// Prometheus metrics (text format)
|
|
export const getPrometheusMetrics = () => {
|
|
const headers: Record<string, string> = {};
|
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
return fetch(`${BASE}/metrics/prometheus`, { headers })
|
|
.then(r => {
|
|
if (!r.ok) throw new Error(`Prometheus metrics failed: ${r.status}`);
|
|
return r.text();
|
|
});
|
|
};
|
|
|
|
// Health
|
|
export const getHealth = () => fetchJSON<{ status: string }>('/health');
|