From 7e0680a71c423ccf324a34f069116945e6123e4b Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 20 Apr 2026 14:45:29 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EC=9D=98=20=EB=B0=8F=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=ED=83=AD=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/ForbiddenMessage.tsx | 34 ++++++--- .../features/clients/ClientConsentsPage.tsx | 70 +++++++++++++++++-- .../features/clients/ClientRelationsPage.tsx | 16 ++++- devfront/src/locales/en.toml | 9 +++ devfront/src/locales/ko.toml | 9 +++ 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/devfront/src/components/common/ForbiddenMessage.tsx b/devfront/src/components/common/ForbiddenMessage.tsx index 2a496ba3..97c2af01 100644 --- a/devfront/src/components/common/ForbiddenMessage.tsx +++ b/devfront/src/components/common/ForbiddenMessage.tsx @@ -4,7 +4,7 @@ import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; interface Props { - resourceToken: "audit" | "clients"; + resourceToken: "audit" | "clients" | "consents"; } export function ForbiddenMessage({ resourceToken }: Props) { @@ -28,17 +28,33 @@ export function ForbiddenMessage({ resourceToken }: Props) { "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다.", ); } else if (role === "user" || role === "tenant_member") { - explanation = t( - "msg.dev.forbidden.user", - "일반 사용자 계정은 담당 RP(앱) 관리자 권한이 부여된 경우에만 해당 기능을 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요.", - ); + if (resourceToken === "consents") { + explanation = t( + "msg.dev.forbidden.user.consents", + "해당 앱(RP)에 대한 동의 내역 조회는 'RP 관리자', '동의 조회', '동의 회수' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요.", + ); + } else if (resourceToken === "audit") { + explanation = t( + "msg.dev.forbidden.user.audit", + "해당 앱(RP)에 대한 감사 로그 조회는 'RP 관리자', '감사 조회' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요.", + ); + } else { + explanation = t( + "msg.dev.forbidden.user.clients", + "일반 사용자 계정은 담당 RP(앱)에 대한 운영 또는 관리 관계가 부여된 경우에만 해당 기능을 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요.", + ); + } } + const resourceLabel = + resourceToken === "audit" + ? t("ui.dev.audit.title", "Audit Logs") + : resourceToken === "consents" + ? t("ui.dev.clients.consents.title", "User Consent Grants") + : t("ui.dev.clients.registry.subtitle", "연동 앱"); + const title = t("msg.dev.forbidden.title", "{{resource}} 접근 권한 없음", { - resource: - resourceToken === "audit" - ? t("ui.dev.audit.title", "Audit Logs") - : t("ui.dev.clients.registry.subtitle", "연동 앱"), + resource: resourceLabel, }); return ( diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index c2498d7d..728b80a3 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import { ArrowLeft, ChevronLeft, @@ -9,6 +10,7 @@ import { } from "lucide-react"; import { useState } from "react"; import { Link, useParams } from "react-router-dom"; +import { ForbiddenMessage } from "../../components/common/ForbiddenMessage"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { @@ -161,6 +163,57 @@ function ClientConsentsPage() { } }; + if (error) { + const axiosError = error as AxiosError<{ error?: string }>; + if (axiosError.response?.status === 403) { + return ( +
+
+
+
+ +
+ +
+

+ {t( + "ui.dev.clients.consents.title", + "User Consent Grants", + )} +

+
+
+
+
+ +
+ +
+ ); + } + } + return (
@@ -359,18 +412,20 @@ function ClientConsentsPage() { {error && ( - + {t( "msg.dev.clients.consents.load_error", "Error loading consents: {{error}}", { - error: (error as Error).message, + error: + (error as AxiosError<{ error?: string }>).response?.data + ?.error ?? (error as Error).message, }, )} )} {isLoading && ( - + {t("msg.dev.clients.consents.loading", "Loading consents...")} )} @@ -408,10 +463,13 @@ function ClientConsentsPage() { - {filteredRows.length === 0 && !isLoading ? ( + {filteredRows.length === 0 && !isLoading && !error ? ( - - {t("msg.dev.clients.consents.empty", "No consents found.")} + +
+ +

{t("msg.dev.clients.consents.empty", "No consents found.")}

+
) : ( diff --git a/devfront/src/features/clients/ClientRelationsPage.tsx b/devfront/src/features/clients/ClientRelationsPage.tsx index df3113ed..e754f540 100644 --- a/devfront/src/features/clients/ClientRelationsPage.tsx +++ b/devfront/src/features/clients/ClientRelationsPage.tsx @@ -102,7 +102,11 @@ function ClientRelationsPage() { "이 RP의 관계를 조회할 권한이 없습니다. 관리자에게 관계 조회 또는 RP 관리자 관계 부여를 요청해 주세요.", ); - const { data: userSearchData, isFetching: isUserSearchLoading } = useQuery({ + const { + data: userSearchData, + isFetching: isUserSearchLoading, + error: userSearchError, + } = useQuery({ queryKey: ["dev-users", deferredUserSearch], queryFn: () => fetchDevUsers(deferredUserSearch, 10, clientId), enabled: @@ -280,6 +284,9 @@ function ClientRelationsPage() { ); } + const isUserSearchForbidden = + (userSearchError as AxiosError | null)?.response?.status === 403; + return (
@@ -390,6 +397,13 @@ function ClientRelationsPage() { "사용자를 찾는 중입니다...", )}
+ ) : isUserSearchForbidden ? ( +
+ {t( + "msg.dev.clients.relationships.search_forbidden_user", + "일반 사용자는 관계 추가를 위한 사용자 검색을 사용할 수 없습니다.", + )} +
) : (userSearchData?.items ?? []).length > 0 ? ( (userSearchData?.items ?? []).map((user) => (