1
0
forked from baron/baron-sso

개발자 권한을 페이지별로 선택/부여 가능하도록 개선

This commit is contained in:
2026-06-09 16:47:20 +09:00
parent 3ed9e912e6
commit 437a3ad98d
18 changed files with 782 additions and 91 deletions

View File

@@ -12,25 +12,51 @@ describe("developer access gate", () => {
});
it("resolves access and request states from the request status", () => {
expect(resolveDeveloperAccessGate("super_admin", "pending")).toEqual({
expect(
resolveDeveloperAccessGate("super_admin", {
status: "pending",
pendingPages: ["overview"],
}),
).toEqual({
hasDeveloperAccess: true,
isDeveloperRequestPending: true,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "approved")).toEqual({
expect(
resolveDeveloperAccessGate("user", {
status: "approved",
approvedPages: ["overview"],
}),
).toEqual({
hasDeveloperAccess: true,
isDeveloperRequestPending: false,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "pending")).toEqual({
expect(
resolveDeveloperAccessGate("user", {
status: "pending",
pendingPages: ["audit"],
}, ["audit"]),
).toEqual({
hasDeveloperAccess: false,
isDeveloperRequestPending: true,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "none")).toEqual({
expect(
resolveDeveloperAccessGate("user", {
status: "approved",
approvedPages: ["overview"],
}, ["audit"]),
).toEqual({
hasDeveloperAccess: false,
isDeveloperRequestPending: false,
canRequestDeveloperAccess: true,
});
expect(resolveDeveloperAccessGate("user", { status: "none" })).toEqual({
hasDeveloperAccess: false,
isDeveloperRequestPending: false,
canRequestDeveloperAccess: true,

View File

@@ -1,8 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import {
type DeveloperRequestStatus,
type DeveloperAccessStatus,
fetchDeveloperRequestStatus,
} from "../../lib/devApi";
import {
hasDeveloperAccessForPages,
isDeveloperRequestPendingForPages,
type DeveloperAccessPage,
} from "./developerAccessPages";
export type DeveloperAccessGateState = {
hasDeveloperAccess: boolean;
@@ -14,16 +19,23 @@ export type DeveloperAccessGateState = {
export function resolveDeveloperAccessGate(
profileRole: string,
requestStatus?: DeveloperRequestStatus,
accessStatus?: DeveloperAccessStatus,
requiredPages: DeveloperAccessPage[] = ["overview"],
): Omit<
DeveloperAccessGateState,
"isLoadingDeveloperAccessGate" | "isTenantContextMissing"
> {
const hasDeveloperAccess =
profileRole === "super_admin" || requestStatus === "approved";
const isDeveloperRequestPending = requestStatus === "pending";
profileRole === "super_admin" ||
hasDeveloperAccessForPages(accessStatus?.approvedPages, requiredPages);
const isDeveloperRequestPending = isDeveloperRequestPendingForPages(
accessStatus?.pendingPages,
requiredPages,
);
const canRequestDeveloperAccess =
profileRole === "user" && !hasDeveloperAccess && !isDeveloperRequestPending;
profileRole === "user" &&
!hasDeveloperAccess &&
!isDeveloperRequestPending;
return {
hasDeveloperAccess,
@@ -50,11 +62,13 @@ export function useDeveloperAccessGate({
hasAccessToken,
profileRole,
tenantId,
requiredPages = ["overview"],
isLoadingIdentity = false,
}: {
hasAccessToken: boolean;
profileRole: string;
tenantId?: string;
requiredPages?: DeveloperAccessPage[];
isLoadingIdentity?: boolean;
}) {
const shouldFetchRequestStatus =
@@ -68,7 +82,8 @@ export function useDeveloperAccessGate({
const resolvedGate = resolveDeveloperAccessGate(
profileRole,
requestStatus?.status,
requestStatus,
requiredPages,
);
return {

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { normalizeDeveloperAccessPageSelection } from "./developerAccessPages";
describe("developer access pages", () => {
it("collapses all non-all pages into all", () => {
expect(
normalizeDeveloperAccessPageSelection([
"overview",
"client_create",
"audit",
]),
).toEqual(["all"]);
});
it("keeps partial selections as-is", () => {
expect(
normalizeDeveloperAccessPageSelection(["overview", "audit"]),
).toEqual(["overview", "audit"]);
});
it("keeps explicit all selection", () => {
expect(normalizeDeveloperAccessPageSelection(["all"])).toEqual(["all"]);
});
});

View File

@@ -0,0 +1,104 @@
export type DeveloperAccessPage =
| "all"
| "overview"
| "client_create"
| "audit";
export const developerAccessPageOrder: DeveloperAccessPage[] = [
"overview",
"client_create",
"audit",
];
export const developerAccessPageOptions: Array<{
value: DeveloperAccessPage;
label: string;
}> = [
{ value: "all", label: "전체" },
{ value: "overview", label: "개요" },
{ value: "client_create", label: "연동 앱 추가" },
{ value: "audit", label: "감사로그" },
];
export function normalizeDeveloperAccessPages(
pages: Array<string | undefined | null>,
): DeveloperAccessPage[] {
const normalized = new Set<DeveloperAccessPage>();
for (const raw of pages) {
const page = String(raw ?? "").trim().toLowerCase();
if (!page) {
continue;
}
if (page === "all") {
return ["all"];
}
if (
page === "overview" ||
page === "client_create" ||
page === "audit"
) {
normalized.add(page);
}
}
return [...developerAccessPageOrder.filter((page) => normalized.has(page))];
}
export function normalizeDeveloperAccessPageSelection(
pages: DeveloperAccessPage[],
): DeveloperAccessPage[] {
if (pages.includes("all")) {
return ["all"];
}
const normalized = normalizeDeveloperAccessPages(pages);
if (normalized.length === 0) {
return ["all"];
}
if (normalized.length === developerAccessPageOrder.length) {
return ["all"];
}
return normalized;
}
export function developerAccessPagesToLabel(pages?: Array<string | null>) {
const normalized = normalizeDeveloperAccessPages(pages ?? []);
if (normalized.length === 0 || normalized.includes("all")) {
return "전체";
}
return normalized
.map((page) => {
switch (page) {
case "overview":
return "개요";
case "client_create":
return "연동 앱 추가";
case "audit":
return "감사로그";
default:
return page;
}
})
.join(", ");
}
export function hasDeveloperAccessForPages(
grantedPages: Array<string | null> | undefined,
requiredPages: DeveloperAccessPage[],
) {
const normalized = normalizeDeveloperAccessPages(grantedPages ?? []);
if (normalized.includes("all")) {
return true;
}
return requiredPages.some((page) => normalized.includes(page));
}
export function isDeveloperRequestPendingForPages(
pendingPages: Array<string | null> | undefined,
requiredPages: DeveloperAccessPage[],
) {
const normalized = normalizeDeveloperAccessPages(pendingPages ?? []);
if (normalized.includes("all")) {
return true;
}
return requiredPages.some((page) => normalized.includes(page));
}