Files
ocdp-go/frontend/src/api/index.ts
Ivan087 0094519f52 feat: first-time setup flow — no .env required for deployment
- Add GET /auth/status endpoint (returns needsSetup when no admin exists)
- Add POST /auth/setup endpoint (public first-admin registration)
- Add IsAdminExists + SetupInitialAdmin methods to AuthService
- Frontend: detect needsSetup on load, show setup page with admin registration
- Frontend: fall back to login page when setup is already complete
- Docker compose: env_file already optional (required: false), no changes needed
- Bootstrap: auto-detect BOOTSTRAP_CLUSTERS without separate enable flag
2026-05-21 13:49:36 +08:00

416 lines
18 KiB
TypeScript

/**
* API Client entry point
* Export configured API client, generated functions, and friendly aliases.
*/
type AxiosOptions<T extends (...args: never[]) => unknown> = Parameters<T>[2];
import {
deleteClustersClusterId,
deleteClustersClusterIdInstancesInstanceId,
deleteRegistriesRegistryId,
getClusters,
getClustersClusterId,
getClustersClusterIdHealth,
getClustersClusterIdInstances,
getClustersClusterIdInstancesInstanceId,
getClustersClusterIdInstancesInstanceIdEntries,
getMonitoringClusters,
getMonitoringClustersClusterId,
getMonitoringClustersClusterIdNodes,
getMonitoringSummary,
getRegistries,
getRegistriesRegistryId,
getRegistriesRegistryIdHealth,
getRegistriesRegistryIdRepositories,
getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts,
getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference,
getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema,
postAuthLogin,
postAuthRefresh,
postAuthRegister,
postClusters,
postClustersClusterIdInstances,
postRegistries,
putClustersClusterId,
putClustersClusterIdInstancesInstanceId,
putRegistriesRegistryId,
} from './generated-orval/api';
import type {
DeleteClustersClusterIdInstancesInstanceIdPathParameters,
DeleteClustersClusterIdPathParameters,
DeleteRegistriesRegistryIdPathParameters,
GetClustersClusterIdInstancesInstanceIdPathParameters,
GetClustersClusterIdInstancesPathParameters,
GetRegistriesRegistryIdHealthPathParameters,
GetRegistriesRegistryIdPathParameters,
GetRegistriesRegistryIdRepositoriesPathParameters,
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters,
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters,
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoArtifactResponse as GeneratedArtifactResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoAuthResponse as GeneratedAuthResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterMetricsResponse as GeneratedClusterMetricsResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterResponse as GeneratedClusterResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest as GeneratedCreateClusterRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest as GeneratedCreateInstanceRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest as GeneratedCreateRegistryRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryResponse as GeneratedInstanceEntry,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse as GeneratedInstanceResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest as GeneratedLoginRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoMonitoringSummaryResponse as GeneratedMonitoringSummary,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse as GeneratedNodeMetricsResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest as GeneratedRefreshTokenRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest as GeneratedRegisterRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryHealthResponse as GeneratedRegistryHealthResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryResponse as GeneratedRegistryResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRepositoryListResponse as GeneratedRepositoryListResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoTagResponse as GeneratedTagResponse,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest as GeneratedUpdateClusterRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest as GeneratedUpdateInstanceRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest as GeneratedUpdateRegistryRequest,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUserResponse as GeneratedUserResponse,
PutClustersClusterIdInstancesInstanceIdPathParameters,
PutClustersClusterIdPathParameters,
PutRegistriesRegistryIdPathParameters,
} from './generated-orval/api.schemas';
import { AXIOS_INSTANCE, customAxiosInstance } from './axios-mutator';
import {
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation as GeneratedInstanceLastOperationEnum,
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus as GeneratedInstanceStatusEnum,
} from './generated-orval/api.schemas';
export { AXIOS_INSTANCE, customAxiosInstance, setAuthToken } from './axios-mutator';
export { keysToCamel, keysToSnake, snakeToCamel, camelToSnake } from '@/shared/utils/case-converter';
// Re-export raw generated APIs/types for advanced usages
export * from './generated-orval/api';
export type * from './generated-orval/api.schemas';
// ---------- Friendly type aliases ----------
export type AuthResponse = GeneratedAuthResponse;
export type RegisterBody = GeneratedRegisterRequest;
export type AdminCreateUserRequest = RegisterBody & {
role?: string;
workspaceId?: string;
namespace?: string;
defaultClusterId?: string;
quotaCpu?: string;
quotaMemory?: string;
quotaGpu?: string;
quotaGpuMemory?: string;
isActive?: boolean;
mustChangePassword?: boolean;
};
export type LoginBody = GeneratedLoginRequest;
export type RefreshTokenBody = GeneratedRefreshTokenRequest;
export type UserResponse = GeneratedUserResponse & {
role?: string;
workspaceId?: string;
workspaceName?: string;
namespace?: string;
defaultClusterId?: string;
quotaCpu?: string;
quotaMemory?: string;
quotaGpu?: string;
quotaGpuMemory?: string;
isActive?: boolean;
mustChangePassword?: boolean;
};
export type UpdateUserRequest = {
role?: string;
workspaceId?: string;
namespace?: string;
defaultClusterId?: string;
quotaCpu?: string;
quotaMemory?: string;
quotaGpu?: string;
quotaGpuMemory?: string;
isActive?: boolean;
mustChangePassword?: boolean;
};
export type ValuesYamlResponse = { valuesYaml: string };
export type ClusterResponse = GeneratedClusterResponse;
export type CreateClusterRequest = GeneratedCreateClusterRequest;
export type UpdateClusterRequest = GeneratedUpdateClusterRequest;
export type RegistryResponse = GeneratedRegistryResponse;
export type CreateRegistryRequest = GeneratedCreateRegistryRequest;
export type UpdateRegistryRequest = GeneratedUpdateRegistryRequest;
export type RegistryHealthResponse = GeneratedRegistryHealthResponse;
export type InstanceResponse = GeneratedInstanceResponse & {
ownerId?: string;
ownerUsername?: string;
};
export type CreateInstanceRequest = GeneratedCreateInstanceRequest;
export type UpdateInstanceRequest = GeneratedUpdateInstanceRequest;
export type InstanceEntry = GeneratedInstanceEntry;
export type InstanceDiagnosticsResponse = {
instanceName?: string;
namespace?: string;
collectedAt?: string;
pods?: Array<{
name?: string;
namespace?: string;
phase?: string;
nodeName?: string;
podIp?: string;
hostIp?: string;
restartCount?: number;
containers?: Array<{
name?: string;
image?: string;
ready?: boolean;
restartCount?: number;
state?: string;
reason?: string;
message?: string;
}>;
conditions?: Array<{ type?: string; status?: string; reason?: string; message?: string }>;
creationTimestamp?: string;
}>;
services?: Array<{
name?: string;
namespace?: string;
type?: string;
clusterIP?: string;
ports?: Array<{ name?: string; protocol?: string; port?: number; targetPort?: string; nodePort?: number }>;
}>;
events?: Array<{
type?: string;
reason?: string;
message?: string;
involvedKind?: string;
involvedName?: string;
count?: number;
firstTimestamp?: string;
lastTimestamp?: string;
}>;
logs?: Array<{ pod?: string; container?: string; tailLines?: number; log?: string; error?: string }>;
};
export const INSTANCE_STATUS = GeneratedInstanceStatusEnum;
export type InstanceStatus = NonNullable<InstanceResponse['status']>;
export const INSTANCE_LAST_OPERATION = GeneratedInstanceLastOperationEnum;
export type InstanceLastOperation = NonNullable<InstanceResponse['lastOperation']>;
export type ArtifactResponse = GeneratedArtifactResponse;
export type ArtifactListItem = GeneratedTagResponse;
export type ListRepositories200Item =
| {
name?: string;
artifact_count?: number;
artifactCount?: number;
}
| string;
export type RepositoryListResponse = GeneratedRepositoryListResponse;
export type ListArtifactsFilter = 'all' | 'chart' | 'image' | 'other';
export type ClusterMonitoring = GeneratedClusterMetricsResponse;
export type ClusterMonitoringStatus = ClusterMonitoring['status'];
export type MonitoringSummary = GeneratedMonitoringSummary;
export type NodeMetricsResponse = GeneratedNodeMetricsResponse;
// ---------- Friendly function aliases ----------
export const login = postAuthLogin;
export const register = postAuthRegister;
export const refreshAuth = postAuthRefresh;
export const fetchAuthStatus = () =>
AXIOS_INSTANCE.get<{ needsSetup: boolean; hasUsers: boolean }>("/auth/status").then((r) => r.data);
export const setupInitialAdmin = (data: { username: string; password: string; email?: string }) =>
AXIOS_INSTANCE.post<{ accessToken: string; refreshToken: string }>("/auth/setup", data).then((r) => r.data);
export const listUsers = () => customAxiosInstance<UserResponse[]>({ url: "/users", method: "GET" });
export const createUser = (data: AdminCreateUserRequest) =>
customAxiosInstance<UserResponse>({ url: "/users", method: "POST", data });
export const updateUser = (userId: string, data: UpdateUserRequest) =>
customAxiosInstance<UserResponse>({ url: `/users/${encodeURIComponent(userId)}`, method: "PUT", data });
export const deleteUser = (userId: string) =>
customAxiosInstance<void>({ url: `/users/${encodeURIComponent(userId)}`, method: "DELETE" });
export const listClusters = getClusters;
export const createCluster = postClusters;
export const getCluster = getClustersClusterId;
export const updateCluster = putClustersClusterId;
export const deleteCluster = deleteClustersClusterId;
export const getClusterHealth = getClustersClusterIdHealth;
export const listInstances = getClustersClusterIdInstances;
export const createInstance = postClustersClusterIdInstances;
export const getInstance = getClustersClusterIdInstancesInstanceId;
export const updateInstance = putClustersClusterIdInstancesInstanceId;
export const deleteInstance = deleteClustersClusterIdInstancesInstanceId;
export const listInstanceEntries = getClustersClusterIdInstancesInstanceIdEntries;
export const scaleInstance = (
clusterId: string,
instanceId: string,
body: { replicas: number; workload?: string },
) => {
return customAxiosInstance<{ instance: InstanceResponse; replicas: number; message: string }>({
url: `/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/scale`,
method: "POST",
data: body,
});
};
export const getInstanceValuesDiff = (
clusterId: string,
instanceId: string,
) => {
return customAxiosInstance<{ current: Record<string, unknown>; defaults: Record<string, unknown> }>({
url: `/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/values-diff`,
method: "GET",
});
};
export const getInstanceDiagnostics = (
params: { clusterId: string; instanceId: string },
options?: { tailLines?: number },
) =>
customAxiosInstance<InstanceDiagnosticsResponse>({
url: `/clusters/${encodeURIComponent(params.clusterId)}/instances/${encodeURIComponent(params.instanceId)}/diagnostics`,
method: "GET",
params: options?.tailLines ? { tailLines: options.tailLines } : undefined,
});
/**
* Stream pod logs via SSE from the backend.
* Returns an AbortController to cancel the stream at any time.
*/
export function streamInstanceLogs(
clusterId: string,
instanceId: string,
pod: string,
container: string,
tailLines: number = 200,
onLine: (line: string) => void,
onDone: () => void,
onError: (err: Error) => void,
): AbortController {
const controller = new AbortController();
const baseUrl = AXIOS_INSTANCE.defaults.baseURL ?? "/api/v1";
const authHeader = AXIOS_INSTANCE.defaults.headers.common["Authorization"] as string | undefined;
const params = new URLSearchParams({ pod, container, tailLines: String(tailLines) });
const url = `${baseUrl}/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/logs/stream?${params}`;
const headers: Record<string, string> = { Accept: "text/event-stream" };
if (authHeader) {
headers["Authorization"] = authHeader;
}
fetch(url, { headers, signal: controller.signal })
.then(async (response) => {
if (!response.ok) {
const text = await response.text().catch(() => response.statusText);
onError(new Error(`HTTP ${response.status}: ${text}`));
return;
}
const reader = response.body?.getReader();
if (!reader) {
onError(new Error("ReadableStream not supported"));
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last potentially-incomplete line in the buffer
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data:")) continue;
const data = trimmed.slice(5).trim();
if (data === "[DONE]") {
onDone();
return;
}
if (data.startsWith("[ERROR]")) {
onError(new Error(data.slice(7).trim()));
continue;
}
onLine(data);
}
}
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") {
// Stream was intentionally cancelled - not an error
return;
}
onError(err instanceof Error ? err : new Error(String(err)));
}
onDone();
})
.catch((err: unknown) => {
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
onError(err instanceof Error ? err : new Error(String(err)));
});
return controller;
}
export const listRegistries = getRegistries;
export const createRegistry = postRegistries;
export const getRegistry = getRegistriesRegistryId;
export const updateRegistry = putRegistriesRegistryId;
export const deleteRegistry = deleteRegistriesRegistryId;
export const checkRegistryHealth = getRegistriesRegistryIdHealth;
export const listRepositories = (
params: GetRegistriesRegistryIdRepositoriesPathParameters,
options?: { artifactType?: 'chart' | 'all' },
) =>
getRegistriesRegistryIdRepositories(params, {
params: options?.artifactType ? { artifact_type: options.artifactType } : undefined,
});
type ListArtifactsRequestOptions = AxiosOptions<typeof getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts>;
export const listArtifacts = (
params: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters,
options?: { filter?: ListArtifactsFilter },
axiosOptions?: ListArtifactsRequestOptions,
) => {
const query =
options?.filter && options.filter !== 'all'
? { media_type: options.filter }
: undefined;
return getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts(params, query, axiosOptions);
};
export const getArtifact = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference;
export const getValuesSchema = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema;
export const getValuesYaml = (params: GetValuesSchemaPathParameters) =>
customAxiosInstance<ValuesYamlResponse>({
url: `/registries/${encodeURIComponent(params.registryId)}/repositories/${encodeURIComponent(params.repositoryName)}/artifacts/${encodeURIComponent(params.reference)}/values-yaml`,
method: "GET",
});
export const listClusterMonitoring = getMonitoringClusters;
export const getClusterMonitoring = getMonitoringClustersClusterId;
export const getClusterNodeMetrics = getMonitoringClustersClusterIdNodes;
export const getMonitoringSummaryData = getMonitoringSummary;
// Re-export parameter types with friendly names for caller convenience
export type DeleteClusterPathParameters = DeleteClustersClusterIdPathParameters;
export type UpdateClusterPathParameters = PutClustersClusterIdPathParameters;
export type ClusterInstancesPathParameters = GetClustersClusterIdInstancesPathParameters;
export type InstancePathParameters = GetClustersClusterIdInstancesInstanceIdPathParameters;
export type UpdateInstancePathParameters = PutClustersClusterIdInstancesInstanceIdPathParameters;
export type DeleteInstancePathParameters = DeleteClustersClusterIdInstancesInstanceIdPathParameters;
export type RegistryPathParameters = GetRegistriesRegistryIdPathParameters;
export type UpdateRegistryPathParameters = PutRegistriesRegistryIdPathParameters;
export type DeleteRegistryPathParameters = DeleteRegistriesRegistryIdPathParameters;
export type RegistryHealthPathParameters = GetRegistriesRegistryIdHealthPathParameters;
export type ListRepositoriesPathParameters = GetRegistriesRegistryIdRepositoriesPathParameters;
export type ListArtifactsPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters;
export type ListArtifactsParams = { filter?: ListArtifactsFilter };
export type GetArtifactPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters;
export type GetValuesSchemaPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters;