Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/db/apiKeys.db.server.ts
T

254 lines
8.9 KiB
TypeScript

import { adminGetAllLegacyTenantSummaries } from "@hivegrid/db/accounts.server";
import {
createLegacyApiKeyLog,
createLegacyApiKey,
countLegacyApiKeyLogsForKey,
deleteLegacyApiKey,
findFirstLegacyActiveApiKey,
findLegacyApiKeyById,
findLegacyApiKeyByKey,
findRedactedLegacyApiKeyByAlias,
findRedactedLegacyApiKeyById,
getRedactedLegacyApiKeysForTenant,
rotateLegacyApiKey,
getLegacyApiKeyLogsForKey,
type LegacyApiKeyLogWithDetails,
type LegacyRedactedApiKeyWithDetails,
type LegacyApiKeySimple,
type LegacyApiKeyWithDetails,
updateLegacyApiKey,
} from "@hivegrid/db/api-keys.server";
import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server";
import { db } from "../db.server";
import { getTrustedClientIpOrNull } from "~/utils/server/IpUtils";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { FiltersDto } from "~/application/dtos/data/FiltersDto";
import RowFiltersHelper from "../helpers/RowFiltersHelper";
import { loadRequiredWebConfig } from "@hivegrid/config";
import { getApiKeyLookupCacheKey, serializeApiLogSearchParams } from "../api/apiKeySecurity.server";
import { invalidateApiKeySnapshot, loadCachedApiKeySnapshot } from "../api/api-key-auth-cache.server";
export type ApiKeyWithDetails = LegacyApiKeyWithDetails;
export type RedactedApiKeyWithDetails = LegacyRedactedApiKeyWithDetails;
export function redactApiKeyForDisplay(apiKey: LegacyApiKeyWithDetails): RedactedApiKeyWithDetails {
const redactedApiKey = { ...apiKey };
delete (redactedApiKey as Partial<LegacyApiKeyWithDetails>).key;
return redactedApiKey;
}
export function redactApiKeysForDisplay(apiKeys: LegacyApiKeyWithDetails[]): RedactedApiKeyWithDetails[] {
return apiKeys.map(redactApiKeyForDisplay);
}
export type ApiKeyLogWithDetails = LegacyApiKeyLogWithDetails;
export type ApiKeySimple = LegacyApiKeySimple;
export async function getAllApiKeys(): Promise<RedactedApiKeyWithDetails[]> {
const groups = await mapAdminTenants((tenantId) => getRedactedLegacyApiKeysForTenant(tenantId));
return groups.flat().sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime());
}
export async function getAllApiKeyLogs(
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: ApiKeyLogWithDetails[]; pagination: PaginationDto }> {
const where = RowFiltersHelper.getFiltersCondition(filters);
const items = await db.apiKeyLog.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
where,
include: {
apiKey: {
include: { tenant: true },
},
},
orderBy: {
createdAt: "desc",
},
});
const totalItems = await db.apiKeyLog.count({
where,
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getTenantApiKeyLogs(
tenantId: string,
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: ApiKeyLogWithDetails[]; pagination: PaginationDto }> {
const where = {
AND: [RowFiltersHelper.getFiltersCondition(filters), { apiKey: { tenantId } }],
};
const items = await db.apiKeyLog.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
where,
include: { apiKey: { include: { tenant: true } } },
orderBy: { createdAt: "desc" },
});
const totalItems = await db.apiKeyLog.count({ where });
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getApiKeys(tenantId: string): Promise<RedactedApiKeyWithDetails[]> {
return runWithRlsContext({ tenantId }, () => getRedactedLegacyApiKeysForTenant(tenantId));
}
export async function adminGetApiKeyById(id: string): Promise<RedactedApiKeyWithDetails | null> {
return findAdminApiKeyById(id);
}
export async function getTenantApiKeyById(id: string, tenantId: string): Promise<RedactedApiKeyWithDetails | null> {
return runWithRlsContext({ tenantId }, () => findRedactedLegacyApiKeyById({ id, tenantId }));
}
export async function getApiKey(key: string): Promise<ApiKeySimple | null> {
return loadCachedApiKeySnapshot(getApiKeyLookupCacheKey(key), () => findLegacyApiKeyByKey(key));
}
export async function getApiKeyByAlias(tenantId: string, alias: string): Promise<RedactedApiKeyWithDetails | null> {
return runWithRlsContext({ tenantId }, () => findRedactedLegacyApiKeyByAlias(tenantId, alias));
}
export async function getApiKeyLogs(id: string, tenantId: string): Promise<ApiKeyLogWithDetails[]> {
return runWithRlsContext({ tenantId }, () => getLegacyApiKeyLogsForKey({ id, tenantId }));
}
export async function countApiKeyLogsForKey(id: string, tenantId: string): Promise<number> {
return runWithRlsContext({ tenantId }, () => countLegacyApiKeyLogsForKey({ id, tenantId }));
}
export async function getFirstApiKey(tenantId: string | null) {
if (!tenantId) return null;
return runWithRlsContext({ tenantId }, () => findFirstLegacyActiveApiKey(tenantId));
}
export async function createApiKey(data: { tenantId: string; createdByUserId: string; alias: string; expires: Date | null; active: boolean }) {
return runWithRlsContext({ tenantId: data.tenantId }, () => createLegacyApiKey(data, undefined, getApiKeyHashSecret()));
}
export async function createApiKeyLog(
request: Request,
data: {
tenantId: string | null;
apiKeyId: string | null;
endpoint: string;
error?: string;
status?: number;
alias?: string;
tenantName?: string;
}
) {
return await createLegacyApiKeyLog({
tenantId: data.tenantId,
apiKeyId: data.apiKeyId,
endpoint: data.endpoint,
error: data.error,
status: data.status,
alias: data.alias,
tenantName: data.tenantName,
ip: getTrustedClientIpOrNull(request) ?? "",
method: request.method,
params: serializeApiLogSearchParams(request),
});
}
export async function updateApiKey(
id: string,
data: {
tenantId: string;
alias: string;
expires: Date | null;
active: boolean;
}
) {
const apiKey = await runWithRlsContext({ tenantId: data.tenantId }, () => updateLegacyApiKey(id, data));
await invalidateApiKeySnapshot(getApiKeyLookupCacheKey(apiKey.key));
return apiKey;
}
export async function rotateApiKey(id: string, tenantId: string) {
const previousApiKey = await runWithRlsContext({ tenantId }, () => findLegacyApiKeyById({ id, tenantId }));
if (!previousApiKey) {
throw new Error(`API key ${id} not found`);
}
const apiKey = await runWithRlsContext({ tenantId }, () => rotateLegacyApiKey({ id, tenantId: previousApiKey.tenantId }, undefined, getApiKeyHashSecret()));
if (previousApiKey?.key) {
await invalidateApiKeySnapshot(getApiKeyLookupCacheKey(previousApiKey.key));
}
await invalidateApiKeySnapshot(getApiKeyLookupCacheKey(apiKey.key));
return apiKey;
}
export async function deleteApiKey(id: string, tenantId: string) {
const previousApiKey = await runWithRlsContext({ tenantId }, () => findLegacyApiKeyById({ id, tenantId }));
if (!previousApiKey) {
throw new Error(`API key ${id} not found`);
}
const apiKey = await runWithRlsContext({ tenantId }, () => deleteLegacyApiKey({ id, tenantId: previousApiKey.tenantId }));
await invalidateApiKeySnapshot(getApiKeyLookupCacheKey(apiKey.key));
return apiKey;
}
function getApiKeyHashSecret() {
return loadRequiredWebConfig().jwtSecret;
}
async function findAdminApiKeyById(id: string): Promise<RedactedApiKeyWithDetails | null> {
const normalizedId = id.trim();
if (!normalizedId) return null;
const matches = (await mapAdminTenants((tenantId) => findRedactedLegacyApiKeyById({ id: normalizedId, tenantId }))).filter(
(item): item is RedactedApiKeyWithDetails => item !== null
);
if (matches.length > 1) {
throw new Error(`API key ${normalizedId} resolved to more than one tenant`);
}
return matches[0] ?? null;
}
async function mapAdminTenants<T>(operation: (tenantId: string) => Promise<T>): Promise<T[]> {
const context = getRlsExecutionContext();
if (!context?.userId || context.tenantId) {
throw new Error("Admin API key access requires an unscoped authenticated operator context");
}
const tenants = await adminGetAllLegacyTenantSummaries();
return mapWithConcurrency(tenants, 8, ({ id }) => runWithRlsContext({ tenantId: id }, () => operation(id)));
}
async function mapWithConcurrency<T, U>(items: readonly T[], concurrency: number, operation: (item: T) => Promise<U>): Promise<U[]> {
const results = new Array<U>(items.length);
let cursor = 0;
await Promise.all(
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
while (cursor < items.length) {
const index = cursor++;
results[index] = await operation(items[index]!);
}
})
);
return results;
}