NODEDC_LAUNCHER/src/shared/api/mockApi.ts

486 lines
17 KiB
TypeScript

import { computeEffectiveAccess } from "../../entities/access/computeEffectiveAccess";
import type { AccessRequest } from "../../entities/access-request/types";
import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant, ServiceModuleEntitlement } from "../../entities/access/types";
import type { Client, TaskManagerWorkspaceManagedBy } from "../../entities/client/types";
import type { EngineWorkflowAccessRequest } from "../../entities/engine-workflow-access-request/types";
import type { Invite } from "../../entities/invite/types";
import { PUBLIC_POOL_CLIENT, isPublicPoolClientId } from "../../entities/public-pool/constants";
import { getServiceLaunchLink } from "../../entities/service/links";
import type { LauncherServiceView, Service } from "../../entities/service/types";
import type { SyncStatus } from "../../entities/sync/types";
import type { TaskerInviteRequest } from "../../entities/tasker-invite-request/types";
import type {
ClientGroup,
ClientMembership,
ClientMembershipRole,
LauncherGlobalRole,
LauncherUser,
} from "../../entities/user/types";
import { resolveLauncherRole, resolvePermissions, type LauncherPermissions } from "../lib/permissions";
import {
mockAuditEvents,
mockAccessRequests,
mockEngineWorkflowAccessRequests,
mockTaskerInviteRequests,
mockClients,
mockExceptions,
mockGrants,
mockGroups,
mockInvites,
mockMemberships,
mockServices,
mockSyncStatuses,
mockUsers,
} from "./mockData";
export interface AuthentikClaimsMock {
sub: string;
email: string;
name: string;
avatarUrl?: string | null;
groups: string[];
activeClientId: string;
}
export interface MeResponse {
user: Pick<LauncherUser, "id" | "authentikUserId" | "name" | "email" | "avatarUrl">;
launcherRole: LauncherGlobalRole;
memberships: Array<{
clientId: string;
clientName: string;
role: ClientMembershipRole;
status: ClientMembership["status"];
coreAssistantRole: ClientMembership["coreAssistantRole"];
}>;
activeClientId: string;
permissions: LauncherPermissions;
mockAuthentikClaims: AuthentikClaimsMock;
}
export interface LauncherData {
clients: Client[];
users: LauncherUser[];
memberships: ClientMembership[];
groups: ClientGroup[];
services: Service[];
grants: ServiceGrant[];
exceptions: ServiceAccessException[];
serviceModuleEntitlements: ServiceModuleEntitlement[];
invites: Invite[];
accessRequests: AccessRequest[];
revokedAccounts: RevokedAccount[];
taskerInviteRequests: TaskerInviteRequest[];
engineWorkflowAccessRequests: EngineWorkflowAccessRequest[];
syncStatuses: SyncStatus[];
auditEvents: typeof mockAuditEvents;
taskManagerMemberships: TaskManagerMembershipAssignment[];
taskManagerProjectMemberships: TaskManagerProjectMembershipAssignment[];
settings: LauncherSettings;
}
export interface RevokedAccount {
id: string;
email: string;
name?: string | null;
sourceUserId?: string | null;
authentikUserId?: string | null;
reason: string;
revokedByUserId?: string | null;
revokedByUserEmail?: string | null;
revokedByUserName?: string | null;
revokedAt: string;
createdAt: string;
updatedAt: string;
}
export interface TaskManagerMembershipAssignment {
id: string;
clientId: string;
userId: string;
workspaceSlug: string;
workspaceName?: string | null;
role: "guest" | "member" | "admin";
managedBy?: TaskManagerWorkspaceManagedBy;
planeUserId?: string | null;
planeRole?: number | null;
updatedAt: string;
}
export interface TaskManagerProjectMembershipAssignment {
id: string;
clientId: string;
userId: string;
workspaceSlug: string;
workspaceName?: string | null;
projectId: string;
projectIdentifier?: string | null;
projectName?: string | null;
role: "guest" | "member" | "admin";
managedBy?: TaskManagerWorkspaceManagedBy;
planeUserId?: string | null;
planeRole?: number | null;
updatedAt: string;
}
export interface LauncherSettings {
brand: {
logoLinkUrl: string;
};
taskManager: {
workspaceCreationPolicy: TaskManagerWorkspaceCreationPolicy;
};
}
export type TaskManagerWorkspaceCreationPolicy = "any_authorized_user" | "task_admins_only" | "disabled";
export interface ProfileOption {
userId: string;
label: string;
description: string;
defaultClientId: string;
}
export interface AccessMatrixCell {
userId: string;
serviceId: string;
effectiveAccess: EffectiveAccessResult;
}
export interface AccessMatrix {
client: Client;
users: LauncherUser[];
groups: ClientGroup[];
services: Service[];
cells: AccessMatrixCell[];
}
export const defaultLauncherSettings: LauncherSettings = {
brand: {
logoLinkUrl: "/",
},
taskManager: {
workspaceCreationPolicy: "any_authorized_user",
},
};
export const initialLauncherData: LauncherData = normalizeLauncherData({
clients: mockClients,
users: mockUsers,
memberships: mockMemberships,
groups: mockGroups,
services: mockServices,
grants: mockGrants,
exceptions: mockExceptions,
invites: mockInvites,
accessRequests: mockAccessRequests,
revokedAccounts: [],
taskerInviteRequests: mockTaskerInviteRequests,
engineWorkflowAccessRequests: mockEngineWorkflowAccessRequests,
syncStatuses: mockSyncStatuses,
auditEvents: mockAuditEvents,
settings: defaultLauncherSettings,
});
export function normalizeLauncherSettings(settings?: Partial<LauncherSettings> | null): LauncherSettings {
const brand =
typeof settings?.brand === "object" && settings.brand !== null
? settings.brand
: ({} as Partial<LauncherSettings["brand"]>);
const taskManager =
typeof settings?.taskManager === "object" && settings.taskManager !== null
? settings.taskManager
: ({} as Partial<LauncherSettings["taskManager"]>);
const logoLinkUrl = typeof brand.logoLinkUrl === "string" && brand.logoLinkUrl.trim() ? brand.logoLinkUrl.trim() : "/";
const workspaceCreationPolicy = isTaskManagerWorkspaceCreationPolicy(taskManager.workspaceCreationPolicy)
? taskManager.workspaceCreationPolicy
: defaultLauncherSettings.taskManager.workspaceCreationPolicy;
return {
brand: {
logoLinkUrl,
},
taskManager: {
workspaceCreationPolicy,
},
};
}
function isTaskManagerWorkspaceCreationPolicy(value: unknown): value is TaskManagerWorkspaceCreationPolicy {
return value === "any_authorized_user" || value === "task_admins_only" || value === "disabled";
}
export function normalizeLauncherData(data: Partial<LauncherData> | null | undefined): LauncherData {
const payload = data ?? {};
return {
clients: Array.isArray(payload.clients) ? payload.clients : mockClients,
users: Array.isArray(payload.users) ? payload.users : mockUsers,
memberships: Array.isArray(payload.memberships) ? payload.memberships.map(normalizeMembership) : mockMemberships.map(normalizeMembership),
groups: Array.isArray(payload.groups) ? payload.groups : mockGroups,
services: Array.isArray(payload.services) ? payload.services : mockServices,
grants: Array.isArray(payload.grants) ? payload.grants : mockGrants,
exceptions: Array.isArray(payload.exceptions) ? payload.exceptions : mockExceptions,
serviceModuleEntitlements: Array.isArray(payload.serviceModuleEntitlements) ? payload.serviceModuleEntitlements : [],
invites: Array.isArray(payload.invites) ? payload.invites : mockInvites,
accessRequests: Array.isArray(payload.accessRequests) ? payload.accessRequests : mockAccessRequests,
revokedAccounts: Array.isArray(payload.revokedAccounts) ? payload.revokedAccounts : [],
taskerInviteRequests: Array.isArray(payload.taskerInviteRequests) ? payload.taskerInviteRequests : mockTaskerInviteRequests,
engineWorkflowAccessRequests: Array.isArray(payload.engineWorkflowAccessRequests)
? payload.engineWorkflowAccessRequests
: mockEngineWorkflowAccessRequests,
syncStatuses: Array.isArray(payload.syncStatuses) ? payload.syncStatuses : mockSyncStatuses,
auditEvents: Array.isArray(payload.auditEvents) ? payload.auditEvents : mockAuditEvents,
taskManagerMemberships: Array.isArray(payload.taskManagerMemberships) ? payload.taskManagerMemberships : [],
taskManagerProjectMemberships: Array.isArray(payload.taskManagerProjectMemberships) ? payload.taskManagerProjectMemberships : [],
settings: normalizeLauncherSettings(payload.settings),
};
}
export const profileOptions: ProfileOption[] = [
{
userId: "user_root",
label: "DC Touch",
description: "NODE.DC superadmin",
defaultClientId: "client_romashka",
},
{
userId: "user_silver_psih",
label: "Silver Psy",
description: "DCTOUCH manager",
defaultClientId: "client_romashka",
},
];
export function buildMe(data: LauncherData, userId: string, requestedClientId?: string): MeResponse {
const user = getUser(data, userId);
const isRoot = user.id === "user_root";
const availableMemberships = isRoot
? data.clients.map((client) => ({
clientId: client.id,
clientName: client.name,
role: "client_owner" as const,
status: "active" as const,
coreAssistantRole: "admin" as const,
}))
: data.memberships
.filter((membership) => membership.userId === user.id)
.map((membership) => ({
clientId: membership.clientId,
clientName: getClient(data, membership.clientId).name,
role: membership.role,
status: membership.status,
coreAssistantRole: normalizeCoreAssistantRole(membership.coreAssistantRole),
}));
const fallbackClientId =
profileOptions.find((option) => option.userId === user.id)?.defaultClientId ??
availableMemberships[0]?.clientId ??
PUBLIC_POOL_CLIENT.id;
const canUseRequestedClient = availableMemberships.some((membership) => membership.clientId === requestedClientId);
const activeClientId = canUseRequestedClient ? requestedClientId! : fallbackClientId;
const activeMembership = availableMemberships.find((membership) => membership.clientId === activeClientId);
const launcherRole = resolveLauncherRole({ isRoot, membershipRole: activeMembership?.role });
const permissions = resolvePermissions({
launcherRole,
membershipStatus: activeMembership?.status,
});
return {
user: {
id: user.id,
authentikUserId: user.authentikUserId,
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
},
launcherRole,
memberships: availableMemberships,
activeClientId,
permissions,
mockAuthentikClaims: {
sub: user.authentikUserId ?? user.id,
email: user.email,
name: user.name,
groups: buildMockGroups(data, user.id, activeClientId, launcherRole),
activeClientId,
},
};
}
function normalizeMembership(membership: ClientMembership): ClientMembership {
return {
...membership,
coreAssistantRole: normalizeCoreAssistantRole(membership.coreAssistantRole, membership.userId === "user_root" ? "admin" : "member"),
};
}
function normalizeCoreAssistantRole(
value: ClientMembership["coreAssistantRole"] | null | undefined,
fallback: NonNullable<ClientMembership["coreAssistantRole"]> = "member"
): NonNullable<ClientMembership["coreAssistantRole"]> {
if (value === "blocked" || value === "member" || value === "admin") return value;
return fallback;
}
export function buildLauncherServices(data: LauncherData, userId: string, activeClientId: string): LauncherServiceView[] {
const me = buildMe(data, userId, activeClientId);
const user = getUser(data, userId);
const client = getClient(data, activeClientId);
const membership = getRuntimeMembership(data, user.id, activeClientId, me.launcherRole === "root_admin");
const userGroups = getUserGroups(data, user.id, activeClientId);
const isRoot = me.launcherRole === "root_admin";
return data.services
.slice()
.sort((a, b) => a.order - b.order)
.map((service) => {
const effectiveAccess = computeEffectiveAccess({
client,
user,
membership,
userGroups,
service,
grants: data.grants,
exceptions: data.exceptions,
});
return {
id: service.id,
slug: service.slug,
title: service.title,
subtitle: service.subtitle,
description: service.description,
fullDescription: service.fullDescription,
status: service.status,
userAccess: effectiveAccess.allowed ? ("allowed" as const) : ("denied" as const),
appRole: effectiveAccess.appRole,
openUrl: effectiveAccess.openEnabled ? getServiceLaunchLink(service) || null : null,
accentColor: service.accentColor,
media: {
icon: service.iconUrl,
thumbnail: service.coverImageUrl,
coverImage: service.coverImageUrl,
coverKind: service.coverMediaKind,
coverSource: service.coverMediaSource,
coverFileName: service.coverMediaFileName,
previewVideo: service.previewVideoUrl,
ambientVideo: service.ambientVideoUrl,
ambientKind: service.ambientMediaKind,
ambientSource: service.ambientMediaSource,
ambientFileName: service.ambientMediaFileName,
fallbackGradient: service.fallbackGradient,
},
effectiveAccess,
};
})
.filter((service) => isRoot || service.status !== "hidden");
}
export function buildAccessMatrix(data: LauncherData, clientId: string, includeAllServices: boolean): AccessMatrix {
const client = getClient(data, clientId);
const memberships = data.memberships.filter((membership) => membership.clientId === clientId);
const users = memberships.map((membership) => getUser(data, membership.userId));
const groups = data.groups.filter((group) => group.clientId === clientId);
const clientServiceIds = new Set(
data.grants
.filter((grant) => grant.targetType === "client" && grant.targetId === clientId && grant.status === "active")
.map((grant) => grant.serviceId)
);
const services = data.services
.filter((service) => includeAllServices || clientServiceIds.has(service.id) || service.status === "active")
.sort((a, b) => a.order - b.order);
const cells = users.flatMap((user) => {
const membership = memberships.find((item) => item.userId === user.id) ?? getRuntimeMembership(data, user.id, clientId);
const userGroups = getUserGroups(data, user.id, clientId);
return services.map((service) => ({
userId: user.id,
serviceId: service.id,
effectiveAccess: computeEffectiveAccess({
client,
user,
membership,
userGroups,
service,
grants: data.grants,
exceptions: data.exceptions,
}),
}));
});
return { client, users, groups, services, cells };
}
export function getClient(data: LauncherData, clientId: string): Client {
if (isPublicPoolClientId(clientId)) {
return PUBLIC_POOL_CLIENT;
}
const client = data.clients.find((item) => item.id === clientId);
if (!client) throw new Error(`Unknown client: ${clientId}`);
return client;
}
export function getUser(data: LauncherData, userId: string): LauncherUser {
const user = data.users.find((item) => item.id === userId);
if (!user) throw new Error(`Unknown user: ${userId}`);
return user;
}
export function getService(data: LauncherData, serviceId: string): Service {
const service = data.services.find((item) => item.id === serviceId);
if (!service) throw new Error(`Unknown service: ${serviceId}`);
return service;
}
export function getClientUsers(data: LauncherData, clientId: string): LauncherUser[] {
const userIds = new Set(data.memberships.filter((membership) => membership.clientId === clientId).map((item) => item.userId));
return data.users.filter((user) => userIds.has(user.id));
}
export function getUserGroups(data: LauncherData, userId: string, clientId: string): ClientGroup[] {
return data.groups.filter((group) => group.clientId === clientId && group.memberIds.includes(userId));
}
export function getRuntimeMembership(
data: LauncherData,
userId: string,
clientId: string,
allowVirtualRoot = false
): ClientMembership {
const membership = data.memberships.find((item) => item.userId === userId && item.clientId === clientId);
if (membership) return membership;
if (allowVirtualRoot) {
return {
id: `virtual_root_${clientId}`,
clientId,
userId,
role: "client_owner",
status: "active",
createdAt: "2026-05-01T09:00:00Z",
updatedAt: "2026-05-01T09:00:00Z",
};
}
return {
id: `missing_${clientId}_${userId}`,
clientId,
userId,
role: "member",
status: "disabled",
createdAt: "2026-05-01T09:00:00Z",
updatedAt: "2026-05-01T09:00:00Z",
};
}
function buildMockGroups(
data: LauncherData,
userId: string,
activeClientId: string,
launcherRole: LauncherGlobalRole
): string[] {
const groups = getUserGroups(data, userId, activeClientId).map((group) => `client:${activeClientId}:group:${group.name}`);
return [`launcher:${launcherRole}`, `client:${activeClientId}`, ...groups];
}