forked from baron/baron-sso
orgfront refresh token 관리 추가
This commit is contained in:
@@ -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: "" };
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ export type OrgPickerSelection = {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
rootTenantName?: string;
|
||||
leafTenantName?: string;
|
||||
};
|
||||
|
||||
export type OrgPickerResult = {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Reference in New Issue
Block a user