forked from baron/baron-sso
개발자 권한을 페이지별로 선택/부여 가능하도록 개선
This commit is contained in:
@@ -30,6 +30,7 @@ import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import type {
|
||||
ClientStatus,
|
||||
@@ -54,6 +55,7 @@ import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { fetchMe, type UserProfile } from "../auth/authApi";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
|
||||
|
||||
@@ -358,16 +360,27 @@ function ClientGeneralPage() {
|
||||
const hasAccessToken = Boolean(auth.user?.access_token);
|
||||
const clientId = params.id;
|
||||
const isCreate = !clientId;
|
||||
const systemRole = resolveProfileRole(
|
||||
auth.user?.profile as Record<string, unknown> | undefined,
|
||||
);
|
||||
const { data: me } = useQuery<UserProfile>({
|
||||
const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
|
||||
const systemRole = resolveProfileRole(userProfile);
|
||||
const { data: me, isLoading: isLoadingMe } = useQuery<UserProfile>({
|
||||
queryKey: ["userMe"],
|
||||
queryFn: fetchMe,
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
const currentUserId = me?.id ?? auth.user?.profile.sub;
|
||||
const effectiveSystemRole = me?.role?.trim() || systemRole;
|
||||
const {
|
||||
hasDeveloperAccess: hasClientCreateAccess,
|
||||
isDeveloperRequestPending,
|
||||
canRequestDeveloperAccess,
|
||||
isLoadingDeveloperAccessGate,
|
||||
} = useDeveloperAccessGate({
|
||||
hasAccessToken,
|
||||
profileRole: effectiveSystemRole,
|
||||
tenantId: userProfile?.tenant_id as string | undefined,
|
||||
requiredPages: ["client_create"],
|
||||
isLoadingIdentity: isLoadingMe,
|
||||
});
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["client", clientId],
|
||||
queryFn: () => fetchClient(clientId as string),
|
||||
@@ -1161,10 +1174,44 @@ function ClientGeneralPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!isCreate && isLoading) {
|
||||
if ((isCreate && isLoadingDeveloperAccessGate) || (!isCreate && isLoading)) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
{t("msg.dev.clients.general.loading", "Loading client...")}
|
||||
{t(
|
||||
"msg.dev.clients.general.loading",
|
||||
isCreate ? "Loading client creation..." : "Loading client...",
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isCreate && !hasClientCreateAccess) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<DeveloperAccessRequestCard
|
||||
title={t("ui.dev.clients.general.title_create", "Create Client")}
|
||||
isPending={isDeveloperRequestPending}
|
||||
canRequest={canRequestDeveloperAccess}
|
||||
pendingMessage={t(
|
||||
"msg.dev.clients.general.create_pending",
|
||||
"개발자 권한 신청을 검토 중입니다.",
|
||||
)}
|
||||
deniedMessage={t(
|
||||
"msg.dev.clients.general.create_forbidden",
|
||||
"이 RP를 생성할 권한이 없습니다.",
|
||||
)}
|
||||
pendingDetailMessage={t(
|
||||
"msg.dev.clients.general.create_pending_detail",
|
||||
"super admin이 승인하면 연동 앱을 추가할 수 있습니다.",
|
||||
)}
|
||||
deniedDetailMessage={t(
|
||||
"msg.dev.clients.general.create_forbidden_detail",
|
||||
"개발자 권한 신청에서 연동 앱 추가 권한을 선택한 뒤 승인받아주세요.",
|
||||
)}
|
||||
actionLabel={t("ui.dev.welcome.btn_request", "개발자 등록 신청하기")}
|
||||
onAction={() => navigate("/developer-requests")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
toggleSort,
|
||||
} from "../../../../common/core/utils";
|
||||
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
|
||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||
import {
|
||||
commonTableShellClass,
|
||||
commonTableViewportClass,
|
||||
@@ -53,6 +54,7 @@ import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import { resolveClientCreateAccess } from "./clientCreateAccess";
|
||||
import { ClientLogo } from "./components/ClientLogo";
|
||||
|
||||
@@ -101,13 +103,26 @@ function ClientsPage() {
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
|
||||
const {
|
||||
hasDeveloperAccess: hasClientsPageAccess,
|
||||
isDeveloperRequestPending,
|
||||
canRequestDeveloperAccess,
|
||||
isLoadingDeveloperAccessGate,
|
||||
} = useDeveloperAccessGate({
|
||||
hasAccessToken,
|
||||
profileRole,
|
||||
tenantId,
|
||||
requiredPages: ["client_create"],
|
||||
isLoadingIdentity: isLoadingMe,
|
||||
});
|
||||
|
||||
const createAccessState = resolveClientCreateAccess({
|
||||
role: profileRole,
|
||||
requestStatus: requestStatus?.status,
|
||||
accessStatus: requestStatus,
|
||||
});
|
||||
const canCreateClient = createAccessState === "can_create";
|
||||
const isDeveloperRequestPending = createAccessState === "pending";
|
||||
const canRequestDeveloperAccess =
|
||||
const isClientCreatePending = createAccessState === "pending";
|
||||
const canRequestClientCreateAccess =
|
||||
createAccessState === "request_required" && !isLoadingRequest;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -189,6 +204,7 @@ function ClientsPage() {
|
||||
const isLoading =
|
||||
isLoadingClients ||
|
||||
isLoadingRequest ||
|
||||
isLoadingDeveloperAccessGate ||
|
||||
(hasAccessToken && !profileRole && isLoadingMe);
|
||||
|
||||
const requestSort = (key: ClientSortKey) => {
|
||||
@@ -203,6 +219,38 @@ function ClientsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasClientsPageAccess) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<DeveloperAccessRequestCard
|
||||
title={t("ui.dev.clients.registry.subtitle", "연동 앱")}
|
||||
isPending={isDeveloperRequestPending}
|
||||
canRequest={canRequestDeveloperAccess}
|
||||
pendingMessage={t(
|
||||
"msg.dev.dashboard.access_pending",
|
||||
"개발자 권한 신청을 검토 중입니다.",
|
||||
)}
|
||||
deniedMessage={t(
|
||||
"msg.dev.clients.access_denied",
|
||||
"연동 앱 페이지에 접근할 권한이 없습니다.",
|
||||
)}
|
||||
pendingDetailMessage={t(
|
||||
"msg.dev.dashboard.access_pending_detail",
|
||||
"super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
|
||||
)}
|
||||
deniedDetailMessage={t(
|
||||
"msg.dev.clients.access_denied_detail",
|
||||
"개발자 권한 신청에서 개요 또는 연동 앱 추가 권한을 선택한 뒤 승인받아주세요.",
|
||||
)}
|
||||
actionLabel={t("ui.dev.welcome.btn_request", "개발자 등록 신청하기")}
|
||||
onAction={() => navigate("/developer-requests")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (clientError) {
|
||||
const axiosError = clientError as AxiosError<{ error?: string }>;
|
||||
if (axiosError.response?.status === 403) {
|
||||
@@ -255,7 +303,7 @@ function ClientsPage() {
|
||||
{t("ui.dev.nav.developer_request", "개발자 권한 신청")}
|
||||
</Button>
|
||||
</div>
|
||||
) : canRequestDeveloperAccess ? (
|
||||
) : canRequestClientCreateAccess ? (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<p className="max-w-xs whitespace-pre-line text-right text-sm text-muted-foreground">
|
||||
{t(
|
||||
@@ -458,11 +506,11 @@ function ClientsPage() {
|
||||
"msg.dev.clients.empty_can_create",
|
||||
"아직 등록된 연동 앱이 없습니다.",
|
||||
)
|
||||
: isDeveloperRequestPending
|
||||
? t(
|
||||
"msg.dev.clients.empty_pending",
|
||||
"개발자 권한 신청을 검토 중입니다.",
|
||||
)
|
||||
: isClientCreatePending
|
||||
? t(
|
||||
"msg.dev.clients.empty_pending",
|
||||
"개발자 권한 신청을 검토 중입니다.",
|
||||
)
|
||||
: t(
|
||||
"msg.dev.clients.empty",
|
||||
"조회 가능한 RP가 없습니다.",
|
||||
@@ -480,7 +528,7 @@ function ClientsPage() {
|
||||
"msg.dev.clients.empty_can_create_detail",
|
||||
"연동 앱 추가 버튼으로 새 RP를 생성하면 이 목록에 표시됩니다.",
|
||||
)
|
||||
: isDeveloperRequestPending
|
||||
: isClientCreatePending
|
||||
? t(
|
||||
"msg.dev.clients.empty_pending_detail",
|
||||
"super admin이 승인하면 연동 앱을 추가할 수 있습니다.",
|
||||
@@ -499,7 +547,7 @@ function ClientsPage() {
|
||||
{t("ui.dev.clients.new", "연동 앱 추가")}
|
||||
</button>
|
||||
)}
|
||||
{!isFilteredOut && canRequestDeveloperAccess && (
|
||||
{!isFilteredOut && canRequestClientCreateAccess && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary font-bold hover:underline"
|
||||
@@ -693,6 +741,7 @@ function RequestAccessModal({
|
||||
organization,
|
||||
reason,
|
||||
tenantId,
|
||||
accessPages: ["all"],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("client create access", () => {
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "user",
|
||||
requestStatus: "none",
|
||||
accessStatus: { status: "none" },
|
||||
}),
|
||||
).toBe("request_required");
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe("client create access", () => {
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "",
|
||||
requestStatus: undefined,
|
||||
accessStatus: undefined,
|
||||
}),
|
||||
).toBe("request_required");
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("client create access", () => {
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "user",
|
||||
requestStatus: "pending",
|
||||
accessStatus: { status: "pending", pendingPages: ["client_create"] },
|
||||
}),
|
||||
).toBe("pending");
|
||||
});
|
||||
@@ -41,7 +41,10 @@ describe("client create access", () => {
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "user",
|
||||
requestStatus: "approved",
|
||||
accessStatus: {
|
||||
status: "approved",
|
||||
approvedPages: ["client_create"],
|
||||
},
|
||||
}),
|
||||
).toBe("can_create");
|
||||
});
|
||||
@@ -50,14 +53,14 @@ describe("client create access", () => {
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "user",
|
||||
requestStatus: "cancelled",
|
||||
accessStatus: { status: "cancelled" },
|
||||
}),
|
||||
).toBe("request_required");
|
||||
|
||||
expect(
|
||||
resolveClientCreateAccess({
|
||||
role: "user",
|
||||
requestStatus: "rejected",
|
||||
accessStatus: { status: "rejected" },
|
||||
}),
|
||||
).toBe("request_required");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { DeveloperRequestStatus } from "../../lib/devApi";
|
||||
import type { DeveloperAccessStatus } from "../../lib/devApi";
|
||||
import {
|
||||
hasDeveloperAccessForPages,
|
||||
isDeveloperRequestPendingForPages,
|
||||
} from "../developer-access/developerAccessPages";
|
||||
|
||||
export type ClientCreateAccessState =
|
||||
| "can_create"
|
||||
@@ -8,7 +12,7 @@ export type ClientCreateAccessState =
|
||||
|
||||
type ResolveClientCreateAccessParams = {
|
||||
role: string;
|
||||
requestStatus?: DeveloperRequestStatus;
|
||||
accessStatus?: DeveloperAccessStatus;
|
||||
};
|
||||
|
||||
function canSelfRequestDeveloperAccess(role: string) {
|
||||
@@ -17,7 +21,7 @@ function canSelfRequestDeveloperAccess(role: string) {
|
||||
|
||||
export function resolveClientCreateAccess({
|
||||
role,
|
||||
requestStatus,
|
||||
accessStatus,
|
||||
}: ResolveClientCreateAccessParams): ClientCreateAccessState {
|
||||
if (!role.trim()) {
|
||||
return "request_required";
|
||||
@@ -27,22 +31,17 @@ export function resolveClientCreateAccess({
|
||||
return "can_create";
|
||||
}
|
||||
|
||||
if (requestStatus === "approved") {
|
||||
if (hasDeveloperAccessForPages(accessStatus?.approvedPages, ["client_create"])) {
|
||||
return "can_create";
|
||||
}
|
||||
|
||||
if (requestStatus === "pending") {
|
||||
if (
|
||||
isDeveloperRequestPendingForPages(accessStatus?.pendingPages, [
|
||||
"client_create",
|
||||
])
|
||||
) {
|
||||
return "pending";
|
||||
}
|
||||
|
||||
if (
|
||||
requestStatus === "none" ||
|
||||
requestStatus === "rejected" ||
|
||||
requestStatus === "cancelled" ||
|
||||
typeof requestStatus === "undefined"
|
||||
) {
|
||||
return "request_required";
|
||||
}
|
||||
|
||||
return "forbidden";
|
||||
return "request_required";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user