1
0
forked from baron/baron-sso

동의 및 사용자 탭 에러 메세지 수정

This commit is contained in:
2026-04-20 14:45:29 +09:00
parent e15de6d334
commit 7e0680a71c
5 changed files with 122 additions and 16 deletions

View File

@@ -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 (

View File

@@ -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 (
<div className="space-y-8">
<header className="space-y-4">
<div className="flex flex-wrap justify-between gap-4">
<div className="space-y-2">
<nav className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Link to="/" className="hover:text-primary">
{t("ui.dev.clients.consents.breadcrumb.home", "Home")}
</Link>
<span>/</span>
<Link to="/clients" className="hover:text-primary">
{t("ui.dev.clients.consents.breadcrumb.clients", "Apps")}
</Link>
<span>/</span>
<span>{clientData?.client?.name || clientId}</span>
<span>/</span>
<span className="text-foreground font-semibold">
{t(
"ui.dev.clients.consents.breadcrumb.current",
"User Consent Grants",
)}
</span>
</nav>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" asChild>
<Link to={`/clients/${clientId}`}>
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<div>
<p className="text-3xl font-black leading-tight">
{t(
"ui.dev.clients.consents.title",
"User Consent Grants",
)}
</p>
</div>
</div>
</div>
</div>
<ClientDetailTabs activeTab="consents" clientId={clientId} />
</header>
<ForbiddenMessage resourceToken="consents" />
</div>
);
}
}
return (
<div className="space-y-8">
<header className="space-y-4">
@@ -359,18 +412,20 @@ function ClientConsentsPage() {
<Card className="glass-panel">
{error && (
<CardContent className="text-sm text-red-500">
<CardContent className="py-8 text-center text-sm text-destructive border-b border-border/50">
{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,
},
)}
</CardContent>
)}
{isLoading && (
<CardContent className="text-sm text-muted-foreground">
<CardContent className="py-8 text-center text-sm text-muted-foreground border-b border-border/50">
{t("msg.dev.clients.consents.loading", "Loading consents...")}
</CardContent>
)}
@@ -408,10 +463,13 @@ function ClientConsentsPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredRows.length === 0 && !isLoading ? (
{filteredRows.length === 0 && !isLoading && !error ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
{t("msg.dev.clients.consents.empty", "No consents found.")}
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<Search className="h-8 w-8 opacity-20" />
<p>{t("msg.dev.clients.consents.empty", "No consents found.")}</p>
</div>
</TableCell>
</TableRow>
) : (

View File

@@ -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 (
<div className="space-y-8">
<header className="space-y-4">
@@ -390,6 +397,13 @@ function ClientRelationsPage() {
"사용자를 찾는 중입니다...",
)}
</div>
) : isUserSearchForbidden ? (
<div className="px-3 py-2 text-sm text-destructive font-medium">
{t(
"msg.dev.clients.relationships.search_forbidden_user",
"일반 사용자는 관계 추가를 위한 사용자 검색을 사용할 수 없습니다.",
)}
</div>
) : (userSearchData?.items ?? []).length > 0 ? (
(userSearchData?.items ?? []).map((user) => (
<button

View File

@@ -1878,3 +1878,12 @@ workspace = "Workspace Tenant (Context)"
workspace_desc = "Select and save the tenant you are currently working on to change the API request context."
switch_success = "Tenant switch completed"
single_notice = "You belong to a single tenant and do not need to switch."
[msg.dev.forbidden]
default = "You do not have permission to access this resource. Please contact an administrator."
rp_admin = "RP administrators can only view resources for their assigned apps."
tenant_admin = "Tenant administrator permissions are not configured correctly or have expired."
user.clients = "General user accounts can only use this feature if they have been granted operational or management relationships for the relevant RP (App). If you need access, please request it from an administrator."
user.consents = "Viewing consent history for this App (RP) is only available when granted 'RP Admin', 'Consent View', or 'Consent Revoke' relationships. If you need access, please request it from an administrator."
user.audit = "Viewing audit logs for this App (RP) is only available when granted 'RP Admin' or 'Audit View' relationships. If you need access, please request it from an administrator."
title = "Access Denied: {{resource}}"

View File

@@ -1874,3 +1874,12 @@ workspace = "작업 테넌트 (컨텍스트)"
workspace_desc = "현재 작업 중인 테넌트를 선택하고 저장하여 API 요청 컨텍스트를 변경합니다."
switch_success = "테넌트 전환 완료"
single_notice = "단일 테넌트에 소속되어 전환할 필요가 없습니다."
[msg.dev.forbidden]
default = "해당 리소스에 접근할 권한이 없습니다. 관리자에게 문의하세요."
rp_admin = "RP 관리자는 담당 앱의 리소스만 조회할 수 있습니다."
tenant_admin = "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다."
user.clients = "일반 사용자 계정은 담당 RP(앱)에 대한 운영 또는 관리 관계가 부여된 경우에만 해당 기능을 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
user.consents = "해당 앱(RP)에 대한 동의 내역 조회는 'RP 관리자', '동의 조회', '동의 회수' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
user.audit = "해당 앱(RP)에 대한 감사 로그 조회는 'RP 관리자', '감사 조회' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
title = "{{resource}} 접근 권한 없음"