Files
homarr/packages/api/src/router/custom-widget/configuration-requests.ts
T

107 lines
3.8 KiB
TypeScript

import { createId } from "@homarr/common";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import type { CustomWidgetSecretKind } from "@homarr/custom-widgets/core";
import type { CustomWidgetSource } from "@homarr/custom-widgets/core";
const CONFIGURATION_REQUEST_TTL_MS = 10 * 60_000;
const CONFIGURATION_REQUEST_PREFIX = "custom-widget:configuration-request:";
const CONFIGURATION_REQUEST_LOCK_PREFIX = "custom-widget:configuration-request-lock:";
export interface CustomWidgetConfigurationRequest {
id: string;
userId: string;
target: { type: "definition"; id: string } | { type: "preview"; id: string };
widgetName: string;
sourceId: string;
sourceName: string;
source: CustomWidgetSource;
kinds: CustomWidgetSecretKind[];
expiresAt: number;
status: "pending" | "completed";
}
const localRequests = new Map<string, CustomWidgetConfigurationRequest>();
const localLocks = new Set<string>();
let redis: RedisClient | undefined;
function getRedis() {
if (process.env.CI !== undefined || process.env.NODE_ENV === "test") return undefined;
redis ??= createRedisClient();
return redis;
}
async function save(request: CustomWidgetConfigurationRequest) {
const ttl = Math.max(1, request.expiresAt - Date.now());
const client = getRedis();
if (client) await client.set(`${CONFIGURATION_REQUEST_PREFIX}${request.id}`, JSON.stringify(request), "PX", ttl);
else localRequests.set(request.id, request);
}
export async function createCustomWidgetConfigurationRequest(
input: Omit<CustomWidgetConfigurationRequest, "id" | "expiresAt" | "status">,
) {
const request: CustomWidgetConfigurationRequest = {
...input,
id: createId(),
expiresAt: Date.now() + CONFIGURATION_REQUEST_TTL_MS,
status: "pending",
};
await save(request);
return request;
}
export async function getCustomWidgetConfigurationRequest(id: string) {
const client = getRedis();
const candidate = client ? await client.get(`${CONFIGURATION_REQUEST_PREFIX}${id}`) : localRequests.get(id);
if (!candidate) return null;
try {
const request = (
typeof candidate === "string" ? JSON.parse(candidate) : candidate
) as CustomWidgetConfigurationRequest;
if (request.expiresAt <= Date.now()) {
if (client) await client.del(`${CONFIGURATION_REQUEST_PREFIX}${id}`);
else localRequests.delete(id);
return null;
}
return request;
} catch {
return null;
}
}
export async function getCustomWidgetConfigurationRequestForUser(id: string, userId: string) {
const request = await getCustomWidgetConfigurationRequest(id);
return request?.userId === userId ? request : null;
}
export async function completeCustomWidgetConfigurationRequest(id: string) {
const request = await getCustomWidgetConfigurationRequest(id);
if (!request || request.status !== "pending") return null;
const completed = { ...request, status: "completed" as const };
await save(completed);
await releaseCustomWidgetConfigurationRequest(id);
return completed;
}
export async function claimCustomWidgetConfigurationRequest(id: string) {
const request = await getCustomWidgetConfigurationRequest(id);
if (!request || request.status !== "pending") return null;
const client = getRedis();
if (client) {
const claimed = await client.set(`${CONFIGURATION_REQUEST_LOCK_PREFIX}${id}`, "1", "PX", 60_000, "NX");
return claimed === "OK" ? request : null;
}
if (localLocks.has(id)) return null;
localLocks.add(id);
return request;
}
export async function releaseCustomWidgetConfigurationRequest(id: string) {
const client = getRedis();
if (client) await client.del(`${CONFIGURATION_REQUEST_LOCK_PREFIX}${id}`);
else localLocks.delete(id);
}
export { setPreviewSessionSecrets } from "./preview-sessions";