forked from baron/baron-sso
개발자 권한 접근 로직 공통화
This commit is contained in:
@@ -10,6 +10,7 @@ import { PageHeader } from "../../../../common/core/components/page";
|
|||||||
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
|
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
|
||||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||||
|
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||||
import { Badge } from "../../components/ui/badge";
|
import { Badge } from "../../components/ui/badge";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
import { fetchDeveloperRequestStatus } from "../../lib/devApi";
|
|
||||||
import type { DevAuditLog } from "../../lib/devApi";
|
import type { DevAuditLog } from "../../lib/devApi";
|
||||||
import { fetchDevAuditLogs } from "../../lib/devApi";
|
import { fetchDevAuditLogs } from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
@@ -92,22 +92,17 @@ function AuditLogsPage() {
|
|||||||
enabled: hasAccessToken,
|
enabled: hasAccessToken,
|
||||||
});
|
});
|
||||||
const profileRole = me?.role?.trim() || role;
|
const profileRole = me?.role?.trim() || role;
|
||||||
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
|
const {
|
||||||
queryKey: ["developer-request", tenantId],
|
hasDeveloperAccess,
|
||||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
isDeveloperRequestPending,
|
||||||
enabled: hasAccessToken && profileRole === "user",
|
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({
|
const query = useInfiniteQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@@ -138,10 +133,7 @@ function AuditLogsPage() {
|
|||||||
downloadCsv(csv, `dev-audit-logs-${stamp}.csv`);
|
downloadCsv(csv, `dev-audit-logs-${stamp}.csv`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (isLoadingDeveloperAccessGate) {
|
||||||
profileRole === "user" &&
|
|
||||||
(isLoadingMe || isLoadingRequestStatus)
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
{t("ui.common.loading", "Loading...")}
|
{t("ui.common.loading", "Loading...")}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
OverviewSelectionChips,
|
OverviewSelectionChips,
|
||||||
} from "../../../../common/core/components/overview";
|
} from "../../../../common/core/components/overview";
|
||||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||||
|
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||||
import {
|
import {
|
||||||
type ClientSummary,
|
type ClientSummary,
|
||||||
fetchClients,
|
fetchClients,
|
||||||
@@ -491,11 +492,6 @@ function GlobalOverviewPage() {
|
|||||||
const [period, setPeriod] = useState<RPUsagePeriod>("day");
|
const [period, setPeriod] = useState<RPUsagePeriod>("day");
|
||||||
const [selectedClientIds, setSelectedClientIds] = useState<string[]>([]);
|
const [selectedClientIds, setSelectedClientIds] = useState<string[]>([]);
|
||||||
const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90;
|
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({
|
const statsQuery = useQuery({
|
||||||
queryKey: ["dev-dashboard-stats"],
|
queryKey: ["dev-dashboard-stats"],
|
||||||
queryFn: fetchDevStats,
|
queryFn: fetchDevStats,
|
||||||
@@ -517,17 +513,17 @@ function GlobalOverviewPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const clients = clientsQuery.data?.items ?? [];
|
const clients = clientsQuery.data?.items ?? [];
|
||||||
const hasDeveloperAccess =
|
const {
|
||||||
profileRole === "super_admin" ||
|
hasDeveloperAccess,
|
||||||
profileRole === "tenant_admin" ||
|
isDeveloperRequestPending,
|
||||||
profileRole === "rp_admin" ||
|
canRequestDeveloperAccess,
|
||||||
requestStatus?.status === "approved";
|
isLoadingDeveloperAccessGate,
|
||||||
const isDeveloperRequestPending = requestStatus?.status === "pending";
|
} = useDeveloperAccessGate({
|
||||||
const canRequestDeveloperAccess =
|
hasAccessToken,
|
||||||
profileRole === "user" &&
|
profileRole,
|
||||||
!isLoadingRequestStatus &&
|
tenantId,
|
||||||
!hasDeveloperAccess &&
|
isLoadingIdentity: isLoadingMe,
|
||||||
!isDeveloperRequestPending;
|
});
|
||||||
const distribution = useMemo(
|
const distribution = useMemo(
|
||||||
() => buildClientDistribution(clients),
|
() => buildClientDistribution(clients),
|
||||||
[clients],
|
[clients],
|
||||||
@@ -616,8 +612,7 @@ function GlobalOverviewPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
profileRole === "user" &&
|
isLoadingDeveloperAccessGate
|
||||||
(isLoadingMe || isLoadingRequestStatus)
|
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user