1
0
forked from baron/baron-sso

orgfront refresh token 관리 추가

This commit is contained in:
2026-06-18 08:00:57 +09:00
parent 5f3167a503
commit 33249eb229
32 changed files with 867 additions and 337 deletions

View File

@@ -134,6 +134,28 @@ export function buildOrgPickerTree({
usersBySlug.set(slug, list);
}
const exposeAllRoots = rootTenantId?.trim().toLowerCase() === "all";
const tree = buildTenantFullTree(visibleTenants);
if (exposeAllRoots) {
const rootNodes = tree.subTree.filter((node) => node.type !== "USER_GROUP");
const companies = rootNodes.flatMap((root) =>
orderHanmacFamilyChildren(root, root.children).filter(
(node) => node.type === "COMPANY",
),
);
return {
roots: rootNodes.map((node) => tenantToPickerNode(node, usersBySlug)),
companies: companies.map((company) => ({
id: company.id,
name: company.name,
companyGroupTenantId: getCompanyGroupId(company, tenants),
})),
companyGroupId: "all",
};
}
const companyGroup =
findTenantByRef(visibleTenants, rootTenantId) ??
visibleTenants.find(isHanmacFamilyCompanyGroup) ??
@@ -144,10 +166,7 @@ export function buildOrgPickerTree({
const { currentBase } = buildTenantFullTree(visibleTenants, companyGroup.id);
const groupNode =
currentBase ??
buildTenantFullTree(visibleTenants).subTree.find(
(node) => node.id === companyGroup.id,
);
currentBase ?? tree.subTree.find((node) => node.id === companyGroup.id);
if (!groupNode) return { roots: [], companies: [], companyGroupId: "" };

View File

@@ -9,6 +9,8 @@ export type OrgPickerSelection = {
id: string;
name: string;
email?: string;
rootTenantName?: string;
leafTenantName?: string;
};
export type OrgPickerResult = {

View File

@@ -40,13 +40,24 @@ function canToggleNode(
);
}
function toSelection(node: OrgPickerTreeNode): OrgPickerSelection {
function toSelection(
node: OrgPickerTreeNode,
ancestors: OrgPickerTreeNode[] = [],
): OrgPickerSelection {
if (node.type === "user") {
const tenantAncestors = ancestors.filter(
(ancestor) => ancestor.type === "tenant",
);
const rootTenant = tenantAncestors[0];
const leafTenant = tenantAncestors[tenantAncestors.length - 1];
return {
type: node.type,
id: node.id,
name: node.name,
email: node.user?.email,
rootTenantName: rootTenant?.name,
leafTenantName: leafTenant?.name,
};
}
@@ -68,27 +79,49 @@ function collectSelectedNodes({
includeDescendants: boolean;
select: OrgPickerSelectableType;
}) {
const selected = new Map<string, OrgPickerTreeNode>();
const visit = (node: OrgPickerTreeNode) => {
const selected = new Map<
string,
{ node: OrgPickerTreeNode; ancestors: OrgPickerTreeNode[] }
>();
const addNode = (node: OrgPickerTreeNode, ancestors: OrgPickerTreeNode[]) => {
if (canSelectNode(node, select)) {
selected.set(nodeKey(node), { node, ancestors });
}
};
const addDescendants = (
node: OrgPickerTreeNode,
ancestors: OrgPickerTreeNode[],
) => {
const visitDescendant = (
descendant: OrgPickerTreeNode,
descendantAncestors: OrgPickerTreeNode[],
) => {
addNode(descendant, descendantAncestors);
for (const child of descendant.children) {
visitDescendant(child, [...descendantAncestors, descendant]);
}
};
for (const child of node.children) {
visitDescendant(child, [...ancestors, node]);
}
};
const visit = (node: OrgPickerTreeNode, ancestors: OrgPickerTreeNode[]) => {
const key = nodeKey(node);
if (selectedKeys.has(key)) {
if (canSelectNode(node, select)) {
selected.set(key, node);
}
addNode(node, ancestors);
if (includeDescendants && node.type === "tenant") {
for (const descendant of flattenDescendants(node)) {
if (canSelectNode(descendant, select)) {
selected.set(nodeKey(descendant), descendant);
}
}
addDescendants(node, ancestors);
}
}
for (const child of node.children) visit(child);
for (const child of node.children) visit(child, [...ancestors, node]);
};
for (const root of roots) visit(root);
return Array.from(selected.values()).map(toSelection);
for (const root of roots) visit(root, []);
return Array.from(selected.values()).map(({ node, ancestors }) =>
toSelection(node, ancestors),
);
}
function collectCheckedKeys({

View File

@@ -1,17 +1,17 @@
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import type { AuthProviderProps } from "react-oidc-context";
import { buildCommonUserManagerSettings } from "../../../common/core/auth";
import {
buildCommonOidcRuntimeConfig,
buildCommonUserManagerSettings,
} from "../../../common/core/auth";
import { resolveOrgFrontPublicOrigin } from "./authConfig";
buildOrgFrontOidcRuntimeConfig,
resolveOrgFrontPublicOrigin,
} from "./authConfig";
const orgFrontPublicOrigin = resolveOrgFrontPublicOrigin(
import.meta.env.VITE_ORGFRONT_PUBLIC_URL,
window.location.origin,
);
export const oidcConfig: AuthProviderProps = buildCommonOidcRuntimeConfig({
export const oidcConfig: AuthProviderProps = buildOrgFrontOidcRuntimeConfig({
authority:
import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc",
clientId: import.meta.env.VITE_OIDC_CLIENT_ID || "orgfront",

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
buildOrgFrontAuthRedirectUris,
buildOrgFrontOidcRuntimeConfig,
ORGFRONT_AUTH_CALLBACK_PATH,
resolveOrgFrontPublicOrigin,
} from "./authConfig";
@@ -26,4 +27,18 @@ describe("orgfront auth config", () => {
it("keeps the callback path aligned with the registered redirect path", () => {
expect(ORGFRONT_AUTH_CALLBACK_PATH).toBe("/auth/callback");
});
it("requests offline access and enables refresh-token based renewal", () => {
const config = buildOrgFrontOidcRuntimeConfig({
authority: "https://sso.hmac.kr/oidc",
clientId: "orgfront",
origin: "https://org.hmac.kr",
userStore: { kind: "test-store" },
});
expect(config.scope.split(/\s+/)).toEqual(
expect.arrayContaining(["openid", "offline_access", "profile", "email"]),
);
expect(config.automaticSilentRenew).toBe(true);
});
});

View File

@@ -1,3 +1,8 @@
import {
buildCommonOidcRuntimeConfig,
type CommonOidcConfigOptions,
} from "../../../common/core/auth";
export interface OrgFrontAuthRedirectUris {
redirectUri: string;
postLogoutRedirectUri: string;
@@ -31,3 +36,12 @@ export function buildOrgFrontAuthRedirectUris(
popupRedirectUri: `${publicOrigin}${ORGFRONT_AUTH_CALLBACK_PATH}`,
};
}
export function buildOrgFrontOidcRuntimeConfig<TUserStore>(
options: Omit<CommonOidcConfigOptions<TUserStore>, "automaticSilentRenew">,
) {
return buildCommonOidcRuntimeConfig({
...options,
automaticSilentRenew: true,
});
}

View File

@@ -43,6 +43,9 @@ function user(id: string, name: string, companyCode: string) {
status: "active",
companyCode,
grade: "사원",
metadata: {
additionalAppointments: [{ tenantSlug: companyCode }],
},
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
};
@@ -338,7 +341,8 @@ test("org chart renders dense member nodes with calculated member columns", asyn
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
const rootNode = page.locator('[data-testid="orgchart-node-root"]');
await expect(rootNode).toHaveAttribute("width", /3\d{2}/);
await expect(rootNode).toHaveAttribute("width", /\d+/);
expect(Number(await rootNode.getAttribute("width"))).toBeGreaterThan(240);
await expect(rootNode.locator('[data-member-columns="2"]')).toBeVisible();
await expect(rootNode.getByText("Dense User 10")).toBeVisible();
});

View File

@@ -46,6 +46,7 @@ test("orgfront login waits for explicit auto parameter", async ({ page }) => {
test("orgfront login auto parameter starts OIDC authorization", async ({
page,
baseURL,
}) => {
const oidc = await stubOidcAuthorization(page);
@@ -55,11 +56,15 @@ test("orgfront login auto parameter starts OIDC authorization", async ({
const parsed = new URL(oidc.authorizationURL());
expect(parsed.searchParams.get("client_id")).toBe("orgfront");
expect(parsed.searchParams.get("redirect_uri")).toBe(
"http://127.0.0.1:4175/auth/callback",
);
const redirectUri = new URL(parsed.searchParams.get("redirect_uri") ?? "");
const appUrl = new URL(baseURL ?? page.url());
expect(["localhost", "127.0.0.1"]).toContain(redirectUri.hostname);
expect(redirectUri.port).toBe(appUrl.port);
expect(redirectUri.pathname).toBe("/auth/callback");
expect(parsed.searchParams.get("response_type")).toBe("code");
expect(parsed.searchParams.get("scope") ?? "").toContain("openid");
expect((parsed.searchParams.get("scope") ?? "").split(/\s+/)).toEqual(
expect.arrayContaining(["openid", "offline_access", "profile", "email"]),
);
});
test("orgfront login can opt out of default OIDC authorization", async ({