1
0
forked from baron/baron-sso

개발자 권한 접근 로직 공통화

This commit is contained in:
2026-05-29 10:16:15 +09:00
parent b4dfbe0480
commit 2c93bd8dfb
4 changed files with 161 additions and 38 deletions

View File

@@ -10,6 +10,7 @@ import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
@@ -20,7 +21,6 @@ import {
CardTitle,
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import { fetchDeveloperRequestStatus } from "../../lib/devApi";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n";
@@ -92,22 +92,17 @@ function AuditLogsPage() {
enabled: hasAccessToken,
});
const profileRole = me?.role?.trim() || role;
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
queryKey: ["developer-request", tenantId],
queryFn: () => fetchDeveloperRequestStatus(tenantId),
enabled: hasAccessToken && profileRole === "user",
const {
hasDeveloperAccess,
isDeveloperRequestPending,
canRequestDeveloperAccess,
isLoadingDeveloperAccessGate,
} = useDeveloperAccessGate({
hasAccessToken,
profileRole,
tenantId,
isLoadingIdentity: isLoadingMe,
});
const hasDeveloperAccess =
profileRole === "super_admin" ||
profileRole === "tenant_admin" ||
profileRole === "rp_admin" ||
requestStatus?.status === "approved";
const isDeveloperRequestPending = requestStatus?.status === "pending";
const canRequestDeveloperAccess =
profileRole === "user" &&
!isLoadingRequestStatus &&
!hasDeveloperAccess &&
!isDeveloperRequestPending;
const query = useInfiniteQuery({
queryKey: [
@@ -138,10 +133,7 @@ function AuditLogsPage() {
downloadCsv(csv, `dev-audit-logs-${stamp}.csv`);
};
if (
profileRole === "user" &&
(isLoadingMe || isLoadingRequestStatus)
) {
if (isLoadingDeveloperAccessGate) {
return (
<div className="p-8 text-center">
{t("ui.common.loading", "Loading...")}

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
resolveDeveloperAccessGate,
shouldFetchDeveloperRequestStatus,
shouldShowDeveloperAccessLoading,
} from "./developerAccessGate";
describe("developer access gate", () => {
it("fetches request status only for user roles", () => {
expect(shouldFetchDeveloperRequestStatus("user")).toBe(true);
expect(shouldFetchDeveloperRequestStatus("tenant_admin")).toBe(false);
expect(shouldFetchDeveloperRequestStatus("rp_admin")).toBe(false);
});
it("resolves access and request states from the request status", () => {
expect(resolveDeveloperAccessGate("super_admin", "pending")).toEqual({
hasDeveloperAccess: true,
isDeveloperRequestPending: true,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "approved")).toEqual({
hasDeveloperAccess: true,
isDeveloperRequestPending: false,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "pending")).toEqual({
hasDeveloperAccess: false,
isDeveloperRequestPending: true,
canRequestDeveloperAccess: false,
});
expect(resolveDeveloperAccessGate("user", "none")).toEqual({
hasDeveloperAccess: false,
isDeveloperRequestPending: false,
canRequestDeveloperAccess: true,
});
});
it("shows the loading gate only for user requests", () => {
expect(shouldShowDeveloperAccessLoading("user", true, false)).toBe(true);
expect(shouldShowDeveloperAccessLoading("user", false, true)).toBe(true);
expect(shouldShowDeveloperAccessLoading("tenant_admin", true, true)).toBe(
false,
);
});
});

View File

@@ -0,0 +1,88 @@
import { useQuery } from "@tanstack/react-query";
import {
fetchDeveloperRequestStatus,
type DeveloperRequestStatus,
} from "../../lib/devApi";
export type DeveloperAccessGateState = {
hasDeveloperAccess: boolean;
isDeveloperRequestPending: boolean;
canRequestDeveloperAccess: boolean;
isLoadingDeveloperAccessGate: boolean;
};
function isPrivilegedDeveloperRole(profileRole: string) {
return (
profileRole === "super_admin" ||
profileRole === "tenant_admin" ||
profileRole === "rp_admin"
);
}
export function resolveDeveloperAccessGate(
profileRole: string,
requestStatus?: DeveloperRequestStatus,
): Omit<DeveloperAccessGateState, "isLoadingDeveloperAccessGate"> {
const hasDeveloperAccess =
isPrivilegedDeveloperRole(profileRole) || requestStatus === "approved";
const isDeveloperRequestPending = requestStatus === "pending";
const canRequestDeveloperAccess =
profileRole === "user" &&
!hasDeveloperAccess &&
!isDeveloperRequestPending;
return {
hasDeveloperAccess,
isDeveloperRequestPending,
canRequestDeveloperAccess,
};
}
export function shouldFetchDeveloperRequestStatus(profileRole: string) {
return profileRole === "user";
}
export function shouldShowDeveloperAccessLoading(
profileRole: string,
isLoadingIdentity: boolean,
isLoadingRequestStatus: boolean,
) {
return (
profileRole === "user" && (isLoadingIdentity || isLoadingRequestStatus)
);
}
export function useDeveloperAccessGate({
hasAccessToken,
profileRole,
tenantId,
isLoadingIdentity = false,
}: {
hasAccessToken: boolean;
profileRole: string;
tenantId?: string;
isLoadingIdentity?: boolean;
}) {
const shouldFetchRequestStatus = shouldFetchDeveloperRequestStatus(
profileRole,
);
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
queryKey: ["developer-request", tenantId],
queryFn: () => fetchDeveloperRequestStatus(tenantId),
enabled: hasAccessToken && shouldFetchRequestStatus,
});
const resolvedGate = resolveDeveloperAccessGate(
profileRole,
requestStatus?.status,
);
return {
...resolvedGate,
isLoadingDeveloperAccessGate: shouldShowDeveloperAccessLoading(
profileRole,
isLoadingIdentity,
isLoadingRequestStatus,
),
} satisfies DeveloperAccessGateState;
}

View File

@@ -17,6 +17,7 @@ import {
OverviewSelectionChips,
} from "../../../../common/core/components/overview";
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import {
type ClientSummary,
fetchClients,
@@ -491,11 +492,6 @@ function GlobalOverviewPage() {
const [period, setPeriod] = useState<RPUsagePeriod>("day");
const [selectedClientIds, setSelectedClientIds] = useState<string[]>([]);
const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90;
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
queryKey: ["developer-request", tenantId],
queryFn: () => fetchDeveloperRequestStatus(tenantId),
enabled: hasAccessToken && profileRole === "user",
});
const statsQuery = useQuery({
queryKey: ["dev-dashboard-stats"],
queryFn: fetchDevStats,
@@ -517,17 +513,17 @@ function GlobalOverviewPage() {
});
const clients = clientsQuery.data?.items ?? [];
const hasDeveloperAccess =
profileRole === "super_admin" ||
profileRole === "tenant_admin" ||
profileRole === "rp_admin" ||
requestStatus?.status === "approved";
const isDeveloperRequestPending = requestStatus?.status === "pending";
const canRequestDeveloperAccess =
profileRole === "user" &&
!isLoadingRequestStatus &&
!hasDeveloperAccess &&
!isDeveloperRequestPending;
const {
hasDeveloperAccess,
isDeveloperRequestPending,
canRequestDeveloperAccess,
isLoadingDeveloperAccessGate,
} = useDeveloperAccessGate({
hasAccessToken,
profileRole,
tenantId,
isLoadingIdentity: isLoadingMe,
});
const distribution = useMemo(
() => buildClientDistribution(clients),
[clients],
@@ -616,8 +612,7 @@ function GlobalOverviewPage() {
};
if (
profileRole === "user" &&
(isLoadingMe || isLoadingRequestStatus)
isLoadingDeveloperAccessGate
) {
return (
<div className="p-8 text-center">