forked from baron/baron-sso
612 lines
15 KiB
TypeScript
612 lines
15 KiB
TypeScript
export type OrgChartTenantSelection = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
export type OrgChartUserSelection = {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
rootTenantName?: string;
|
|
leafTenantName?: string;
|
|
};
|
|
|
|
export type TenantFilterTarget = {
|
|
id?: string;
|
|
tenantId?: string;
|
|
slug?: string;
|
|
tenantSlug?: string;
|
|
type?: string;
|
|
parentId?: string | null;
|
|
name?: string;
|
|
tenantName?: string;
|
|
visibility?: string;
|
|
config?: Record<string, unknown>;
|
|
};
|
|
|
|
export type HanmacFamilyUserTarget = {
|
|
companyCode?: string;
|
|
tenantSlug?: string;
|
|
tenant?: TenantFilterTarget;
|
|
joinedTenants?: TenantFilterTarget[];
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type UserMembershipTenantTabId =
|
|
| "hanmac-family"
|
|
| "commercial"
|
|
| "public-org"
|
|
| "edu"
|
|
| "personal";
|
|
|
|
export type UserMembershipTenantTab = {
|
|
id: UserMembershipTenantTabId;
|
|
label: string;
|
|
rootSlug: string;
|
|
seedTenantId?: string;
|
|
};
|
|
|
|
type OrgChartPickerMessage = {
|
|
type?: unknown;
|
|
payload?: {
|
|
selections?: Array<{
|
|
type?: unknown;
|
|
id?: unknown;
|
|
name?: unknown;
|
|
email?: unknown;
|
|
rootTenantName?: unknown;
|
|
leafTenantName?: unknown;
|
|
tenantName?: unknown;
|
|
}>;
|
|
};
|
|
};
|
|
|
|
type OrgChartTenantPickerOptions = {
|
|
includeInternal?: boolean;
|
|
tenantId?: string;
|
|
};
|
|
|
|
type OrgChartUserMultiPickerOptions = {
|
|
tenantId?: string;
|
|
};
|
|
|
|
type OrgChartLoginOptions = {
|
|
includeInternal?: boolean;
|
|
returnTo?: string;
|
|
};
|
|
|
|
const DEFAULT_ORGFRONT_BASE_URL = "http://localhost:5175";
|
|
|
|
export const USER_MEMBERSHIP_TENANT_TABS: UserMembershipTenantTab[] = [
|
|
{
|
|
id: "hanmac-family",
|
|
label: "한맥가족",
|
|
rootSlug: "hanmac-family",
|
|
seedTenantId: "038326b6-954a-48a7-a85f-efd83f62b82a",
|
|
},
|
|
{
|
|
id: "commercial",
|
|
label: "일반회사",
|
|
rootSlug: "commercial",
|
|
},
|
|
{
|
|
id: "public-org",
|
|
label: "공공기관",
|
|
rootSlug: "public-org",
|
|
},
|
|
{
|
|
id: "edu",
|
|
label: "교육기관",
|
|
rootSlug: "edu",
|
|
},
|
|
{
|
|
id: "personal",
|
|
label: "개인",
|
|
rootSlug: "personal",
|
|
seedTenantId: "9607eb7b-04d2-42ab-80fe-780fe21c7e8f",
|
|
},
|
|
];
|
|
|
|
export const GPDTDC_GRADE_OPTIONS = [
|
|
"연구원",
|
|
"선임",
|
|
"책임",
|
|
"수석",
|
|
"부사장",
|
|
"사장",
|
|
] as const;
|
|
|
|
export const HANMAC_FAMILY_GRADE_OPTIONS = [
|
|
"사원",
|
|
"대리",
|
|
"과장",
|
|
"차장",
|
|
"부장",
|
|
"이사",
|
|
"상무이사",
|
|
"전무이사",
|
|
"부사장",
|
|
"사장",
|
|
"회장",
|
|
] as const;
|
|
|
|
function isSystemTenant(tenant: TenantFilterTarget) {
|
|
const slug = tenant.slug?.trim().toLowerCase();
|
|
const type = tenant.type?.trim().toUpperCase();
|
|
|
|
return (
|
|
!tenant.id?.trim() ||
|
|
!tenant.slug?.trim() ||
|
|
type === "SYSTEM" ||
|
|
slug === "system" ||
|
|
slug === "global"
|
|
);
|
|
}
|
|
|
|
function resolveTenantTarget<T extends TenantFilterTarget>(
|
|
target: TenantFilterTarget | undefined,
|
|
tenants: T[],
|
|
) {
|
|
if (!target) return undefined;
|
|
const tenantID = target.id ?? target.tenantId ?? "";
|
|
const tenantSlug = target.slug ?? target.tenantSlug ?? "";
|
|
return (
|
|
tenants.find((tenant) => tenantID && tenant.id === tenantID) ??
|
|
tenants.find(
|
|
(tenant) =>
|
|
tenantSlug &&
|
|
tenant.slug?.trim().toLowerCase() === tenantSlug.trim().toLowerCase(),
|
|
) ??
|
|
target
|
|
);
|
|
}
|
|
|
|
function resolveMembershipRoot<T extends TenantFilterTarget>(
|
|
tab: UserMembershipTenantTab,
|
|
tenants: T[],
|
|
) {
|
|
const rootSlug = tab.rootSlug.toLowerCase();
|
|
return (
|
|
tenants.find(
|
|
(tenant) => tab.seedTenantId && tenant.id === tab.seedTenantId,
|
|
) ??
|
|
tenants.find((tenant) => tenant.slug?.trim().toLowerCase() === rootSlug)
|
|
);
|
|
}
|
|
|
|
export function classifyTenantByMembershipRoot<T extends TenantFilterTarget>(
|
|
target: TenantFilterTarget | undefined,
|
|
tenants: T[],
|
|
) {
|
|
const tenant = resolveTenantTarget(target, tenants);
|
|
if (!tenant?.id) return undefined;
|
|
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((item) => item.id?.trim())
|
|
.map((item) => [item.id as string, item]),
|
|
);
|
|
|
|
return USER_MEMBERSHIP_TENANT_TABS.find((tab) => {
|
|
const root = resolveMembershipRoot(tab, tenants);
|
|
if (!root?.id) return false;
|
|
const resolvedTenant = tenantById.get(tenant.id ?? "") ?? tenant;
|
|
return isInTenantSubtree(resolvedTenant, root.id, tenantById);
|
|
});
|
|
}
|
|
|
|
export function filterTenantsByMembershipRoot<T extends TenantFilterTarget>(
|
|
tenants: T[],
|
|
tabId: UserMembershipTenantTabId,
|
|
) {
|
|
const tab = USER_MEMBERSHIP_TENANT_TABS.find((item) => item.id === tabId);
|
|
if (!tab) return [];
|
|
|
|
const root = resolveMembershipRoot(tab, tenants);
|
|
if (!root?.id) return [];
|
|
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.id?.trim())
|
|
.map((tenant) => [tenant.id as string, tenant]),
|
|
);
|
|
|
|
return tenants.filter(
|
|
(tenant) =>
|
|
!isSystemTenant(tenant) &&
|
|
isPublicRepresentativeTenant(tenant) &&
|
|
isInTenantSubtree(tenant, root.id as string, tenantById),
|
|
);
|
|
}
|
|
|
|
export function resolveUserMembershipTenantTab<T extends TenantFilterTarget>(
|
|
user: HanmacFamilyUserTarget,
|
|
tenants: T[],
|
|
) {
|
|
const metadataAppointments = Array.isArray(
|
|
user.metadata?.additionalAppointments,
|
|
)
|
|
? user.metadata.additionalAppointments
|
|
.map((appointment) => appointment as TenantFilterTarget)
|
|
.filter(
|
|
(appointment) =>
|
|
typeof appointment.tenantId === "string" ||
|
|
typeof appointment.id === "string" ||
|
|
typeof appointment.tenantSlug === "string" ||
|
|
typeof appointment.slug === "string",
|
|
)
|
|
.map((appointment) => ({
|
|
id: appointment.id ?? appointment.tenantId,
|
|
slug: appointment.slug ?? appointment.tenantSlug,
|
|
parentId: appointment.parentId,
|
|
type: appointment.type,
|
|
name: appointment.name ?? appointment.tenantName,
|
|
}))
|
|
: [];
|
|
const tenantBySlug = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.slug?.trim())
|
|
.map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]),
|
|
);
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.id?.trim())
|
|
.map((tenant) => [tenant.id as string, tenant]),
|
|
);
|
|
const candidates = [
|
|
user.tenant,
|
|
...(user.joinedTenants ?? []),
|
|
...metadataAppointments,
|
|
...metadataAppointments.map((appointment) =>
|
|
tenantById.get(appointment.id ?? ""),
|
|
),
|
|
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
|
|
];
|
|
|
|
return (
|
|
USER_MEMBERSHIP_TENANT_TABS.find((tab) =>
|
|
candidates.some(
|
|
(candidate) =>
|
|
classifyTenantByMembershipRoot(candidate, tenants)?.id === tab.id,
|
|
),
|
|
) ?? USER_MEMBERSHIP_TENANT_TABS[0]
|
|
);
|
|
}
|
|
|
|
function isGPDTDCTenant<T extends TenantFilterTarget>(
|
|
target: TenantFilterTarget | undefined,
|
|
tenants: T[],
|
|
) {
|
|
const tenant = resolveTenantTarget(target, tenants);
|
|
if (!tenant) return false;
|
|
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((item) => item.id?.trim())
|
|
.map((item) => [item.id as string, item]),
|
|
);
|
|
let current: TenantFilterTarget | undefined = tenant;
|
|
const visited = new Set<string>();
|
|
|
|
while (current) {
|
|
const slug = current.slug?.trim().toLowerCase();
|
|
if (slug === "gpdtdc") {
|
|
return true;
|
|
}
|
|
const parentId = current.parentId ?? "";
|
|
if (!parentId || visited.has(parentId)) {
|
|
return false;
|
|
}
|
|
visited.add(parentId);
|
|
current = tenantById.get(parentId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function getTenantGradeOptions<T extends TenantFilterTarget>(
|
|
target: TenantFilterTarget | undefined,
|
|
tenants: T[],
|
|
) {
|
|
return isGPDTDCTenant(target, tenants)
|
|
? [...GPDTDC_GRADE_OPTIONS]
|
|
: [...HANMAC_FAMILY_GRADE_OPTIONS];
|
|
}
|
|
|
|
function isPublicRepresentativeTenant(tenant: TenantFilterTarget) {
|
|
const visibility = String(
|
|
tenant.visibility ?? tenant.config?.visibility ?? "public",
|
|
)
|
|
.trim()
|
|
.toLowerCase();
|
|
return visibility !== "internal" && visibility !== "private";
|
|
}
|
|
|
|
function isInTenantSubtree<T extends TenantFilterTarget>(
|
|
tenant: T,
|
|
rootTenantId: string,
|
|
tenantById: Map<string, T>,
|
|
) {
|
|
if (!rootTenantId) {
|
|
return false;
|
|
}
|
|
if (tenant.id === rootTenantId) {
|
|
return true;
|
|
}
|
|
|
|
const visitedTenantIds = new Set<string>();
|
|
let parentId = tenant.parentId ?? "";
|
|
while (parentId) {
|
|
if (parentId === rootTenantId) {
|
|
return true;
|
|
}
|
|
if (visitedTenantIds.has(parentId)) {
|
|
return false;
|
|
}
|
|
visitedTenantIds.add(parentId);
|
|
parentId = tenantById.get(parentId)?.parentId ?? "";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function resolveHanmacFamilyTenantId<T extends TenantFilterTarget>(
|
|
tenants: T[],
|
|
hanmacFamilyTenantId?: string,
|
|
) {
|
|
const envTenantId = hanmacFamilyTenantId?.trim();
|
|
if (envTenantId) return envTenantId;
|
|
|
|
return (
|
|
tenants.find((tenant) => tenant.slug?.toLowerCase() === "hanmac-family")
|
|
?.id ?? ""
|
|
);
|
|
}
|
|
|
|
export function isHanmacFamilyTenant<T extends TenantFilterTarget>(
|
|
tenant: T | undefined,
|
|
tenants: T[],
|
|
hanmacFamilyTenantId?: string,
|
|
) {
|
|
if (!tenant?.id) return false;
|
|
|
|
const rootTenantId = resolveHanmacFamilyTenantId(
|
|
tenants,
|
|
hanmacFamilyTenantId,
|
|
);
|
|
if (!rootTenantId) return false;
|
|
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((item) => item.id?.trim())
|
|
.map((item) => [item.id as string, item]),
|
|
);
|
|
const target = tenantById.get(tenant.id) ?? tenant;
|
|
|
|
return isInTenantSubtree(target, rootTenantId, tenantById);
|
|
}
|
|
|
|
export function isHanmacFamilyUser<T extends TenantFilterTarget>(
|
|
user: HanmacFamilyUserTarget,
|
|
tenants: T[],
|
|
hanmacFamilyTenantId?: string,
|
|
) {
|
|
const metadataAppointments = Array.isArray(
|
|
user.metadata?.additionalAppointments,
|
|
)
|
|
? user.metadata.additionalAppointments
|
|
.map((appointment) => appointment as TenantFilterTarget)
|
|
.filter(
|
|
(appointment) =>
|
|
typeof appointment.tenantId === "string" ||
|
|
typeof appointment.id === "string" ||
|
|
typeof appointment.tenantSlug === "string" ||
|
|
typeof appointment.slug === "string",
|
|
)
|
|
.map((appointment) => ({
|
|
id: appointment.id ?? appointment.tenantId,
|
|
slug: appointment.slug ?? appointment.tenantSlug,
|
|
parentId: appointment.parentId,
|
|
type: appointment.type,
|
|
name: appointment.name ?? appointment.tenantName,
|
|
}))
|
|
: [];
|
|
const tenantBySlug = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.slug?.trim())
|
|
.map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]),
|
|
);
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.id?.trim())
|
|
.map((tenant) => [tenant.id as string, tenant]),
|
|
);
|
|
const tenantCandidates = [
|
|
user.tenant,
|
|
...(user.joinedTenants ?? []),
|
|
...metadataAppointments,
|
|
...metadataAppointments.map((appointment) =>
|
|
tenantById.get(appointment.id ?? ""),
|
|
),
|
|
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
|
|
];
|
|
|
|
return tenantCandidates.some((tenant) =>
|
|
isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId),
|
|
);
|
|
}
|
|
|
|
export function filterNonHanmacFamilyTenants<T extends TenantFilterTarget>(
|
|
tenants: T[],
|
|
hanmacFamilyTenantId?: string,
|
|
) {
|
|
const rootTenantId = resolveHanmacFamilyTenantId(
|
|
tenants,
|
|
hanmacFamilyTenantId,
|
|
);
|
|
const tenantById = new Map(
|
|
tenants
|
|
.filter((tenant) => tenant.id?.trim())
|
|
.map((tenant) => [tenant.id as string, tenant]),
|
|
);
|
|
|
|
return tenants.filter(
|
|
(tenant) =>
|
|
!isSystemTenant(tenant) &&
|
|
isPublicRepresentativeTenant(tenant) &&
|
|
!isInTenantSubtree(tenant, rootTenantId, tenantById),
|
|
);
|
|
}
|
|
|
|
export function buildOrgChartTenantPickerUrl(
|
|
baseUrl?: string,
|
|
options: OrgChartTenantPickerOptions = {},
|
|
) {
|
|
const normalizedBase = (baseUrl ?? "").replace(/\/+$/, "");
|
|
const params = new URLSearchParams({
|
|
mode: "single",
|
|
select: "tenant",
|
|
width: "400",
|
|
height: "600",
|
|
});
|
|
const tenantId = options.tenantId?.trim();
|
|
if (tenantId) {
|
|
params.set("tenantId", tenantId);
|
|
}
|
|
if (options.includeInternal) {
|
|
params.set("includeInternal", "true");
|
|
}
|
|
|
|
return `${normalizedBase}/embed/picker?${params.toString()}`;
|
|
}
|
|
|
|
export function buildAuthenticatedOrgChartTenantPickerUrl(
|
|
baseUrl?: string,
|
|
options: OrgChartTenantPickerOptions = {},
|
|
) {
|
|
const pickerUrl = buildOrgChartTenantPickerUrl("", {
|
|
includeInternal: true,
|
|
...options,
|
|
});
|
|
return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl });
|
|
}
|
|
|
|
export function buildOrgChartUserMultiPickerUrl(
|
|
baseUrl?: string,
|
|
options: OrgChartUserMultiPickerOptions = {},
|
|
) {
|
|
const normalizedBase = (baseUrl ?? "").replace(/\/+$/, "");
|
|
const params = new URLSearchParams({
|
|
mode: "multiple",
|
|
select: "user",
|
|
width: "720",
|
|
height: "640",
|
|
});
|
|
params.set("includeInternal", "true");
|
|
params.set("includeDescendants", "true");
|
|
params.set("showDescendantToggle", "true");
|
|
if (options.tenantId?.trim()) {
|
|
params.set("tenantId", options.tenantId.trim());
|
|
} else {
|
|
params.set("rootTenantId", "all");
|
|
}
|
|
|
|
return `${normalizedBase}/embed/picker?${params.toString()}`;
|
|
}
|
|
|
|
export function buildAuthenticatedOrgChartUserMultiPickerUrl(
|
|
baseUrl?: string,
|
|
options: OrgChartUserMultiPickerOptions = {},
|
|
) {
|
|
const pickerUrl = buildOrgChartUserMultiPickerUrl("", options);
|
|
return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl });
|
|
}
|
|
|
|
export function buildAuthenticatedOrgChartUrl(
|
|
baseUrl?: string,
|
|
options: OrgChartLoginOptions = { includeInternal: false },
|
|
) {
|
|
const normalizedBase =
|
|
baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_ORGFRONT_BASE_URL;
|
|
let returnTo = options.returnTo?.trim() || "/chart";
|
|
if (options.includeInternal && returnTo.startsWith("/chart")) {
|
|
const [path, query = ""] = returnTo.split("?", 2);
|
|
const params = new URLSearchParams(query);
|
|
params.set("includeInternal", "true");
|
|
returnTo = `${path}?${params.toString()}`;
|
|
}
|
|
const params = new URLSearchParams({
|
|
auto: "1",
|
|
returnTo,
|
|
});
|
|
|
|
return `${normalizedBase}/login?${params.toString()}`;
|
|
}
|
|
|
|
export function parseOrgChartTenantSelection(
|
|
message: unknown,
|
|
): OrgChartTenantSelection | null {
|
|
const data = message as OrgChartPickerMessage;
|
|
if (data?.type !== "orgfront:picker:confirm") {
|
|
return null;
|
|
}
|
|
|
|
const selection = data.payload?.selections?.[0];
|
|
if (
|
|
selection?.type !== "tenant" ||
|
|
typeof selection.id !== "string" ||
|
|
typeof selection.name !== "string" ||
|
|
selection.id.trim() === ""
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: selection.id,
|
|
name: selection.name,
|
|
};
|
|
}
|
|
|
|
export function parseOrgChartUserSelections(
|
|
message: unknown,
|
|
): OrgChartUserSelection[] {
|
|
const data = message as OrgChartPickerMessage;
|
|
if (data?.type !== "orgfront:picker:confirm") {
|
|
return [];
|
|
}
|
|
|
|
return (data.payload?.selections ?? [])
|
|
.filter(
|
|
(
|
|
selection,
|
|
): selection is {
|
|
type: "user";
|
|
id: string;
|
|
name: string;
|
|
email?: string;
|
|
rootTenantName?: string;
|
|
leafTenantName?: string;
|
|
tenantName?: string;
|
|
} =>
|
|
selection?.type === "user" &&
|
|
typeof selection.id === "string" &&
|
|
typeof selection.name === "string" &&
|
|
selection.id.trim() !== "",
|
|
)
|
|
.map((selection) => ({
|
|
id: selection.id,
|
|
name: selection.name,
|
|
email: typeof selection.email === "string" ? selection.email : "",
|
|
rootTenantName:
|
|
typeof selection.rootTenantName === "string"
|
|
? selection.rootTenantName
|
|
: undefined,
|
|
leafTenantName:
|
|
typeof selection.leafTenantName === "string"
|
|
? selection.leafTenantName
|
|
: typeof selection.tenantName === "string"
|
|
? selection.tenantName
|
|
: undefined,
|
|
}));
|
|
}
|