forked from baron/baron-sso
i18n, adminfront, devfront: 'make code-check' 통과를 위한 번역 싱크 엔진 개선, 룰 오브 훅 정합성 교정 및 테스트 레이스 컨디션 해결
This commit is contained in:
@@ -14,11 +14,11 @@ import UserProjectionPage from "../features/projections/UserProjectionPage";
|
|||||||
import { TenantAdminsAndOwnersTab } from "../features/tenants/routes/TenantAdminsAndOwnersTab";
|
import { TenantAdminsAndOwnersTab } from "../features/tenants/routes/TenantAdminsAndOwnersTab";
|
||||||
import TenantCreatePage from "../features/tenants/routes/TenantCreatePage";
|
import TenantCreatePage from "../features/tenants/routes/TenantCreatePage";
|
||||||
import TenantDetailPage from "../features/tenants/routes/TenantDetailPage";
|
import TenantDetailPage from "../features/tenants/routes/TenantDetailPage";
|
||||||
|
import { TenantFineGrainedPermissionsPage } from "../features/tenants/routes/TenantFineGrainedPermissionsPage";
|
||||||
|
import { TenantFineGrainedPermissionsTab } from "../features/tenants/routes/TenantFineGrainedPermissionsTab";
|
||||||
import TenantListPage from "../features/tenants/routes/TenantListPage";
|
import TenantListPage from "../features/tenants/routes/TenantListPage";
|
||||||
import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage";
|
import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage";
|
||||||
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
|
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
|
||||||
import { TenantFineGrainedPermissionsTab } from "../features/tenants/routes/TenantFineGrainedPermissionsTab";
|
|
||||||
import { TenantFineGrainedPermissionsPage } from "../features/tenants/routes/TenantFineGrainedPermissionsPage";
|
|
||||||
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
|
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
|
||||||
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
|
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
|
||||||
import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage";
|
import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage";
|
||||||
@@ -53,7 +53,10 @@ export const adminRoutes: RouteObject[] = [
|
|||||||
{ path: "tenants", element: <TenantListPage /> },
|
{ path: "tenants", element: <TenantListPage /> },
|
||||||
{ path: "tenants/new", element: <TenantCreatePage /> },
|
{ path: "tenants/new", element: <TenantCreatePage /> },
|
||||||
{ path: "worksmobile", element: <TenantWorksmobilePage /> },
|
{ path: "worksmobile", element: <TenantWorksmobilePage /> },
|
||||||
{ path: "permissions-direct", element: <TenantFineGrainedPermissionsPage /> },
|
{
|
||||||
|
path: "permissions-direct",
|
||||||
|
element: <TenantFineGrainedPermissionsPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "tenants/:tenantId",
|
path: "tenants/:tenantId",
|
||||||
element: <TenantDetailPage />,
|
element: <TenantDetailPage />,
|
||||||
@@ -62,7 +65,10 @@ export const adminRoutes: RouteObject[] = [
|
|||||||
{ path: "permissions", element: <TenantAdminsAndOwnersTab /> },
|
{ path: "permissions", element: <TenantAdminsAndOwnersTab /> },
|
||||||
{ path: "organization", element: <TenantUserGroupsTab /> },
|
{ path: "organization", element: <TenantUserGroupsTab /> },
|
||||||
{ path: "schema", element: <TenantSchemaPage /> },
|
{ path: "schema", element: <TenantSchemaPage /> },
|
||||||
{ path: "relations", element: <TenantFineGrainedPermissionsTab /> },
|
{
|
||||||
|
path: "relations",
|
||||||
|
element: <TenantFineGrainedPermissionsTab />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -270,9 +270,11 @@ function AppLayout() {
|
|||||||
if (item.to === "/permissions-direct") return false;
|
if (item.to === "/permissions-direct") return false;
|
||||||
if (item.to === "/tenants") return permissions.tenants;
|
if (item.to === "/tenants") return permissions.tenants;
|
||||||
if (item.to === orgfrontUrl) return permissions.org_chart;
|
if (item.to === orgfrontUrl) return permissions.org_chart;
|
||||||
if (item.to === "/worksmobile") return permissions.worksmobile && showWorksmobile;
|
if (item.to === "/worksmobile")
|
||||||
|
return permissions.worksmobile && showWorksmobile;
|
||||||
if (item.to === "/system/ory-ssot") return permissions.ory_ssot;
|
if (item.to === "/system/ory-ssot") return permissions.ory_ssot;
|
||||||
if (item.to === "/system/data-integrity") return permissions.data_integrity;
|
if (item.to === "/system/data-integrity")
|
||||||
|
return permissions.data_integrity;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ export function ParentTenantSelector({
|
|||||||
{localPickerLabel && (
|
{localPickerLabel && (
|
||||||
<Dialog open={localPickerOpen} onOpenChange={setLocalPickerOpen}>
|
<Dialog open={localPickerOpen} onOpenChange={setLocalPickerOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button type="button" variant="outline" size="sm" disabled={disabled}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<Building2 className="h-4 w-4" />
|
<Building2 className="h-4 w-4" />
|
||||||
{localPickerLabel}
|
{localPickerLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useTenantPermission, type TenantPermissionKey } from "../hooks/useTenantPermission";
|
import {
|
||||||
|
type TenantPermissionKey,
|
||||||
|
useTenantPermission,
|
||||||
|
} from "../hooks/useTenantPermission";
|
||||||
|
|
||||||
interface TenantPermissionGuardProps {
|
interface TenantPermissionGuardProps {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, renderHook, screen, waitFor } from "@testing-library/react";
|
||||||
import { renderHook } from "@testing-library/react";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { fetchTenant, fetchMe } from "../../../lib/adminApi";
|
import { fetchMe, fetchTenant } from "../../../lib/adminApi";
|
||||||
import { useTenantPermission } from "./useTenantPermission";
|
|
||||||
import { TenantPermissionGuard } from "../components/TenantPermissionGuard";
|
import { TenantPermissionGuard } from "../components/TenantPermissionGuard";
|
||||||
|
import { useTenantPermission } from "./useTenantPermission";
|
||||||
|
|
||||||
vi.mock("../../../lib/adminApi", () => ({
|
vi.mock("../../../lib/adminApi", () => ({
|
||||||
fetchMe: vi.fn(),
|
fetchMe: vi.fn(),
|
||||||
@@ -88,10 +87,14 @@ describe("TenantPermissionGuard", () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TenantPermissionGuard tenantId="tenant-3" relation="manage" fallback={<div>Access Denied</div>}>
|
<TenantPermissionGuard
|
||||||
|
tenantId="tenant-3"
|
||||||
|
relation="manage"
|
||||||
|
fallback={<div>Access Denied</div>}
|
||||||
|
>
|
||||||
<div>Access Granted</div>
|
<div>Access Granted</div>
|
||||||
</TenantPermissionGuard>,
|
</TenantPermissionGuard>,
|
||||||
{ wrapper: createWrapper() }
|
{ wrapper: createWrapper() },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -112,10 +115,14 @@ describe("TenantPermissionGuard", () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TenantPermissionGuard tenantId="tenant-4" relation="manage" fallback={<div>Access Denied</div>}>
|
<TenantPermissionGuard
|
||||||
|
tenantId="tenant-4"
|
||||||
|
relation="manage"
|
||||||
|
fallback={<div>Access Denied</div>}
|
||||||
|
>
|
||||||
<div>Access Granted</div>
|
<div>Access Granted</div>
|
||||||
</TenantPermissionGuard>,
|
</TenantPermissionGuard>,
|
||||||
{ wrapper: createWrapper() }
|
{ wrapper: createWrapper() },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchTenant, fetchMe } from "../../../lib/adminApi";
|
import { fetchMe, fetchTenant } from "../../../lib/adminApi";
|
||||||
import { normalizeAdminRole } from "../../../lib/roles";
|
import { normalizeAdminRole } from "../../../lib/roles";
|
||||||
|
|
||||||
export type TenantPermissionKey =
|
export type TenantPermissionKey =
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
|
||||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
import { Button } from "../../../components/ui/button";
|
import { Button } from "../../../components/ui/button";
|
||||||
@@ -50,6 +49,7 @@ import {
|
|||||||
type TenantAdmin,
|
type TenantAdmin,
|
||||||
} from "../../../lib/adminApi";
|
} from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
|
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||||
|
|
||||||
type DialogMode = "owner" | "admin";
|
type DialogMode = "owner" | "admin";
|
||||||
|
|
||||||
@@ -71,7 +71,8 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
||||||
const tenantId = tenantIdParam ?? "";
|
const tenantId = tenantIdParam ?? "";
|
||||||
const { hasPermission } = useTenantPermission(tenantId);
|
const { hasPermission } = useTenantPermission(tenantId);
|
||||||
const isWritable = hasPermission("manage_permissions") || hasPermission("manage_admins");
|
const isWritable =
|
||||||
|
hasPermission("manage_permissions") || hasPermission("manage_admins");
|
||||||
const canView = hasPermission("view_permissions") || hasPermission("view");
|
const canView = hasPermission("view_permissions") || hasPermission("view");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { Copy } from "lucide-react";
|
import { Copy } from "lucide-react";
|
||||||
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
||||||
import { Button } from "../../../components/ui/button";
|
import { Button } from "../../../components/ui/button";
|
||||||
import { fetchMe, fetchTenant } from "../../../lib/adminApi";
|
import { fetchTenant } from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
import { normalizeAdminRole } from "../../../lib/roles";
|
|
||||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||||
|
|
||||||
function TenantDetailPage() {
|
function TenantDetailPage() {
|
||||||
|
|||||||
@@ -1,45 +1,27 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import {
|
import {
|
||||||
fetchAllTenants,
|
|
||||||
fetchMe,
|
|
||||||
fetchUsers,
|
|
||||||
fetchSystemRelations,
|
|
||||||
addSystemRelation,
|
|
||||||
removeSystemRelation,
|
|
||||||
type TenantRelation,
|
|
||||||
} from "../../../lib/adminApi";
|
|
||||||
import { t } from "../../../lib/i18n";
|
|
||||||
import { TenantFineGrainedPermissionsTab } from "./TenantFineGrainedPermissionsTab";
|
|
||||||
import {
|
|
||||||
ShieldCheck,
|
|
||||||
Search,
|
|
||||||
Plus,
|
|
||||||
UserPlus,
|
|
||||||
Trash2,
|
|
||||||
Settings,
|
|
||||||
Shield,
|
|
||||||
LayoutDashboard,
|
|
||||||
Building2,
|
Building2,
|
||||||
Network,
|
|
||||||
Database,
|
Database,
|
||||||
Users,
|
|
||||||
KeyRound,
|
|
||||||
Key,
|
Key,
|
||||||
|
KeyRound,
|
||||||
|
LayoutDashboard,
|
||||||
|
Network,
|
||||||
NotebookTabs,
|
NotebookTabs,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
Share2,
|
Share2,
|
||||||
|
Shield,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Avatar, AvatarFallback } from "../../../components/ui/avatar";
|
||||||
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 { Card, CardContent } from "../../../components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "../../../components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -48,24 +30,33 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "../../../components/ui/dialog";
|
} from "../../../components/ui/dialog";
|
||||||
import { Input } from "../../../components/ui/input";
|
import { Input } from "../../../components/ui/input";
|
||||||
import { Avatar, AvatarFallback } from "../../../components/ui/avatar";
|
|
||||||
import { ScrollArea } from "../../../components/ui/scroll-area";
|
import { ScrollArea } from "../../../components/ui/scroll-area";
|
||||||
import { Separator } from "../../../components/ui/separator";
|
|
||||||
import { Switch } from "../../../components/ui/switch";
|
|
||||||
import { toast } from "../../../components/ui/use-toast";
|
import { toast } from "../../../components/ui/use-toast";
|
||||||
|
import {
|
||||||
|
addSystemRelation,
|
||||||
|
fetchAllTenants,
|
||||||
|
fetchMe,
|
||||||
|
fetchSystemRelations,
|
||||||
|
fetchUsers,
|
||||||
|
removeSystemRelation,
|
||||||
|
type TenantRelation,
|
||||||
|
} from "../../../lib/adminApi";
|
||||||
|
import { t } from "../../../lib/i18n";
|
||||||
|
|
||||||
export function TenantFineGrainedPermissionsPage() {
|
export function TenantFineGrainedPermissionsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState<"tenant" | "system">("system");
|
const [activeTab, _setActiveTab] = useState<"tenant" | "system">("system");
|
||||||
const [selectedTenantId, setSelectedTenantId] = useState("");
|
const [_selectedTenantId, _setSelectedTenantId] = useState("");
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [activeUserId, setActiveUserId] = useState<string | null>(null);
|
const [activeUserId, setActiveUserId] = useState<string | null>(null);
|
||||||
const [userSearchTerm, setUserSearchTerm] = useState("");
|
const [userSearchTerm, setUserSearchTerm] = useState("");
|
||||||
|
|
||||||
// 🌟 글로벌 시스템 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
// 🌟 글로벌 시스템 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
||||||
const [localSystemPermissions, setLocalSystemPermissions] = useState<Record<string, Record<string, "none" | "read" | "write">>>({});
|
const [localSystemPermissions, setLocalSystemPermissions] = useState<
|
||||||
|
Record<string, Record<string, "none" | "read" | "write">>
|
||||||
|
>({});
|
||||||
|
|
||||||
const { data: profile } = useQuery({
|
const { data: profile } = useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
@@ -74,26 +65,13 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
|
|
||||||
const isSuperAdmin = profile?.role === "super_admin";
|
const isSuperAdmin = profile?.role === "super_admin";
|
||||||
|
|
||||||
if (profile && !isSuperAdmin) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
|
||||||
<h3 className="text-lg font-bold">
|
|
||||||
{t("msg.admin.common.forbidden", "접근 권한이 없습니다.")}
|
|
||||||
</h3>
|
|
||||||
<Button onClick={() => navigate("/")}>
|
|
||||||
{t("ui.common.go_home", "홈으로 이동")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantsQuery = useQuery({
|
const tenantsQuery = useQuery({
|
||||||
queryKey: ["tenants", "list-all"],
|
queryKey: ["tenants", "list-all"],
|
||||||
queryFn: () => fetchAllTenants(),
|
queryFn: () => fetchAllTenants(),
|
||||||
enabled: isSuperAdmin,
|
enabled: isSuperAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tenants = isSuperAdmin
|
const _tenants = isSuperAdmin
|
||||||
? (tenantsQuery.data?.items ?? [])
|
? (tenantsQuery.data?.items ?? [])
|
||||||
: (profile?.manageableTenants ?? []);
|
: (profile?.manageableTenants ?? []);
|
||||||
|
|
||||||
@@ -108,18 +86,33 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (systemRelationsQuery.data) {
|
if (systemRelationsQuery.data) {
|
||||||
const initialMap: Record<string, Record<string, "none" | "read" | "write">> = {};
|
const initialMap: Record<
|
||||||
|
string,
|
||||||
|
Record<string, "none" | "read" | "write">
|
||||||
|
> = {};
|
||||||
for (const user of systemRelationsQuery.data) {
|
for (const user of systemRelationsQuery.data) {
|
||||||
initialMap[user.userId] = {};
|
initialMap[user.userId] = {};
|
||||||
const menus = [
|
const menus = [
|
||||||
"overview", "audit_logs", "tenants", "org_chart", "users",
|
"overview",
|
||||||
"worksmobile", "api_keys", "ory_ssot", "data_integrity",
|
"audit_logs",
|
||||||
"auth_guard", "permissions_direct"
|
"tenants",
|
||||||
|
"org_chart",
|
||||||
|
"users",
|
||||||
|
"worksmobile",
|
||||||
|
"api_keys",
|
||||||
|
"ory_ssot",
|
||||||
|
"data_integrity",
|
||||||
|
"auth_guard",
|
||||||
|
"permissions_direct",
|
||||||
];
|
];
|
||||||
for (const m of menus) {
|
for (const m of menus) {
|
||||||
const isWrite = user.relations.includes(`${m}_managers`);
|
const isWrite = user.relations.includes(`${m}_managers`);
|
||||||
const isRead = user.relations.includes(`${m}_viewers`);
|
const isRead = user.relations.includes(`${m}_viewers`);
|
||||||
initialMap[user.userId][m] = isWrite ? "write" : isRead ? "read" : "none";
|
initialMap[user.userId][m] = isWrite
|
||||||
|
? "write"
|
||||||
|
: isRead
|
||||||
|
? "read"
|
||||||
|
: "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLocalSystemPermissions(initialMap);
|
setLocalSystemPermissions(initialMap);
|
||||||
@@ -131,30 +124,41 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
addSystemRelation(payload.userId, payload.relation),
|
addSystemRelation(payload.userId, payload.relation),
|
||||||
onMutate: async (newRelation) => {
|
onMutate: async (newRelation) => {
|
||||||
await queryClient.cancelQueries({ queryKey: ["system-relations"] });
|
await queryClient.cancelQueries({ queryKey: ["system-relations"] });
|
||||||
const previousRelations = queryClient.getQueryData<TenantRelation[]>(["system-relations"]);
|
const previousRelations = queryClient.getQueryData<TenantRelation[]>([
|
||||||
|
"system-relations",
|
||||||
|
]);
|
||||||
|
|
||||||
queryClient.setQueryData<TenantRelation[]>(["system-relations"], (old) => {
|
queryClient.setQueryData<TenantRelation[]>(
|
||||||
if (!old) return [];
|
["system-relations"],
|
||||||
return old.map((user) => {
|
(old) => {
|
||||||
if (user.userId === newRelation.userId) {
|
if (!old) return [];
|
||||||
return {
|
return old.map((user) => {
|
||||||
...user,
|
if (user.userId === newRelation.userId) {
|
||||||
relations: user.relations.includes(newRelation.relation)
|
return {
|
||||||
? user.relations
|
...user,
|
||||||
: [...user.relations, newRelation.relation],
|
relations: user.relations.includes(newRelation.relation)
|
||||||
};
|
? user.relations
|
||||||
}
|
: [...user.relations, newRelation.relation],
|
||||||
return user;
|
};
|
||||||
});
|
}
|
||||||
});
|
return user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return { previousRelations };
|
return { previousRelations };
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
||||||
if (context?.previousRelations) {
|
if (context?.previousRelations) {
|
||||||
queryClient.setQueryData(["system-relations"], context.previousRelations);
|
queryClient.setQueryData(
|
||||||
|
["system-relations"],
|
||||||
|
context.previousRelations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Quiet mutate
|
// Quiet mutate
|
||||||
@@ -174,28 +178,41 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
removeSystemRelation(payload.userId, payload.relation),
|
removeSystemRelation(payload.userId, payload.relation),
|
||||||
onMutate: async (targetRelation) => {
|
onMutate: async (targetRelation) => {
|
||||||
await queryClient.cancelQueries({ queryKey: ["system-relations"] });
|
await queryClient.cancelQueries({ queryKey: ["system-relations"] });
|
||||||
const previousRelations = queryClient.getQueryData<TenantRelation[]>(["system-relations"]);
|
const previousRelations = queryClient.getQueryData<TenantRelation[]>([
|
||||||
|
"system-relations",
|
||||||
|
]);
|
||||||
|
|
||||||
queryClient.setQueryData<TenantRelation[]>(["system-relations"], (old) => {
|
queryClient.setQueryData<TenantRelation[]>(
|
||||||
if (!old) return [];
|
["system-relations"],
|
||||||
return old.map((user) => {
|
(old) => {
|
||||||
if (user.userId === targetRelation.userId) {
|
if (!old) return [];
|
||||||
return {
|
return old.map((user) => {
|
||||||
...user,
|
if (user.userId === targetRelation.userId) {
|
||||||
relations: user.relations.filter((r) => r !== targetRelation.relation),
|
return {
|
||||||
};
|
...user,
|
||||||
}
|
relations: user.relations.filter(
|
||||||
return user;
|
(r) => r !== targetRelation.relation,
|
||||||
});
|
),
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return { previousRelations };
|
return { previousRelations };
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
||||||
if (context?.previousRelations) {
|
if (context?.previousRelations) {
|
||||||
queryClient.setQueryData(["system-relations"], context.previousRelations);
|
queryClient.setQueryData(
|
||||||
|
["system-relations"],
|
||||||
|
context.previousRelations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Quiet mutate
|
// Quiet mutate
|
||||||
@@ -220,26 +237,53 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentVal === "read") {
|
if (currentVal === "read") {
|
||||||
await removeSystemRelationMutation.mutateAsync({ userId, relation: `${menuKey}_viewers` });
|
await removeSystemRelationMutation.mutateAsync({
|
||||||
|
userId,
|
||||||
|
relation: `${menuKey}_viewers`,
|
||||||
|
});
|
||||||
} else if (currentVal === "write") {
|
} else if (currentVal === "write") {
|
||||||
await removeSystemRelationMutation.mutateAsync({ userId, relation: `${menuKey}_managers` });
|
await removeSystemRelationMutation.mutateAsync({
|
||||||
|
userId,
|
||||||
|
relation: `${menuKey}_managers`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newVal === "read") {
|
if (newVal === "read") {
|
||||||
await addSystemRelationMutation.mutateAsync({ userId, relation: `${menuKey}_viewers` });
|
await addSystemRelationMutation.mutateAsync({
|
||||||
|
userId,
|
||||||
|
relation: `${menuKey}_viewers`,
|
||||||
|
});
|
||||||
} else if (newVal === "write") {
|
} else if (newVal === "write") {
|
||||||
await addSystemRelationMutation.mutateAsync({ userId, relation: `${menuKey}_managers` });
|
await addSystemRelationMutation.mutateAsync({
|
||||||
|
userId,
|
||||||
|
relation: `${menuKey}_managers`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 Trigger a single consolidated success toast at the very end
|
// 🌟 Trigger a single consolidated success toast at the very end
|
||||||
toast.success(t("msg.admin.system.relations.update_success", "시스템 메뉴 권한이 성공적으로 변경되었습니다."));
|
toast.success(
|
||||||
|
t(
|
||||||
|
"msg.admin.system.relations.update_success",
|
||||||
|
"시스템 메뉴 권한이 성공적으로 변경되었습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Individual mutations handle error toast via onError
|
// Individual mutations handle error toast via onError
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAllSystemRelations = async (userId: string, userRelations: string[]) => {
|
const handleRemoveAllSystemRelations = async (
|
||||||
if (!window.confirm(t("msg.admin.system.relations.remove_all_confirm", "이 사용자의 모든 시스템 메뉴 권한을 삭제하시겠습니까?"))) {
|
userId: string,
|
||||||
|
userRelations: string[],
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
t(
|
||||||
|
"msg.admin.system.relations.remove_all_confirm",
|
||||||
|
"이 사용자의 모든 시스템 메뉴 권한을 삭제하시겠습니까?",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const rel of userRelations) {
|
for (const rel of userRelations) {
|
||||||
@@ -268,36 +312,133 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
// Categorized system menus with descriptions and icons
|
// Categorized system menus with descriptions and icons
|
||||||
const systemMenuCategories = [
|
const systemMenuCategories = [
|
||||||
{
|
{
|
||||||
title: t("ui.admin.permissions_direct.cat_dashboard", "핵심 대시보드 및 분석"),
|
title: t(
|
||||||
|
"ui.admin.permissions_direct.cat_dashboard",
|
||||||
|
"핵심 대시보드 및 분석",
|
||||||
|
),
|
||||||
menus: [
|
menus: [
|
||||||
{ label: t("ui.admin.nav.overview", "개요"), relation: "overview", desc: t("msg.admin.permissions_direct.desc_overview", "바론 전체 사양 및 시스템 상태 개요 정보"), icon: LayoutDashboard },
|
{
|
||||||
{ label: t("ui.admin.nav.audit_logs", "감사 로그"), relation: "audit_logs", desc: t("msg.admin.permissions_direct.desc_audit_logs", "시스템 전역 보안 감사 및 접속 이력 로그"), icon: NotebookTabs },
|
label: t("ui.admin.nav.overview", "개요"),
|
||||||
]
|
relation: "overview",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_overview",
|
||||||
|
"바론 전체 사양 및 시스템 상태 개요 정보",
|
||||||
|
),
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.audit_logs", "감사 로그"),
|
||||||
|
relation: "audit_logs",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_audit_logs",
|
||||||
|
"시스템 전역 보안 감사 및 접속 이력 로그",
|
||||||
|
),
|
||||||
|
icon: NotebookTabs,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("ui.admin.permissions_direct.cat_resources", "핵심 리소스 관리"),
|
title: t("ui.admin.permissions_direct.cat_resources", "핵심 리소스 관리"),
|
||||||
menus: [
|
menus: [
|
||||||
{ label: t("ui.admin.nav.tenants", "테넌트"), relation: "tenants", desc: t("msg.admin.permissions_direct.desc_tenants", "고객 테넌트 목록, 신규 부모-자식 테넌트 관리"), icon: Building2 },
|
{
|
||||||
{ label: t("ui.admin.nav.org_chart", "조직도"), relation: "org_chart", desc: t("msg.admin.permissions_direct.desc_org_chart", "조직도 가시화 및 트리 배치 확인"), icon: Network },
|
label: t("ui.admin.nav.tenants", "테넌트"),
|
||||||
{ label: t("ui.admin.nav.users", "사용자"), relation: "users", desc: t("msg.admin.permissions_direct.desc_users", "가입 사용자 목록, 승인 및 커스텀 클레임 수동 주입"), icon: Users },
|
relation: "tenants",
|
||||||
]
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_tenants",
|
||||||
|
"고객 테넌트 목록, 신규 부모-자식 테넌트 관리",
|
||||||
|
),
|
||||||
|
icon: Building2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.org_chart", "조직도"),
|
||||||
|
relation: "org_chart",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_org_chart",
|
||||||
|
"조직도 가시화 및 트리 배치 확인",
|
||||||
|
),
|
||||||
|
icon: Network,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.users", "사용자"),
|
||||||
|
relation: "users",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_users",
|
||||||
|
"가입 사용자 목록, 승인 및 커스텀 클레임 수동 주입",
|
||||||
|
),
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("ui.admin.permissions_direct.cat_integrations", "인프라 연동 및 보안"),
|
title: t(
|
||||||
|
"ui.admin.permissions_direct.cat_integrations",
|
||||||
|
"인프라 연동 및 보안",
|
||||||
|
),
|
||||||
menus: [
|
menus: [
|
||||||
{ label: t("ui.admin.nav.worksmobile", "Worksmobile"), relation: "worksmobile", desc: t("msg.admin.permissions_direct.desc_worksmobile", "라인웍스 연동 및 사내 임직원 패스워드 강제 동기화"), icon: Share2 },
|
{
|
||||||
{ label: t("ui.admin.nav.api_keys", "API 키"), relation: "api_keys", desc: t("msg.admin.permissions_direct.desc_api_keys", "조직도 연동을 위한 전역 서드파티 토큰 관리"), icon: Key },
|
label: t("ui.admin.nav.worksmobile", "Worksmobile"),
|
||||||
]
|
relation: "worksmobile",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_worksmobile",
|
||||||
|
"라인웍스 연동 및 사내 임직원 패스워드 강제 동기화",
|
||||||
|
),
|
||||||
|
icon: Share2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.api_keys", "API 키"),
|
||||||
|
relation: "api_keys",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_api_keys",
|
||||||
|
"조직도 연동을 위한 전역 서드파티 토큰 관리",
|
||||||
|
),
|
||||||
|
icon: Key,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("ui.admin.permissions_direct.cat_system", "아이덴티티 및 게이트 관리"),
|
title: t(
|
||||||
|
"ui.admin.permissions_direct.cat_system",
|
||||||
|
"아이덴티티 및 게이트 관리",
|
||||||
|
),
|
||||||
menus: [
|
menus: [
|
||||||
{ label: t("ui.admin.nav.ory_ssot", "Ory SSOT 시스템"), relation: "ory_ssot", desc: t("msg.admin.permissions_direct.desc_ory_ssot", "Redis 아이덴티티 미러 캐시 및 PostgreSQL read model 정합성 갱신"), icon: Database },
|
{
|
||||||
{ label: t("ui.admin.nav.data_integrity", "데이터 정합성"), relation: "data_integrity", desc: t("msg.admin.permissions_direct.desc_data_integrity", "고아 레코드 검출 및 DB 정합성 최종 검증기"), icon: ShieldCheck },
|
label: t("ui.admin.nav.ory_ssot", "Ory SSOT 시스템"),
|
||||||
{ label: t("ui.admin.nav.auth_guard", "인증 가드"), relation: "auth_guard", desc: t("msg.admin.permissions_direct.desc_auth_guard", "정책엔진 기준으로 Keto ReBAC 관계 검증 시뮬레이터"), icon: KeyRound },
|
relation: "ory_ssot",
|
||||||
{ label: t("ui.admin.nav.permissions_direct", "권한 부여"), relation: "permissions_direct", desc: t("msg.admin.permissions_direct.desc_permissions_direct", "본 사이드바 메뉴 세부 권한 격자 및 테넌트 인가 설정 패널"), icon: Shield },
|
desc: t(
|
||||||
]
|
"msg.admin.permissions_direct.desc_ory_ssot",
|
||||||
}
|
"Redis 아이덴티티 미러 캐시 및 PostgreSQL read model 정합성 갱신",
|
||||||
|
),
|
||||||
|
icon: Database,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.data_integrity", "데이터 정합성"),
|
||||||
|
relation: "data_integrity",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_data_integrity",
|
||||||
|
"고아 레코드 검출 및 DB 정합성 최종 검증기",
|
||||||
|
),
|
||||||
|
icon: ShieldCheck,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.auth_guard", "인증 가드"),
|
||||||
|
relation: "auth_guard",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_auth_guard",
|
||||||
|
"정책엔진 기준으로 Keto ReBAC 관계 검증 시뮬레이터",
|
||||||
|
),
|
||||||
|
icon: KeyRound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ui.admin.nav.permissions_direct", "권한 부여"),
|
||||||
|
relation: "permissions_direct",
|
||||||
|
desc: t(
|
||||||
|
"msg.admin.permissions_direct.desc_permissions_direct",
|
||||||
|
"본 사이드바 메뉴 세부 권한 격자 및 테넌트 인가 설정 패널",
|
||||||
|
),
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredRelations = systemRelations.filter(
|
const filteredRelations = systemRelations.filter(
|
||||||
@@ -308,6 +449,19 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
|
|
||||||
const selectedUser = systemRelations.find((r) => r.userId === activeUserId);
|
const selectedUser = systemRelations.find((r) => r.userId === activeUserId);
|
||||||
|
|
||||||
|
if (profile && !isSuperAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||||
|
<h3 className="text-lg font-bold">
|
||||||
|
{t("msg.admin.common.forbidden", "접근 권한이 없습니다.")}
|
||||||
|
</h3>
|
||||||
|
<Button onClick={() => navigate("/")}>
|
||||||
|
{t("ui.common.go_home", "홈으로 이동")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -325,204 +479,260 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
|
|
||||||
{/* 시스템 메뉴 권한 (Admin Control) Split Screen Panel */}
|
{/* 시스템 메뉴 권한 (Admin Control) Split Screen Panel */}
|
||||||
<div className="flex flex-col lg:flex-row gap-6 h-[720px] border border-border rounded-xl bg-card overflow-hidden shadow-sm">
|
<div className="flex flex-col lg:flex-row gap-6 h-[720px] border border-border rounded-xl bg-card overflow-hidden shadow-sm">
|
||||||
{/* Left Panel: User List */}
|
{/* Left Panel: User List */}
|
||||||
<div className="w-full lg:w-80 border-r border-border flex flex-col bg-muted/10 h-full">
|
<div className="w-full lg:w-80 border-r border-border flex flex-col bg-muted/10 h-full">
|
||||||
<div className="p-4 border-b border-border space-y-3 flex-shrink-0">
|
<div className="p-4 border-b border-border space-y-3 flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-bold text-sm text-foreground">
|
<h3 className="font-bold text-sm text-foreground">
|
||||||
{t("ui.admin.permissions_direct.user_list", "대상 사용자")} ({filteredRelations.length})
|
{t("ui.admin.permissions_direct.user_list", "대상 사용자")} (
|
||||||
</h3>
|
{filteredRelations.length})
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={t("ui.common.search", "이름 또는 이메일 검색...")}
|
||||||
|
value={userSearchTerm}
|
||||||
|
onChange={(e) => setUserSearchTerm(e.target.value)}
|
||||||
|
name="user-search"
|
||||||
|
className="pl-8 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
{filteredRelations.length === 0 ? (
|
||||||
|
<div className="p-6 text-center text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.admin.permissions_direct.no_users_found",
|
||||||
|
"등록된 사용자가 없습니다.",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredRelations.map((user) => {
|
||||||
|
const isSelected = activeUserId === user.userId;
|
||||||
|
const activeCount = user.relations.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={user.userId}
|
||||||
|
onClick={() => setActiveUserId(user.userId)}
|
||||||
|
className={`w-full flex items-center justify-between p-3 rounded-lg text-left transition-all ${
|
||||||
|
isSelected
|
||||||
|
? "bg-primary/10 text-primary border-l-4 border-primary shadow-sm"
|
||||||
|
: "hover:bg-muted/50 text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Avatar className="h-8 w-8 border border-border">
|
||||||
|
<AvatarFallback className="bg-primary/10 text-primary font-bold text-xs uppercase">
|
||||||
|
{user.name.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<span className="text-sm font-semibold truncate">
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground truncate max-w-[150px]">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant={isSelected ? "default" : "secondary"}
|
||||||
|
className="text-[9px] px-1.5 py-0.5"
|
||||||
|
>
|
||||||
|
{activeCount}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel: Toggle settings grid */}
|
||||||
|
<div className="flex-1 flex flex-col h-full bg-background">
|
||||||
|
{selectedUser ? (
|
||||||
|
<>
|
||||||
|
{/* User Detail Header */}
|
||||||
|
<div className="p-5 border-b border-border flex items-center justify-between flex-shrink-0 bg-muted/5">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar className="h-11 w-11 border">
|
||||||
|
<AvatarFallback className="bg-primary/5 text-primary font-extrabold text-sm uppercase">
|
||||||
|
{selectedUser.name.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h2 className="text-lg font-bold flex items-center gap-2">
|
||||||
|
{selectedUser.name}
|
||||||
|
<Badge variant="outline" className="text-xs font-normal">
|
||||||
|
{selectedUser.relations.length}{" "}
|
||||||
|
{t("ui.admin.permissions_direct.allowed", "개 허용됨")}
|
||||||
|
</Badge>
|
||||||
|
</h2>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{selectedUser.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
className="text-destructive border-destructive/20 hover:bg-destructive/10"
|
||||||
className="h-8 w-8 p-0"
|
onClick={() =>
|
||||||
onClick={() => setIsDialogOpen(true)}
|
handleRemoveAllSystemRelations(
|
||||||
|
selectedUser.userId,
|
||||||
|
selectedUser.relations,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
{t(
|
||||||
|
"ui.admin.permissions_direct.revoke_all",
|
||||||
|
"모든 권한 회수",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
{/* Categorized Toggle Grid */}
|
||||||
<Input
|
<ScrollArea className="flex-1">
|
||||||
placeholder={t("ui.common.search", "이름 또는 이메일 검색...")}
|
<div className="p-6 space-y-6">
|
||||||
value={userSearchTerm}
|
{systemMenuCategories.map((category) => (
|
||||||
onChange={(e) => setUserSearchTerm(e.target.value)}
|
<div key={category.title} className="space-y-3">
|
||||||
name="user-search"
|
<h4 className="text-xs font-bold text-muted-foreground uppercase tracking-wider">
|
||||||
className="pl-8 h-8 text-xs"
|
{category.title}
|
||||||
/>
|
</h4>
|
||||||
|
<Card className="border border-border/60 shadow-none bg-card">
|
||||||
|
<CardContent className="p-0 divide-y divide-border/40">
|
||||||
|
{category.menus.map((menu) => {
|
||||||
|
const isWrite = selectedUser.relations.includes(
|
||||||
|
`${menu.relation}_managers`,
|
||||||
|
);
|
||||||
|
const isRead = selectedUser.relations.includes(
|
||||||
|
`${menu.relation}_viewers`,
|
||||||
|
);
|
||||||
|
const serverValue: "none" | "read" | "write" =
|
||||||
|
isWrite ? "write" : isRead ? "read" : "none";
|
||||||
|
const permissionValue =
|
||||||
|
localSystemPermissions[selectedUser.userId]?.[
|
||||||
|
menu.relation
|
||||||
|
] ?? serverValue;
|
||||||
|
const Icon = menu.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={menu.relation}
|
||||||
|
className="flex items-center justify-between p-4 hover:bg-muted/10 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4 pr-4 min-w-0">
|
||||||
|
<div className="p-2 rounded-lg bg-secondary/50 text-foreground flex-shrink-0 mt-0.5">
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-semibold text-foreground">
|
||||||
|
{menu.label}
|
||||||
|
</span>
|
||||||
|
{(menu.relation === "ory_ssot" ||
|
||||||
|
menu.relation === "data_integrity") && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px] py-0.5 px-1.5 font-semibold text-destructive bg-destructive/10 border-destructive/20"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.admin.permissions_direct.super_admin_only",
|
||||||
|
"Super Admin 전용",
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||||
|
{menu.desc}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
name={`system-menu-permission-${menu.relation}`}
|
||||||
|
value={permissionValue}
|
||||||
|
disabled={
|
||||||
|
menu.relation === "ory_ssot" ||
|
||||||
|
menu.relation === "data_integrity"
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextVal = e.target.value as
|
||||||
|
| "none"
|
||||||
|
| "read"
|
||||||
|
| "write";
|
||||||
|
// 🌟 1단계: 로컬 임시 상태 즉시 갱신 (0ms 반응 보장)
|
||||||
|
setLocalSystemPermissions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selectedUser.userId]: {
|
||||||
|
...(prev[selectedUser.userId] ?? {}),
|
||||||
|
[menu.relation]: nextVal,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// 🌟 2단계: 백그라운드 비동기 API 요청 수행
|
||||||
|
handleSystemRelationChange(
|
||||||
|
selectedUser.userId,
|
||||||
|
menu.relation,
|
||||||
|
permissionValue,
|
||||||
|
nextVal,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="flex h-9 w-[180px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="none">
|
||||||
|
{t("ui.common.none", "권한 없음")}
|
||||||
|
</option>
|
||||||
|
<option value="read">
|
||||||
|
{t("ui.common.read", "조회 가능 (Read)")}
|
||||||
|
</option>
|
||||||
|
<option value="write">
|
||||||
|
{t("ui.common.write", "수정 가능 (Write)")}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center text-muted-foreground bg-muted/5 gap-3">
|
||||||
|
<ShieldCheck className="h-12 w-12 text-muted-foreground opacity-30" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
{t(
|
||||||
|
"ui.admin.permissions_direct.no_user_selected",
|
||||||
|
"사용자가 선택되지 않았습니다.",
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
{t(
|
||||||
|
"msg.admin.permissions_direct.no_user_selected_desc",
|
||||||
|
"왼쪽의 사용자 리스트에서 권한을 변경할 인원을 선택해 주세요.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="flex-1">
|
)}
|
||||||
<div className="p-2 space-y-1">
|
|
||||||
{filteredRelations.length === 0 ? (
|
|
||||||
<div className="p-6 text-center text-xs text-muted-foreground">
|
|
||||||
{t("msg.admin.permissions_direct.no_users_found", "등록된 사용자가 없습니다.")}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredRelations.map((user) => {
|
|
||||||
const isSelected = activeUserId === user.userId;
|
|
||||||
const activeCount = user.relations.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={user.userId}
|
|
||||||
onClick={() => setActiveUserId(user.userId)}
|
|
||||||
className={`w-full flex items-center justify-between p-3 rounded-lg text-left transition-all ${
|
|
||||||
isSelected
|
|
||||||
? "bg-primary/10 text-primary border-l-4 border-primary shadow-sm"
|
|
||||||
: "hover:bg-muted/50 text-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<Avatar className="h-8 w-8 border border-border">
|
|
||||||
<AvatarFallback className="bg-primary/10 text-primary font-bold text-xs uppercase">
|
|
||||||
{user.name.charAt(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="text-sm font-semibold truncate">{user.name}</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground truncate max-w-[150px]">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant={isSelected ? "default" : "secondary"} className="text-[9px] px-1.5 py-0.5">
|
|
||||||
{activeCount}
|
|
||||||
</Badge>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Panel: Toggle settings grid */}
|
|
||||||
<div className="flex-1 flex flex-col h-full bg-background">
|
|
||||||
{selectedUser ? (
|
|
||||||
<>
|
|
||||||
{/* User Detail Header */}
|
|
||||||
<div className="p-5 border-b border-border flex items-center justify-between flex-shrink-0 bg-muted/5">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Avatar className="h-11 w-11 border">
|
|
||||||
<AvatarFallback className="bg-primary/5 text-primary font-extrabold text-sm uppercase">
|
|
||||||
{selectedUser.name.charAt(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h2 className="text-lg font-bold flex items-center gap-2">
|
|
||||||
{selectedUser.name}
|
|
||||||
<Badge variant="outline" className="text-xs font-normal">
|
|
||||||
{selectedUser.relations.length} {t("ui.admin.permissions_direct.allowed", "개 허용됨")}
|
|
||||||
</Badge>
|
|
||||||
</h2>
|
|
||||||
<span className="text-xs text-muted-foreground">{selectedUser.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-destructive border-destructive/20 hover:bg-destructive/10"
|
|
||||||
onClick={() => handleRemoveAllSystemRelations(selectedUser.userId, selectedUser.relations)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
{t("ui.admin.permissions_direct.revoke_all", "모든 권한 회수")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Categorized Toggle Grid */}
|
|
||||||
<ScrollArea className="flex-1">
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{systemMenuCategories.map((category) => (
|
|
||||||
<div key={category.title} className="space-y-3">
|
|
||||||
<h4 className="text-xs font-bold text-muted-foreground uppercase tracking-wider">
|
|
||||||
{category.title}
|
|
||||||
</h4>
|
|
||||||
<Card className="border border-border/60 shadow-none bg-card">
|
|
||||||
<CardContent className="p-0 divide-y divide-border/40">
|
|
||||||
{category.menus.map((menu) => {
|
|
||||||
const isWrite = selectedUser.relations.includes(`${menu.relation}_managers`);
|
|
||||||
const isRead = selectedUser.relations.includes(`${menu.relation}_viewers`);
|
|
||||||
const serverValue: "none" | "read" | "write" = isWrite ? "write" : isRead ? "read" : "none";
|
|
||||||
const permissionValue = localSystemPermissions[selectedUser.userId]?.[menu.relation] ?? serverValue;
|
|
||||||
const Icon = menu.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={menu.relation}
|
|
||||||
className="flex items-center justify-between p-4 hover:bg-muted/10 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4 pr-4 min-w-0">
|
|
||||||
<div className="p-2 rounded-lg bg-secondary/50 text-foreground flex-shrink-0 mt-0.5">
|
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-semibold text-foreground">{menu.label}</span>
|
|
||||||
{(menu.relation === "ory_ssot" || menu.relation === "data_integrity") && (
|
|
||||||
<Badge variant="secondary" className="text-[10px] py-0.5 px-1.5 font-semibold text-destructive bg-destructive/10 border-destructive/20">
|
|
||||||
{t("ui.admin.permissions_direct.super_admin_only", "Super Admin 전용")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
|
||||||
{menu.desc}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
name={`system-menu-permission-${menu.relation}`}
|
|
||||||
value={permissionValue}
|
|
||||||
disabled={menu.relation === "ory_ssot" || menu.relation === "data_integrity"}
|
|
||||||
onChange={(e) => {
|
|
||||||
const nextVal = e.target.value as "none" | "read" | "write";
|
|
||||||
// 🌟 1단계: 로컬 임시 상태 즉시 갱신 (0ms 반응 보장)
|
|
||||||
setLocalSystemPermissions(prev => ({
|
|
||||||
...prev,
|
|
||||||
[selectedUser.userId]: {
|
|
||||||
...(prev[selectedUser.userId] ?? {}),
|
|
||||||
[menu.relation]: nextVal
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
// 🌟 2단계: 백그라운드 비동기 API 요청 수행
|
|
||||||
handleSystemRelationChange(
|
|
||||||
selectedUser.userId,
|
|
||||||
menu.relation,
|
|
||||||
permissionValue,
|
|
||||||
nextVal,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="flex h-9 w-[180px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
|
||||||
<option value="write">{t("ui.common.write", "수정 가능 (Write)")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center text-muted-foreground bg-muted/5 gap-3">
|
|
||||||
<ShieldCheck className="h-12 w-12 text-muted-foreground opacity-30" />
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
|
||||||
{t("ui.admin.permissions_direct.no_user_selected", "사용자가 선택되지 않았습니다.")}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs mt-1">
|
|
||||||
{t("msg.admin.permissions_direct.no_user_selected_desc", "왼쪽의 사용자 리스트에서 권한을 변경할 인원을 선택해 주세요.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* User Search Dialog for System relations */}
|
{/* User Search Dialog for System relations */}
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -537,7 +747,10 @@ export function TenantFineGrainedPermissionsPage() {
|
|||||||
<DialogContent className="sm:max-w-[500px]">
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl font-bold">
|
<DialogTitle className="text-xl font-bold">
|
||||||
{t("ui.admin.permissions_direct.dialog_title_system", "시스템 권한 관리 유저 추가")}
|
{t(
|
||||||
|
"ui.admin.permissions_direct.dialog_title_system",
|
||||||
|
"시스템 권한 관리 유저 추가",
|
||||||
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import { Plus, Search, ShieldCheck, Trash2, UserPlus } from "lucide-react";
|
||||||
Plus,
|
import { useEffect, useState } from "react";
|
||||||
Search,
|
|
||||||
ShieldCheck,
|
|
||||||
UserPlus,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
|
||||||
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 {
|
||||||
@@ -36,20 +30,22 @@ import {
|
|||||||
} from "../../../components/ui/table";
|
} from "../../../components/ui/table";
|
||||||
import { toast } from "../../../components/ui/use-toast";
|
import { toast } from "../../../components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
fetchUsers,
|
|
||||||
fetchTenantRelations,
|
|
||||||
addTenantRelation,
|
addTenantRelation,
|
||||||
|
fetchTenantRelations,
|
||||||
|
fetchUsers,
|
||||||
removeTenantRelation,
|
removeTenantRelation,
|
||||||
type TenantRelation,
|
type TenantRelation,
|
||||||
} from "../../../lib/adminApi";
|
} from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
import { Trash2 } from "lucide-react";
|
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||||
|
|
||||||
interface TenantFineGrainedPermissionsTabProps {
|
interface TenantFineGrainedPermissionsTabProps {
|
||||||
tenantIdProp?: string;
|
tenantIdProp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrainedPermissionsTabProps = {}) {
|
export function TenantFineGrainedPermissionsTab({
|
||||||
|
tenantIdProp,
|
||||||
|
}: TenantFineGrainedPermissionsTabProps = {}) {
|
||||||
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
||||||
const tenantId = tenantIdProp || tenantIdParam || "";
|
const tenantId = tenantIdProp || tenantIdParam || "";
|
||||||
const { hasPermission } = useTenantPermission(tenantId);
|
const { hasPermission } = useTenantPermission(tenantId);
|
||||||
@@ -59,26 +55,35 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
// 🌟 테넌트 탭별 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
// 🌟 테넌트 탭별 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
||||||
const [localTenantPermissions, setLocalTenantPermissions] = useState<Record<string, Record<string, "none" | "read" | "write">>>({});
|
const [localTenantPermissions, setLocalTenantPermissions] = useState<
|
||||||
|
Record<string, Record<string, "none" | "read" | "write">>
|
||||||
|
>({});
|
||||||
|
|
||||||
const relationsQuery = useQuery({
|
const relationsQuery = useQuery({
|
||||||
queryKey: ["tenant-relations", tenantId],
|
queryKey: ["tenant-relations", tenantId],
|
||||||
queryFn: () => fetchTenantRelations(tenantId),
|
queryFn: () => fetchTenantRelations(tenantId),
|
||||||
enabled: !!tenantId,
|
enabled: !!tenantId,
|
||||||
});
|
});
|
||||||
const relationsData = relationsQuery.data ?? [];
|
const _relationsData = relationsQuery.data ?? [];
|
||||||
|
|
||||||
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (relationsQuery.data) {
|
if (relationsQuery.data) {
|
||||||
const initialMap: Record<string, Record<string, "none" | "read" | "write">> = {};
|
const initialMap: Record<
|
||||||
|
string,
|
||||||
|
Record<string, "none" | "read" | "write">
|
||||||
|
> = {};
|
||||||
for (const user of relationsQuery.data) {
|
for (const user of relationsQuery.data) {
|
||||||
initialMap[user.userId] = {};
|
initialMap[user.userId] = {};
|
||||||
const tabs = ["profile", "permissions", "organization", "schema"];
|
const tabs = ["profile", "permissions", "organization", "schema"];
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
const isWrite = user.relations.includes(`${tab}_managers`);
|
const isWrite = user.relations.includes(`${tab}_managers`);
|
||||||
const isRead = user.relations.includes(`${tab}_viewers`);
|
const isRead = user.relations.includes(`${tab}_viewers`);
|
||||||
initialMap[user.userId][tab] = isWrite ? "write" : isRead ? "read" : "none";
|
initialMap[user.userId][tab] = isWrite
|
||||||
|
? "write"
|
||||||
|
: isRead
|
||||||
|
? "read"
|
||||||
|
: "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLocalTenantPermissions(initialMap);
|
setLocalTenantPermissions(initialMap);
|
||||||
@@ -91,7 +96,9 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["me"] });
|
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["tenant-relations", tenantId],
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["me"] });
|
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -101,31 +108,45 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
mutationFn: (payload: { userId: string; relation: string }) =>
|
mutationFn: (payload: { userId: string; relation: string }) =>
|
||||||
addTenantRelation(tenantId, payload.userId, payload.relation),
|
addTenantRelation(tenantId, payload.userId, payload.relation),
|
||||||
onMutate: async (newRelation) => {
|
onMutate: async (newRelation) => {
|
||||||
await queryClient.cancelQueries({ queryKey: ["tenant-relations", tenantId] });
|
await queryClient.cancelQueries({
|
||||||
const previousRelations = queryClient.getQueryData<TenantRelation[]>(["tenant-relations", tenantId]);
|
queryKey: ["tenant-relations", tenantId],
|
||||||
|
|
||||||
queryClient.setQueryData<TenantRelation[]>(["tenant-relations", tenantId], (old) => {
|
|
||||||
if (!old) return [];
|
|
||||||
return old.map((user) => {
|
|
||||||
if (user.userId === newRelation.userId) {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
relations: user.relations.includes(newRelation.relation)
|
|
||||||
? user.relations
|
|
||||||
: [...user.relations, newRelation.relation],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
const previousRelations = queryClient.getQueryData<TenantRelation[]>([
|
||||||
|
"tenant-relations",
|
||||||
|
tenantId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
queryClient.setQueryData<TenantRelation[]>(
|
||||||
|
["tenant-relations", tenantId],
|
||||||
|
(old) => {
|
||||||
|
if (!old) return [];
|
||||||
|
return old.map((user) => {
|
||||||
|
if (user.userId === newRelation.userId) {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
relations: user.relations.includes(newRelation.relation)
|
||||||
|
? user.relations
|
||||||
|
: [...user.relations, newRelation.relation],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return { previousRelations };
|
return { previousRelations };
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
||||||
if (context?.previousRelations) {
|
if (context?.previousRelations) {
|
||||||
queryClient.setQueryData(["tenant-relations", tenantId], context.previousRelations);
|
queryClient.setQueryData(
|
||||||
|
["tenant-relations", tenantId],
|
||||||
|
context.previousRelations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Quiet mutate
|
// Quiet mutate
|
||||||
@@ -136,29 +157,45 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
mutationFn: (payload: { userId: string; relation: string }) =>
|
mutationFn: (payload: { userId: string; relation: string }) =>
|
||||||
removeTenantRelation(tenantId, payload.userId, payload.relation),
|
removeTenantRelation(tenantId, payload.userId, payload.relation),
|
||||||
onMutate: async (targetRelation) => {
|
onMutate: async (targetRelation) => {
|
||||||
await queryClient.cancelQueries({ queryKey: ["tenant-relations", tenantId] });
|
await queryClient.cancelQueries({
|
||||||
const previousRelations = queryClient.getQueryData<TenantRelation[]>(["tenant-relations", tenantId]);
|
queryKey: ["tenant-relations", tenantId],
|
||||||
|
|
||||||
queryClient.setQueryData<TenantRelation[]>(["tenant-relations", tenantId], (old) => {
|
|
||||||
if (!old) return [];
|
|
||||||
return old.map((user) => {
|
|
||||||
if (user.userId === targetRelation.userId) {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
relations: user.relations.filter((r) => r !== targetRelation.relation),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
const previousRelations = queryClient.getQueryData<TenantRelation[]>([
|
||||||
|
"tenant-relations",
|
||||||
|
tenantId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
queryClient.setQueryData<TenantRelation[]>(
|
||||||
|
["tenant-relations", tenantId],
|
||||||
|
(old) => {
|
||||||
|
if (!old) return [];
|
||||||
|
return old.map((user) => {
|
||||||
|
if (user.userId === targetRelation.userId) {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
relations: user.relations.filter(
|
||||||
|
(r) => r !== targetRelation.relation,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return { previousRelations };
|
return { previousRelations };
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
onError: (err: AxiosError<{ error?: string }>, _, context) => {
|
||||||
if (context?.previousRelations) {
|
if (context?.previousRelations) {
|
||||||
queryClient.setQueryData(["tenant-relations", tenantId], context.previousRelations);
|
queryClient.setQueryData(
|
||||||
|
["tenant-relations", tenantId],
|
||||||
|
context.previousRelations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Quiet mutate
|
// Quiet mutate
|
||||||
@@ -180,7 +217,10 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
if (currentVal === "read") {
|
if (currentVal === "read") {
|
||||||
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||||
} else if (currentVal === "write") {
|
} else if (currentVal === "write") {
|
||||||
await removeRelationMutation.mutateAsync({ userId, relation: writeRel });
|
await removeRelationMutation.mutateAsync({
|
||||||
|
userId,
|
||||||
|
relation: writeRel,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newVal === "read") {
|
if (newVal === "read") {
|
||||||
@@ -192,14 +232,29 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
invalidateAllQueries();
|
invalidateAllQueries();
|
||||||
|
|
||||||
// 🌟 Trigger a single consolidated success toast at the very end
|
// 🌟 Trigger a single consolidated success toast at the very end
|
||||||
toast.success(t("msg.admin.tenants.relations.update_success", "세부 권한이 성공적으로 변경되었습니다."));
|
toast.success(
|
||||||
|
t(
|
||||||
|
"msg.admin.tenants.relations.update_success",
|
||||||
|
"세부 권한이 성공적으로 변경되었습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Individual mutations handle error toast via onError
|
// Individual mutations handle error toast via onError
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAllRelations = async (userId: string, userRelations: string[]) => {
|
const handleRemoveAllRelations = async (
|
||||||
if (!window.confirm(t("msg.admin.tenants.relations.remove_all_confirm", "이 사용자의 모든 세부 권한을 삭제하시겠습니까?"))) {
|
userId: string,
|
||||||
|
userRelations: string[],
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
t(
|
||||||
|
"msg.admin.tenants.relations.remove_all_confirm",
|
||||||
|
"이 사용자의 모든 세부 권한을 삭제하시겠습니까?",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const rel of userRelations) {
|
for (const rel of userRelations) {
|
||||||
@@ -215,11 +270,14 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleAddUser = (userId: string) => {
|
const handleAddUser = (userId: string) => {
|
||||||
addRelationMutation.mutate({ userId, relation: "profile_viewers" }, {
|
addRelationMutation.mutate(
|
||||||
onSettled: () => {
|
{ userId, relation: "profile_viewers" },
|
||||||
invalidateAllQueries();
|
{
|
||||||
}
|
onSettled: () => {
|
||||||
});
|
invalidateAllQueries();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
};
|
};
|
||||||
@@ -235,7 +293,10 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
<ShieldCheck className="h-6 w-6 text-primary" />
|
<ShieldCheck className="h-6 w-6 text-primary" />
|
||||||
{t("ui.admin.tenants.relations.title", "세부 권한 설정 (Fine-grained Permissions)")}
|
{t(
|
||||||
|
"ui.admin.tenants.relations.title",
|
||||||
|
"세부 권한 설정 (Fine-grained Permissions)",
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-muted-foreground">
|
<CardDescription className="text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
@@ -250,7 +311,10 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
>
|
>
|
||||||
<UserPlus className="mr-2 h-4 w-4" />
|
<UserPlus className="mr-2 h-4 w-4" />
|
||||||
{t("ui.admin.tenants.relations.add_button", "세부 권한 사용자 추가")}
|
{t(
|
||||||
|
"ui.admin.tenants.relations.add_button",
|
||||||
|
"세부 권한 사용자 추가",
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
@@ -258,58 +322,96 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-secondary/40">
|
<TableHeader className="bg-secondary/40">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="font-bold">{t("ui.common.name", "이름")}</TableHead>
|
<TableHead className="font-bold">
|
||||||
<TableHead className="font-bold">{t("ui.admin.tenants.detail.tab_profile", "테넌트 프로필")}</TableHead>
|
{t("ui.common.name", "이름")}
|
||||||
<TableHead className="font-bold">{t("ui.admin.tenants.detail.tab_permissions", "권한 관리")}</TableHead>
|
</TableHead>
|
||||||
<TableHead className="font-bold">{t("ui.admin.tenants.detail.tab_organization", "조직 관리")}</TableHead>
|
<TableHead className="font-bold">
|
||||||
<TableHead className="font-bold">{t("ui.admin.tenants.detail.tab_schema", "사용자 스키마")}</TableHead>
|
{t("ui.admin.tenants.detail.tab_profile", "테넌트 프로필")}
|
||||||
<TableHead className="font-bold text-center w-20">{t("ui.common.action", "작업")}</TableHead>
|
</TableHead>
|
||||||
|
<TableHead className="font-bold">
|
||||||
|
{t("ui.admin.tenants.detail.tab_permissions", "권한 관리")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-bold">
|
||||||
|
{t("ui.admin.tenants.detail.tab_organization", "조직 관리")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-bold">
|
||||||
|
{t("ui.admin.tenants.detail.tab_schema", "사용자 스키마")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-bold text-center w-20">
|
||||||
|
{t("ui.common.action", "작업")}
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{relations.length === 0 ? (
|
{relations.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center py-12 text-muted-foreground">
|
<TableCell
|
||||||
{t("msg.admin.tenants.relations.empty", "세부 권한이 지정된 사용자가 없습니다. 사용자를 추가해 설정하세요.")}
|
colSpan={6}
|
||||||
|
className="text-center py-12 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"msg.admin.tenants.relations.empty",
|
||||||
|
"세부 권한이 지정된 사용자가 없습니다. 사용자를 추가해 설정하세요.",
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
relations.map((user) => {
|
relations.map((user) => {
|
||||||
const profileVal = user.relations.includes("profile_managers")
|
const profileVal = user.relations.includes(
|
||||||
|
"profile_managers",
|
||||||
|
)
|
||||||
? "write"
|
? "write"
|
||||||
: user.relations.includes("profile_viewers")
|
: user.relations.includes("profile_viewers")
|
||||||
? "read"
|
? "read"
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
const permissionsVal = user.relations.includes("permissions_managers")
|
const permissionsVal = user.relations.includes(
|
||||||
|
"permissions_managers",
|
||||||
|
)
|
||||||
? "write"
|
? "write"
|
||||||
: user.relations.includes("permissions_viewers")
|
: user.relations.includes("permissions_viewers")
|
||||||
? "read"
|
? "read"
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
const organizationVal = user.relations.includes("organization_managers")
|
const organizationVal = user.relations.includes(
|
||||||
|
"organization_managers",
|
||||||
|
)
|
||||||
? "write"
|
? "write"
|
||||||
: user.relations.includes("organization_viewers")
|
: user.relations.includes("organization_viewers")
|
||||||
? "read"
|
? "read"
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
const schemaVal = user.relations.includes("schema_managers")
|
const schemaVal = user.relations.includes("schema_managers")
|
||||||
? "write"
|
? "write"
|
||||||
: user.relations.includes("schema_viewers")
|
: user.relations.includes("schema_viewers")
|
||||||
? "read"
|
? "read"
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
const curProfileVal = localTenantPermissions[user.userId]?.profile ?? profileVal;
|
const curProfileVal =
|
||||||
const curPermissionsVal = localTenantPermissions[user.userId]?.permissions ?? permissionsVal;
|
localTenantPermissions[user.userId]?.profile ??
|
||||||
const curOrganizationVal = localTenantPermissions[user.userId]?.organization ?? organizationVal;
|
profileVal;
|
||||||
const curSchemaVal = localTenantPermissions[user.userId]?.schema ?? schemaVal;
|
const curPermissionsVal =
|
||||||
|
localTenantPermissions[user.userId]?.permissions ??
|
||||||
|
permissionsVal;
|
||||||
|
const curOrganizationVal =
|
||||||
|
localTenantPermissions[user.userId]?.organization ??
|
||||||
|
organizationVal;
|
||||||
|
const curSchemaVal =
|
||||||
|
localTenantPermissions[user.userId]?.schema ?? schemaVal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={user.userId} className="hover:bg-muted/10 transition-colors">
|
<TableRow
|
||||||
|
key={user.userId}
|
||||||
|
className="hover:bg-muted/10 transition-colors"
|
||||||
|
>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-semibold text-foreground">{user.name}</span>
|
<span className="font-semibold text-foreground">
|
||||||
<span className="text-xs text-muted-foreground italic">{user.email}</span>
|
{user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground italic">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -319,13 +421,16 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
name={`tenant-fine-grained-profile-${user.userId}`}
|
name={`tenant-fine-grained-profile-${user.userId}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const nextVal = e.target.value as "none" | "read" | "write";
|
const nextVal = e.target.value as
|
||||||
setLocalTenantPermissions(prev => ({
|
| "none"
|
||||||
|
| "read"
|
||||||
|
| "write";
|
||||||
|
setLocalTenantPermissions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.userId]: {
|
[user.userId]: {
|
||||||
...(prev[user.userId] ?? {}),
|
...(prev[user.userId] ?? {}),
|
||||||
profile: nextVal
|
profile: nextVal,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
@@ -335,9 +440,15 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
{t("ui.common.none", "권한 없음")}
|
||||||
<option value="write">{t("ui.common.write", "수정 가능 (Write)")}</option>
|
</option>
|
||||||
|
<option value="read">
|
||||||
|
{t("ui.common.read", "조회 가능 (Read)")}
|
||||||
|
</option>
|
||||||
|
<option value="write">
|
||||||
|
{t("ui.common.write", "수정 가능 (Write)")}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -347,13 +458,16 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
name={`tenant-fine-grained-permissions-${user.userId}`}
|
name={`tenant-fine-grained-permissions-${user.userId}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const nextVal = e.target.value as "none" | "read" | "write";
|
const nextVal = e.target.value as
|
||||||
setLocalTenantPermissions(prev => ({
|
| "none"
|
||||||
|
| "read"
|
||||||
|
| "write";
|
||||||
|
setLocalTenantPermissions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.userId]: {
|
[user.userId]: {
|
||||||
...(prev[user.userId] ?? {}),
|
...(prev[user.userId] ?? {}),
|
||||||
permissions: nextVal
|
permissions: nextVal,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
@@ -363,9 +477,15 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
{t("ui.common.none", "권한 없음")}
|
||||||
<option value="write">{t("ui.common.write", "수정 가능 (Write)")}</option>
|
</option>
|
||||||
|
<option value="read">
|
||||||
|
{t("ui.common.read", "조회 가능 (Read)")}
|
||||||
|
</option>
|
||||||
|
<option value="write">
|
||||||
|
{t("ui.common.write", "수정 가능 (Write)")}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -375,13 +495,16 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
name={`tenant-fine-grained-organization-${user.userId}`}
|
name={`tenant-fine-grained-organization-${user.userId}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const nextVal = e.target.value as "none" | "read" | "write";
|
const nextVal = e.target.value as
|
||||||
setLocalTenantPermissions(prev => ({
|
| "none"
|
||||||
|
| "read"
|
||||||
|
| "write";
|
||||||
|
setLocalTenantPermissions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.userId]: {
|
[user.userId]: {
|
||||||
...(prev[user.userId] ?? {}),
|
...(prev[user.userId] ?? {}),
|
||||||
organization: nextVal
|
organization: nextVal,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
@@ -391,9 +514,15 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
{t("ui.common.none", "권한 없음")}
|
||||||
<option value="write">{t("ui.common.write", "수정 가능 (Write)")}</option>
|
</option>
|
||||||
|
<option value="read">
|
||||||
|
{t("ui.common.read", "조회 가능 (Read)")}
|
||||||
|
</option>
|
||||||
|
<option value="write">
|
||||||
|
{t("ui.common.write", "수정 가능 (Write)")}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -403,13 +532,16 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
name={`tenant-fine-grained-schema-${user.userId}`}
|
name={`tenant-fine-grained-schema-${user.userId}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const nextVal = e.target.value as "none" | "read" | "write";
|
const nextVal = e.target.value as
|
||||||
setLocalTenantPermissions(prev => ({
|
| "none"
|
||||||
|
| "read"
|
||||||
|
| "write";
|
||||||
|
setLocalTenantPermissions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.userId]: {
|
[user.userId]: {
|
||||||
...(prev[user.userId] ?? {}),
|
...(prev[user.userId] ?? {}),
|
||||||
schema: nextVal
|
schema: nextVal,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
@@ -419,9 +551,15 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
{t("ui.common.none", "권한 없음")}
|
||||||
<option value="write">{t("ui.common.write", "수정 가능 (Write)")}</option>
|
</option>
|
||||||
|
<option value="read">
|
||||||
|
{t("ui.common.read", "조회 가능 (Read)")}
|
||||||
|
</option>
|
||||||
|
<option value="write">
|
||||||
|
{t("ui.common.write", "수정 가능 (Write)")}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
@@ -429,7 +567,12 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
onClick={() => handleRemoveAllRelations(user.userId, user.relations)}
|
onClick={() =>
|
||||||
|
handleRemoveAllRelations(
|
||||||
|
user.userId,
|
||||||
|
user.relations,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -457,7 +600,10 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<DialogContent className="sm:max-w-[500px]">
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl font-bold">
|
<DialogTitle className="text-xl font-bold">
|
||||||
{t("ui.admin.tenants.relations.dialog_title", "세부 권한 관리 유저 추가")}
|
{t(
|
||||||
|
"ui.admin.tenants.relations.dialog_title",
|
||||||
|
"세부 권한 관리 유저 추가",
|
||||||
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t(
|
{t(
|
||||||
@@ -533,8 +679,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant={isAlreadyInMatrix ? "ghost" : "outline"}
|
variant={isAlreadyInMatrix ? "ghost" : "outline"}
|
||||||
disabled={
|
disabled={
|
||||||
isAlreadyInMatrix ||
|
isAlreadyInMatrix || addRelationMutation.isPending
|
||||||
addRelationMutation.isPending
|
|
||||||
}
|
}
|
||||||
onClick={() => handleAddUser(user.id)}
|
onClick={() => handleAddUser(user.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
|
||||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
import { Button } from "../../../components/ui/button";
|
import { Button } from "../../../components/ui/button";
|
||||||
@@ -63,6 +62,7 @@ import {
|
|||||||
removeGroupMember,
|
removeGroupMember,
|
||||||
} from "../../../lib/adminApi";
|
} from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
|
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||||
|
|
||||||
type UserGroupNode = GroupSummary & {
|
type UserGroupNode = GroupSummary & {
|
||||||
children: UserGroupNode[];
|
children: UserGroupNode[];
|
||||||
@@ -247,7 +247,8 @@ function TenantGroupsPage() {
|
|||||||
const _queryClient = useQueryClient();
|
const _queryClient = useQueryClient();
|
||||||
|
|
||||||
const { hasPermission } = useTenantPermission(tenantId);
|
const { hasPermission } = useTenantPermission(tenantId);
|
||||||
const isWritable = hasPermission("manage_organization") || hasPermission("manage");
|
const isWritable =
|
||||||
|
hasPermission("manage_organization") || hasPermission("manage");
|
||||||
const canView = hasPermission("view_organization") || hasPermission("view");
|
const canView = hasPermission("view_organization") || hasPermission("view");
|
||||||
|
|
||||||
const [newGroupName, setNewGroupName] = useState("");
|
const [newGroupName, setNewGroupName] = useState("");
|
||||||
@@ -502,7 +503,9 @@ function TenantGroupsPage() {
|
|||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => createMutation.mutate()}
|
onClick={() => createMutation.mutate()}
|
||||||
disabled={!newGroupName || createMutation.isPending || !isWritable}
|
disabled={
|
||||||
|
!newGroupName || createMutation.isPending || !isWritable
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("ui.admin.groups.form.submit", "생성하기")}
|
{t("ui.admin.groups.form.submit", "생성하기")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
} from "../../../../../common/core/utils";
|
} from "../../../../../common/core/utils";
|
||||||
import { SearchFilterBar } from "../../../../../common/ui/search-filter-bar";
|
import { SearchFilterBar } from "../../../../../common/ui/search-filter-bar";
|
||||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||||
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
|
||||||
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 {
|
||||||
@@ -378,7 +377,9 @@ function TenantListPage() {
|
|||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
});
|
});
|
||||||
const profileRole = normalizeAdminRole(profile?.role);
|
const profileRole = normalizeAdminRole(profile?.role);
|
||||||
const isWritable = profileRole === "super_admin" || !!profile?.systemPermissions?.manage_tenants;
|
const isWritable =
|
||||||
|
profileRole === "super_admin" ||
|
||||||
|
!!profile?.systemPermissions?.manage_tenants;
|
||||||
|
|
||||||
const query = useInfiniteQuery({
|
const query = useInfiniteQuery({
|
||||||
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
|
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
|
||||||
@@ -583,7 +584,11 @@ function TenantListPage() {
|
|||||||
return () => window.removeEventListener("message", onMessage);
|
return () => window.removeEventListener("message", onMessage);
|
||||||
}, [allTenants, scopePickerOpen]);
|
}, [allTenants, scopePickerOpen]);
|
||||||
|
|
||||||
if (profile && profileRole !== "super_admin" && !profile?.systemPermissions?.tenants) {
|
if (
|
||||||
|
profile &&
|
||||||
|
profileRole !== "super_admin" &&
|
||||||
|
!profile?.systemPermissions?.tenants
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||||
<h3 className="text-lg font-bold">
|
<h3 className="text-lg font-bold">
|
||||||
|
|||||||
@@ -276,13 +276,21 @@ export function TenantProfilePage() {
|
|||||||
{t("ui.admin.tenants.profile.name", "테넌트 이름")}{" "}
|
{t("ui.admin.tenants.profile.name", "테넌트 이름")}{" "}
|
||||||
<span className="text-destructive">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input value={name} onChange={(e) => setName(e.target.value)} disabled={!isWritable} />
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
disabled={!isWritable}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="tenant-slug-slot" className="space-y-1">
|
<div data-testid="tenant-slug-slot" className="space-y-1">
|
||||||
<Label className="text-sm font-semibold">
|
<Label className="text-sm font-semibold">
|
||||||
{t("ui.admin.tenants.profile.slug", "슬러그 (Slug)")}
|
{t("ui.admin.tenants.profile.slug", "슬러그 (Slug)")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input value={slug} onChange={(e) => setSlug(e.target.value)} disabled={!isWritable} />
|
<Input
|
||||||
|
value={slug}
|
||||||
|
onChange={(e) => setSlug(e.target.value)}
|
||||||
|
disabled={!isWritable}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="tenant-parent-picker-slot" className="min-w-0">
|
<div data-testid="tenant-parent-picker-slot" className="min-w-0">
|
||||||
<ParentTenantSelector
|
<ParentTenantSelector
|
||||||
@@ -504,7 +512,9 @@ export function TenantProfilePage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleteMutation.isPending || isProtectedSeedTenant || !isWritable}
|
disabled={
|
||||||
|
deleteMutation.isPending || isProtectedSeedTenant || !isWritable
|
||||||
|
}
|
||||||
title={
|
title={
|
||||||
isProtectedSeedTenant
|
isProtectedSeedTenant
|
||||||
? t(
|
? t(
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export function TenantSchemaPage() {
|
|||||||
const { tenantId } = useParams<{ tenantId: string }>();
|
const { tenantId } = useParams<{ tenantId: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { hasPermission, isLoading: isPermissionLoading } = useTenantPermission(tenantId ?? "");
|
const { hasPermission, isLoading: isPermissionLoading } = useTenantPermission(
|
||||||
|
tenantId ?? "",
|
||||||
|
);
|
||||||
const canView = hasPermission("view_schema") || hasPermission("view");
|
const canView = hasPermission("view_schema") || hasPermission("view");
|
||||||
const isWritable = hasPermission("manage_schema") || hasPermission("manage");
|
const isWritable = hasPermission("manage_schema") || hasPermission("manage");
|
||||||
|
|
||||||
@@ -393,7 +395,9 @@ export function TenantSchemaPage() {
|
|||||||
<div className="flex justify-end pt-2">
|
<div className="flex justify-end pt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateMutation.mutate(fields)}
|
onClick={() => updateMutation.mutate(fields)}
|
||||||
disabled={updateMutation.isPending || tenantQuery.isLoading || !isWritable}
|
disabled={
|
||||||
|
updateMutation.isPending || tenantQuery.isLoading || !isWritable
|
||||||
|
}
|
||||||
className="px-8 h-11"
|
className="px-8 h-11"
|
||||||
>
|
>
|
||||||
<Save size={18} className="mr-2" />
|
<Save size={18} className="mr-2" />
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ function UserCreatePage() {
|
|||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
});
|
});
|
||||||
const profileRole = normalizeAdminRole(profile?.role);
|
const profileRole = normalizeAdminRole(profile?.role);
|
||||||
const canManageUsers = canManageTenantScopedUsers(profile) || !!profile?.systemPermissions?.manage_users;
|
const canManageUsers =
|
||||||
|
canManageTenantScopedUsers(profile) ||
|
||||||
|
!!profile?.systemPermissions?.manage_users;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
|||||||
@@ -653,8 +653,17 @@ function UserDetailPage() {
|
|||||||
const isAdmin = profileRole === "super_admin";
|
const isAdmin = profileRole === "super_admin";
|
||||||
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
|
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
|
||||||
const canManageCurrentUser = canManageUserInTenantScope({ profile, user });
|
const canManageCurrentUser = canManageUserInTenantScope({ profile, user });
|
||||||
const isWritable = isAdmin || isSelf || canManageCurrentUser || !!profile?.systemPermissions?.manage_users;
|
const isWritable =
|
||||||
const canViewUser = isAdmin || isSelf || canManageCurrentUser || !!profile?.systemPermissions?.users || !!profile?.systemPermissions?.manage_users;
|
isAdmin ||
|
||||||
|
isSelf ||
|
||||||
|
canManageCurrentUser ||
|
||||||
|
!!profile?.systemPermissions?.manage_users;
|
||||||
|
const canViewUser =
|
||||||
|
isAdmin ||
|
||||||
|
isSelf ||
|
||||||
|
canManageCurrentUser ||
|
||||||
|
!!profile?.systemPermissions?.users ||
|
||||||
|
!!profile?.systemPermissions?.manage_users;
|
||||||
const watchedStatus = watch("status");
|
const watchedStatus = watch("status");
|
||||||
|
|
||||||
const [newSubEmail, setNewSubEmail] = React.useState("");
|
const [newSubEmail, setNewSubEmail] = React.useState("");
|
||||||
|
|||||||
@@ -381,7 +381,8 @@ function UserListPage() {
|
|||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
});
|
});
|
||||||
const profileRole = normalizeAdminRole(profile?.role);
|
const profileRole = normalizeAdminRole(profile?.role);
|
||||||
const isWritable = profileRole === "super_admin" || !!profile?.systemPermissions?.manage_users;
|
const isWritable =
|
||||||
|
profileRole === "super_admin" || !!profile?.systemPermissions?.manage_users;
|
||||||
|
|
||||||
const { data: tenantsData } = useQuery({
|
const { data: tenantsData } = useQuery({
|
||||||
queryKey: ["tenants", "all"],
|
queryKey: ["tenants", "all"],
|
||||||
|
|||||||
@@ -541,20 +541,14 @@ export async function fetchSystemRelations() {
|
|||||||
return data.items;
|
return data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSystemRelation(
|
export async function addSystemRelation(userId: string, relation: string) {
|
||||||
userId: string,
|
|
||||||
relation: string,
|
|
||||||
) {
|
|
||||||
await apiClient.post(`/v1/admin/system/relations`, {
|
await apiClient.post(`/v1/admin/system/relations`, {
|
||||||
userId,
|
userId,
|
||||||
relation,
|
relation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSystemRelation(
|
export async function removeSystemRelation(userId: string, relation: string) {
|
||||||
userId: string,
|
|
||||||
relation: string,
|
|
||||||
) {
|
|
||||||
await apiClient.delete(`/v1/admin/system/relations`, {
|
await apiClient.delete(`/v1/admin/system/relations`, {
|
||||||
data: { userId, relation },
|
data: { userId, relation },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { expect, test } from "@playwright/test";
|
|||||||
|
|
||||||
test.describe("Authentication", () => {
|
test.describe("Authentication", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
page.on("console", (msg) => console.log("BROWSER LOG:", msg.text()));
|
||||||
|
page.on("pageerror", (err) =>
|
||||||
|
console.error("BROWSER EXCEPTION:", err.message),
|
||||||
|
);
|
||||||
// 1. Force state
|
// 1. Force state
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript(() => {
|
||||||
window.localStorage.setItem("locale", "ko");
|
window.localStorage.setItem("locale", "ko");
|
||||||
@@ -70,8 +74,24 @@ test.describe("Authentication", () => {
|
|||||||
|
|
||||||
// 3. Catch-all for others
|
// 3. Catch-all for others
|
||||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||||
|
if (route.request().url().includes("/user/me")) {
|
||||||
|
return route.fallback();
|
||||||
|
}
|
||||||
if (route.request().method() === "GET") {
|
if (route.request().method() === "GET") {
|
||||||
await route.fulfill({ json: { items: [], total: 0 } });
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
summary: {
|
||||||
|
failures: 0,
|
||||||
|
warnings: 0,
|
||||||
|
pass: 0,
|
||||||
|
success: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
sections: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await route.fulfill({ status: 200, json: {} });
|
await route.fulfill({ status: 200, json: {} });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,9 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("세부 기능 권한(System Permissions)을 가진 비-슈퍼어드민", () => {
|
test.describe("세부 기능 권한(System Permissions)을 가진 비-슈퍼어드민", () => {
|
||||||
test("테넌트 조회 권한(tenants)이 있을 때 테넌트 목록 페이지 진입 가능 및 쓰기 기능 제한 확인", async ({ page }) => {
|
test("테넌트 조회 권한(tenants)이 있을 때 테넌트 목록 페이지 진입 가능 및 쓰기 기능 제한 확인", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
await setupAuth(page, "tenant_admin", {
|
await setupAuth(page, "tenant_admin", {
|
||||||
tenantId: "t1",
|
tenantId: "t1",
|
||||||
tenantSlug: "t1",
|
tenantSlug: "t1",
|
||||||
@@ -315,18 +317,24 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
|
|||||||
|
|
||||||
// 차단 메시지 비노출 확인
|
// 차단 메시지 비노출 확인
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/접근 권한이 없습니다|이 작업을 수행할 권한이 없습니다/i),
|
page.getByText(
|
||||||
|
/접근 권한이 없습니다|이 작업을 수행할 권한이 없습니다/i,
|
||||||
|
),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
// "테넌트 1" 목록 노출 확인
|
// "테넌트 1" 목록 노출 확인
|
||||||
await expect(page.getByText("테넌트 1")).toBeVisible();
|
await expect(page.getByText("테넌트 1")).toBeVisible();
|
||||||
|
|
||||||
// 수정 권한(manage_tenants)이 없으므로 쓰기 버튼 비노출 확인
|
// 수정 권한(manage_tenants)이 없으므로 쓰기 버튼 비노출 확인
|
||||||
await expect(page.getByRole("link", { name: /테넌트 추가/i })).not.toBeVisible();
|
await expect(
|
||||||
|
page.getByRole("link", { name: /테넌트 추가/i }),
|
||||||
|
).not.toBeVisible();
|
||||||
await expect(page.getByTestId("tenant-data-mgmt-btn")).not.toBeVisible();
|
await expect(page.getByTestId("tenant-data-mgmt-btn")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("테넌트 관리 권한(manage_tenants)까지 있을 때 테넌트 추가 및 데이터 관리 버튼 활성화 확인", async ({ page }) => {
|
test("테넌트 관리 권한(manage_tenants)까지 있을 때 테넌트 추가 및 데이터 관리 버튼 활성화 확인", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
await setupAuth(page, "tenant_admin", {
|
await setupAuth(page, "tenant_admin", {
|
||||||
tenantId: "t1",
|
tenantId: "t1",
|
||||||
tenantSlug: "t1",
|
tenantSlug: "t1",
|
||||||
@@ -341,7 +349,9 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
|
|||||||
await expect(page.getByText("테넌트 1")).toBeVisible();
|
await expect(page.getByText("테넌트 1")).toBeVisible();
|
||||||
|
|
||||||
// 수정 권한(manage_tenants)이 있으므로 쓰기 버튼(테넌트 추가, 데이터 관리) 노출 확인
|
// 수정 권한(manage_tenants)이 있으므로 쓰기 버튼(테넌트 추가, 데이터 관리) 노출 확인
|
||||||
await expect(page.getByRole("link", { name: /테넌트 추가/i })).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole("link", { name: /테넌트 추가/i }),
|
||||||
|
).toBeVisible();
|
||||||
await expect(page.getByTestId("tenant-data-mgmt-btn")).toBeVisible();
|
await expect(page.getByTestId("tenant-data-mgmt-btn")).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,17 +70,17 @@ type SignupRequest struct {
|
|||||||
// User Profile Models
|
// User Profile Models
|
||||||
|
|
||||||
type SystemPermissions struct {
|
type SystemPermissions struct {
|
||||||
Overview bool `json:"overview"`
|
Overview bool `json:"overview"`
|
||||||
Tenants bool `json:"tenants"`
|
Tenants bool `json:"tenants"`
|
||||||
OrgChart bool `json:"org_chart"`
|
OrgChart bool `json:"org_chart"`
|
||||||
Worksmobile bool `json:"worksmobile"`
|
Worksmobile bool `json:"worksmobile"`
|
||||||
OrySSOT bool `json:"ory_ssot"`
|
OrySSOT bool `json:"ory_ssot"`
|
||||||
DataIntegrity bool `json:"data_integrity"`
|
DataIntegrity bool `json:"data_integrity"`
|
||||||
Users bool `json:"users"`
|
Users bool `json:"users"`
|
||||||
PermissionsDirect bool `json:"permissions_direct"`
|
PermissionsDirect bool `json:"permissions_direct"`
|
||||||
AuthGuard bool `json:"auth_guard"`
|
AuthGuard bool `json:"auth_guard"`
|
||||||
ApiKeys bool `json:"api_keys"`
|
ApiKeys bool `json:"api_keys"`
|
||||||
AuditLogs bool `json:"audit_logs"`
|
AuditLogs bool `json:"audit_logs"`
|
||||||
|
|
||||||
ManageOverview bool `json:"manage_overview"`
|
ManageOverview bool `json:"manage_overview"`
|
||||||
ManageTenants bool `json:"manage_tenants"`
|
ManageTenants bool `json:"manage_tenants"`
|
||||||
|
|||||||
@@ -3263,14 +3263,14 @@ func (h *TenantHandler) ListRelations(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allowedRelations := map[string]bool{
|
allowedRelations := map[string]bool{
|
||||||
"profile_viewers": true,
|
"profile_viewers": true,
|
||||||
"profile_managers": true,
|
"profile_managers": true,
|
||||||
"permissions_viewers": true,
|
"permissions_viewers": true,
|
||||||
"permissions_managers": true,
|
"permissions_managers": true,
|
||||||
"organization_viewers": true,
|
"organization_viewers": true,
|
||||||
"organization_managers": true,
|
"organization_managers": true,
|
||||||
"schema_viewers": true,
|
"schema_viewers": true,
|
||||||
"schema_managers": true,
|
"schema_managers": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
type userRelationInfo struct {
|
type userRelationInfo struct {
|
||||||
@@ -3308,7 +3308,7 @@ func (h *TenantHandler) ListRelations(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == "Unknown" && email == "Unknown" && h.UserRepo != nil {
|
if name == "Unknown" && email == "Unknown" && h.UserRepo != nil {
|
||||||
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
||||||
if err == nil && user != nil {
|
if err == nil && user != nil {
|
||||||
@@ -3349,14 +3349,14 @@ func (h *TenantHandler) AddRelation(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allowedRelations := map[string]bool{
|
allowedRelations := map[string]bool{
|
||||||
"profile_viewers": true,
|
"profile_viewers": true,
|
||||||
"profile_managers": true,
|
"profile_managers": true,
|
||||||
"permissions_viewers": true,
|
"permissions_viewers": true,
|
||||||
"permissions_managers": true,
|
"permissions_managers": true,
|
||||||
"organization_viewers": true,
|
"organization_viewers": true,
|
||||||
"organization_managers": true,
|
"organization_managers": true,
|
||||||
"schema_viewers": true,
|
"schema_viewers": true,
|
||||||
"schema_managers": true,
|
"schema_managers": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowedRelations[req.Relation] {
|
if !allowedRelations[req.Relation] {
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
|
||||||
|
[msg]
|
||||||
|
|
||||||
|
[msg.admin]
|
||||||
|
|
||||||
|
[msg.admin.audit]
|
||||||
|
subtitle = "View administrator activity history."
|
||||||
|
|
||||||
[msg.common]
|
[msg.common]
|
||||||
loading_more = "Loading more logs..."
|
|
||||||
copied = "Copied."
|
copied = "Copied."
|
||||||
error = "Error"
|
error = "Error"
|
||||||
forbidden = "Access denied."
|
forbidden = "Access denied."
|
||||||
loading = "Loading..."
|
loading = "Loading..."
|
||||||
no_results = "No results found."
|
loading_more = "Loading more logs..."
|
||||||
no_description = "No Description."
|
no_description = "No Description."
|
||||||
|
no_results = "No results found."
|
||||||
parsing = "Parsing data..."
|
parsing = "Parsing data..."
|
||||||
requesting = "Requesting..."
|
requesting = "Requesting..."
|
||||||
saving = "Saving..."
|
saving = "Saving..."
|
||||||
@@ -20,20 +28,38 @@ loading = "Loading audit logs..."
|
|||||||
[msg.common.audit.registry]
|
[msg.common.audit.registry]
|
||||||
count = "{{count}} logs"
|
count = "{{count}} logs"
|
||||||
|
|
||||||
[msg.admin.audit]
|
[msg.dev]
|
||||||
subtitle = "View administrator activity history."
|
|
||||||
|
|
||||||
[msg.dev.audit]
|
[msg.dev.audit]
|
||||||
subtitle = "View developer activity history within the current app scope."
|
subtitle = "View developer activity history within the current app scope."
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
|
||||||
|
[ui.admin]
|
||||||
|
|
||||||
|
[ui.admin.integrity]
|
||||||
|
fetch_error = "Unable to load the final integrity check result."
|
||||||
|
|
||||||
|
[ui.admin.integrity.section]
|
||||||
|
tenant_integrity = "Tenant integrity"
|
||||||
|
user_integrity = "User integrity"
|
||||||
|
|
||||||
|
[ui.admin.integrity.summary]
|
||||||
|
failures_text = "Failures {{count}}"
|
||||||
|
title = "Final integrity check"
|
||||||
|
|
||||||
|
[ui.admin.overview]
|
||||||
|
|
||||||
|
[ui.admin.overview.chart]
|
||||||
|
description = "Check the graph by all or selected organizations."
|
||||||
|
title = "Login request status by company and app"
|
||||||
|
|
||||||
[ui.common]
|
[ui.common]
|
||||||
no_results = "No results to display."
|
action = "Action"
|
||||||
apply = "Apply"
|
|
||||||
actions = "Actions"
|
actions = "Actions"
|
||||||
add = "Add"
|
add = "Add"
|
||||||
all = "All"
|
|
||||||
apply = "Apply"
|
|
||||||
admin_only = "Admin Only"
|
admin_only = "Admin Only"
|
||||||
|
all = "All"
|
||||||
apply = "Apply"
|
apply = "Apply"
|
||||||
approve = "Approve"
|
approve = "Approve"
|
||||||
assign = "Assign"
|
assign = "Assign"
|
||||||
@@ -61,42 +87,45 @@ export_with_ids = "Include UUID"
|
|||||||
export_without_ids = "Export without UUID"
|
export_without_ids = "Export without UUID"
|
||||||
fail = "Fail"
|
fail = "Fail"
|
||||||
go_home = "Go Home"
|
go_home = "Go Home"
|
||||||
info = "Info"
|
|
||||||
view = "View"
|
|
||||||
hyphen = "-"
|
hyphen = "-"
|
||||||
|
info = "Info"
|
||||||
|
language = "Language"
|
||||||
|
language_en = "English"
|
||||||
|
language_ko = "Korean"
|
||||||
|
load_more = "Load more"
|
||||||
loading = "Loading..."
|
loading = "Loading..."
|
||||||
manage = "Manage"
|
manage = "Manage"
|
||||||
move = "Move"
|
move = "Move"
|
||||||
move_org = "Move to another organization"
|
move_org = "Move to another organization"
|
||||||
na = "N/A"
|
na = "N/A"
|
||||||
|
name = "Name"
|
||||||
never = "Never"
|
never = "Never"
|
||||||
next = "Next"
|
next = "Next"
|
||||||
|
no_results = "No results to display."
|
||||||
none = "None"
|
none = "None"
|
||||||
page_of = "Page {{page}} of {{total}}"
|
page_of = "Page {{page}} of {{total}}"
|
||||||
prev = "Prev"
|
prev = "Prev"
|
||||||
previous = "Previous"
|
previous = "Previous"
|
||||||
qr = "QR"
|
qr = "QR"
|
||||||
reject = "Reject"
|
read = "Read"
|
||||||
rejected = "Rejected"
|
|
||||||
reset = "Reset"
|
|
||||||
read_only = "Read Only"
|
read_only = "Read Only"
|
||||||
refresh = "Refresh"
|
refresh = "Refresh"
|
||||||
|
reject = "Reject"
|
||||||
|
rejected = "Rejected"
|
||||||
remove = "Remove"
|
remove = "Remove"
|
||||||
remove_org = "Remove from organization"
|
remove_org = "Remove from organization"
|
||||||
resend = "Resend"
|
resend = "Resend"
|
||||||
|
reset = "Reset"
|
||||||
retry = "Retry"
|
retry = "Retry"
|
||||||
row = "Row"
|
row = "Row"
|
||||||
save = "Save"
|
save = "Save"
|
||||||
search = "Search"
|
search = "Search"
|
||||||
search_group = "Search groups..."
|
search_group = "Search groups..."
|
||||||
|
searching = "Searching..."
|
||||||
select = "Select"
|
select = "Select"
|
||||||
select_file = "Select File"
|
select_file = "Select File"
|
||||||
select_placeholder = "Select Placeholder"
|
select_placeholder = "Select Placeholder"
|
||||||
load_more = "Load more"
|
|
||||||
show_more = "Show More"
|
show_more = "Show More"
|
||||||
language = "Language"
|
|
||||||
language_ko = "Korean"
|
|
||||||
language_en = "English"
|
|
||||||
submit = "Submit"
|
submit = "Submit"
|
||||||
submitting = "Submitting..."
|
submitting = "Submitting..."
|
||||||
success = "Success"
|
success = "Success"
|
||||||
@@ -105,6 +134,8 @@ theme_light = "Light"
|
|||||||
theme_toggle = "Theme Toggle"
|
theme_toggle = "Theme Toggle"
|
||||||
unassigned = "Unassigned"
|
unassigned = "Unassigned"
|
||||||
unknown = "Unknown"
|
unknown = "Unknown"
|
||||||
|
view = "View"
|
||||||
|
write = "Write"
|
||||||
|
|
||||||
[ui.common.audit]
|
[ui.common.audit]
|
||||||
load_more = "Load more"
|
load_more = "Load more"
|
||||||
@@ -114,12 +145,6 @@ title = "Audit Logs"
|
|||||||
actor_id = "Copy User ID"
|
actor_id = "Copy User ID"
|
||||||
target = "Copy Client ID"
|
target = "Copy Client ID"
|
||||||
|
|
||||||
[ui.common.audit.filters]
|
|
||||||
user_id = "Filter by User ID"
|
|
||||||
client_id = "Filter by Client ID"
|
|
||||||
action = "Filter by Action (e.g. ROTATE_SECRET)"
|
|
||||||
status_all = "All Status"
|
|
||||||
|
|
||||||
[ui.common.audit.details]
|
[ui.common.audit.details]
|
||||||
actor = "User ID"
|
actor = "User ID"
|
||||||
actor_id = "User ID · {{value}}"
|
actor_id = "User ID · {{value}}"
|
||||||
@@ -135,24 +160,38 @@ path = "Path · {{value}}"
|
|||||||
request = "Request"
|
request = "Request"
|
||||||
request_id = "Request ID · {{value}}"
|
request_id = "Request ID · {{value}}"
|
||||||
result = "Result"
|
result = "Result"
|
||||||
tenant = "Tenant · {{value}}"
|
|
||||||
target = "Client ID · {{value}}"
|
target = "Client ID · {{value}}"
|
||||||
|
tenant = "Tenant · {{value}}"
|
||||||
|
|
||||||
|
[ui.common.audit.filters]
|
||||||
|
action = "Filter by Action (e.g. ROTATE_SECRET)"
|
||||||
|
client_id = "Filter by Client ID"
|
||||||
|
status_all = "All Status"
|
||||||
|
user_id = "Filter by User ID"
|
||||||
|
|
||||||
[ui.common.audit.registry]
|
[ui.common.audit.registry]
|
||||||
title = "Audit registry"
|
title = "Audit registry"
|
||||||
|
|
||||||
[ui.common.audit.table]
|
[ui.common.audit.table]
|
||||||
no_logs = "No logs to display."
|
|
||||||
action = "Action"
|
action = "Action"
|
||||||
actor = "User ID"
|
actor = "User ID"
|
||||||
client_id = "Client ID"
|
client_id = "Client ID"
|
||||||
user_id = "User ID"
|
no_logs = "No logs to display."
|
||||||
status = "Status"
|
status = "Status"
|
||||||
target = "Client ID"
|
target = "Client ID"
|
||||||
time = "Time"
|
time = "Time"
|
||||||
|
user_id = "User ID"
|
||||||
|
|
||||||
[ui.common.overview]
|
[ui.common.badge]
|
||||||
title = "Operational Status"
|
admin_only = "Admin only"
|
||||||
|
command_only = "Command only"
|
||||||
|
system = "System"
|
||||||
|
|
||||||
|
[ui.common.chart]
|
||||||
|
|
||||||
|
[ui.common.chart.axis]
|
||||||
|
x = "X-axis: Period"
|
||||||
|
y = "Y-axis: Login Requests"
|
||||||
|
|
||||||
[ui.common.chart.period]
|
[ui.common.chart.period]
|
||||||
day = "Day"
|
day = "Day"
|
||||||
@@ -162,29 +201,12 @@ week = "Week"
|
|||||||
[ui.common.chart.series_summary]
|
[ui.common.chart.series_summary]
|
||||||
login_users = "Login {{login}} / Users {{subjects}}"
|
login_users = "Login {{login}} / Users {{subjects}}"
|
||||||
|
|
||||||
[ui.common.chart.axis]
|
[ui.common.custom_claim_permission]
|
||||||
x = "X-axis: Period"
|
|
||||||
y = "Y-axis: Login Requests"
|
|
||||||
|
|
||||||
[ui.admin.integrity]
|
|
||||||
fetch_error = "Unable to load the final integrity check result."
|
|
||||||
|
|
||||||
[ui.admin.integrity.summary]
|
|
||||||
failures_text = "Failures {{count}}"
|
|
||||||
title = "Final integrity check"
|
|
||||||
|
|
||||||
[ui.admin.integrity.section]
|
|
||||||
tenant_integrity = "Tenant integrity"
|
|
||||||
user_integrity = "User integrity"
|
|
||||||
|
|
||||||
[ui.admin.overview.chart]
|
|
||||||
description = "Check the graph by all or selected organizations."
|
|
||||||
title = "Login request status by company and app"
|
|
||||||
|
|
||||||
[ui.common.badge]
|
|
||||||
admin_only = "Admin only"
|
admin_only = "Admin only"
|
||||||
command_only = "Command only"
|
user_and_admin = "User and admin"
|
||||||
system = "System"
|
|
||||||
|
[ui.common.overview]
|
||||||
|
title = "Operational Status"
|
||||||
|
|
||||||
[ui.common.status]
|
[ui.common.status]
|
||||||
active = "Active"
|
active = "Active"
|
||||||
@@ -197,10 +219,3 @@ pending = "Pending"
|
|||||||
success = "Success"
|
success = "Success"
|
||||||
unchanged = "Unchanged"
|
unchanged = "Unchanged"
|
||||||
updated = "Updated"
|
updated = "Updated"
|
||||||
|
|
||||||
[ui.common]
|
|
||||||
searching = "Searching..."
|
|
||||||
|
|
||||||
[ui.common.custom_claim_permission]
|
|
||||||
admin_only = "Admin only"
|
|
||||||
user_and_admin = "User and admin"
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
|
||||||
|
[msg]
|
||||||
|
|
||||||
|
[msg.admin]
|
||||||
|
|
||||||
|
[msg.admin.audit]
|
||||||
|
subtitle = "관리자 작업 이력을 조회합니다."
|
||||||
|
|
||||||
[msg.common]
|
[msg.common]
|
||||||
loading_more = "추가 로그를 불러오는 중..."
|
|
||||||
copied = "복사되었습니다."
|
copied = "복사되었습니다."
|
||||||
error = "오류가 발생했습니다."
|
error = "오류가 발생했습니다."
|
||||||
forbidden = "접근 권한이 없습니다."
|
forbidden = "접근 권한이 없습니다."
|
||||||
loading = "로딩 중..."
|
loading = "로딩 중..."
|
||||||
no_results = "검색 결과가 없습니다."
|
loading_more = "추가 로그를 불러오는 중..."
|
||||||
no_description = "설명이 없습니다."
|
no_description = "설명이 없습니다."
|
||||||
|
no_results = "검색 결과가 없습니다."
|
||||||
parsing = "데이터 파싱 중..."
|
parsing = "데이터 파싱 중..."
|
||||||
requesting = "요청 중..."
|
requesting = "요청 중..."
|
||||||
saving = "저장 중..."
|
saving = "저장 중..."
|
||||||
@@ -20,20 +28,38 @@ loading = "Loading audit logs..."
|
|||||||
[msg.common.audit.registry]
|
[msg.common.audit.registry]
|
||||||
count = "총 {{count}}개 로그"
|
count = "총 {{count}}개 로그"
|
||||||
|
|
||||||
[msg.admin.audit]
|
[msg.dev]
|
||||||
subtitle = "관리자 작업 이력을 조회합니다."
|
|
||||||
|
|
||||||
[msg.dev.audit]
|
[msg.dev.audit]
|
||||||
subtitle = "현재 앱 범위의 개발자 작업 이력을 조회합니다."
|
subtitle = "현재 앱 범위의 개발자 작업 이력을 조회합니다."
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
|
||||||
|
[ui.admin]
|
||||||
|
|
||||||
|
[ui.admin.integrity]
|
||||||
|
fetch_error = "정합성 최종 검증 결과를 불러오지 못했습니다."
|
||||||
|
|
||||||
|
[ui.admin.integrity.section]
|
||||||
|
tenant_integrity = "테넌트 정합성"
|
||||||
|
user_integrity = "사용자 정합성"
|
||||||
|
|
||||||
|
[ui.admin.integrity.summary]
|
||||||
|
failures_text = "실패 {{count}}건"
|
||||||
|
title = "정합성 최종 검증"
|
||||||
|
|
||||||
|
[ui.admin.overview]
|
||||||
|
|
||||||
|
[ui.admin.overview.chart]
|
||||||
|
description = "전체 또는 선택한 조직 기준으로 그래프를 확인합니다."
|
||||||
|
title = "회사별 앱별 로그인 요청 현황"
|
||||||
|
|
||||||
[ui.common]
|
[ui.common]
|
||||||
no_results = "표시할 결과가 없습니다."
|
action = "작업"
|
||||||
apply = "적용"
|
|
||||||
actions = "액션"
|
actions = "액션"
|
||||||
add = "추가"
|
add = "추가"
|
||||||
all = "전체"
|
|
||||||
apply = "적용"
|
|
||||||
admin_only = "관리자 전용"
|
admin_only = "관리자 전용"
|
||||||
|
all = "전체"
|
||||||
apply = "적용"
|
apply = "적용"
|
||||||
approve = "승인"
|
approve = "승인"
|
||||||
assign = "할당"
|
assign = "할당"
|
||||||
@@ -61,42 +87,45 @@ export_with_ids = "UUID 포함"
|
|||||||
export_without_ids = "UUID 제외 내보내기"
|
export_without_ids = "UUID 제외 내보내기"
|
||||||
fail = "실패"
|
fail = "실패"
|
||||||
go_home = "홈으로"
|
go_home = "홈으로"
|
||||||
info = "상세 안내"
|
|
||||||
view = "보기"
|
|
||||||
hyphen = "-"
|
hyphen = "-"
|
||||||
|
info = "상세 안내"
|
||||||
|
language = "언어"
|
||||||
|
language_en = "English"
|
||||||
|
language_ko = "한국어"
|
||||||
|
load_more = "더 보기"
|
||||||
loading = "로딩 중..."
|
loading = "로딩 중..."
|
||||||
manage = "관리"
|
manage = "관리"
|
||||||
move = "이동"
|
move = "이동"
|
||||||
move_org = "타 조직으로 이동"
|
move_org = "타 조직으로 이동"
|
||||||
na = "N/A"
|
na = "N/A"
|
||||||
|
name = "이름"
|
||||||
never = "Never"
|
never = "Never"
|
||||||
next = "다음"
|
next = "다음"
|
||||||
|
no_results = "표시할 결과가 없습니다."
|
||||||
none = "없음"
|
none = "없음"
|
||||||
page_of = "Page {{page}} of {{total}}"
|
page_of = "Page {{page}} of {{total}}"
|
||||||
prev = "이전"
|
prev = "이전"
|
||||||
previous = "이전"
|
previous = "이전"
|
||||||
qr = "QR"
|
qr = "QR"
|
||||||
reject = "반려"
|
read = "조회 가능 (Read)"
|
||||||
rejected = "반려됨"
|
|
||||||
reset = "초기화"
|
|
||||||
read_only = "읽기 전용"
|
read_only = "읽기 전용"
|
||||||
refresh = "새로고침"
|
refresh = "새로고침"
|
||||||
|
reject = "반려"
|
||||||
|
rejected = "반려됨"
|
||||||
remove = "제외"
|
remove = "제외"
|
||||||
remove_org = "조직에서 제외"
|
remove_org = "조직에서 제외"
|
||||||
resend = "재발송"
|
resend = "재발송"
|
||||||
|
reset = "초기화"
|
||||||
retry = "다시 시도"
|
retry = "다시 시도"
|
||||||
row = "행"
|
row = "행"
|
||||||
save = "저장"
|
save = "저장"
|
||||||
search = "검색"
|
search = "검색"
|
||||||
search_group = "그룹 검색..."
|
search_group = "그룹 검색..."
|
||||||
|
searching = "검색 중..."
|
||||||
select = "선택"
|
select = "선택"
|
||||||
select_file = "파일 선택"
|
select_file = "파일 선택"
|
||||||
select_placeholder = "선택하세요"
|
select_placeholder = "선택하세요"
|
||||||
load_more = "더 보기"
|
|
||||||
show_more = "+ 더보기"
|
show_more = "+ 더보기"
|
||||||
language = "언어"
|
|
||||||
language_ko = "한국어"
|
|
||||||
language_en = "English"
|
|
||||||
submit = "신청하기"
|
submit = "신청하기"
|
||||||
submitting = "제출 중..."
|
submitting = "제출 중..."
|
||||||
success = "성공"
|
success = "성공"
|
||||||
@@ -105,6 +134,8 @@ theme_light = "Light"
|
|||||||
theme_toggle = "테마 전환"
|
theme_toggle = "테마 전환"
|
||||||
unassigned = "미배정"
|
unassigned = "미배정"
|
||||||
unknown = "Unknown"
|
unknown = "Unknown"
|
||||||
|
view = "보기"
|
||||||
|
write = "수정 가능 (Write)"
|
||||||
|
|
||||||
[ui.common.audit]
|
[ui.common.audit]
|
||||||
load_more = "더 보기"
|
load_more = "더 보기"
|
||||||
@@ -114,12 +145,6 @@ title = "감사 로그"
|
|||||||
actor_id = "사용자 ID 복사"
|
actor_id = "사용자 ID 복사"
|
||||||
target = "클라이언트 ID 복사"
|
target = "클라이언트 ID 복사"
|
||||||
|
|
||||||
[ui.common.audit.filters]
|
|
||||||
user_id = "사용자 ID로 검색"
|
|
||||||
client_id = "클라이언트 ID로 검색"
|
|
||||||
action = "액션으로 검색 (예: ROTATE_SECRET)"
|
|
||||||
status_all = "전체 상태"
|
|
||||||
|
|
||||||
[ui.common.audit.details]
|
[ui.common.audit.details]
|
||||||
actor = "사용자 ID"
|
actor = "사용자 ID"
|
||||||
actor_id = "사용자 ID · {{value}}"
|
actor_id = "사용자 ID · {{value}}"
|
||||||
@@ -135,24 +160,38 @@ path = "Path · {{value}}"
|
|||||||
request = "Request"
|
request = "Request"
|
||||||
request_id = "Request ID · {{value}}"
|
request_id = "Request ID · {{value}}"
|
||||||
result = "Result"
|
result = "Result"
|
||||||
tenant = "Tenant · {{value}}"
|
|
||||||
target = "클라이언트 ID · {{value}}"
|
target = "클라이언트 ID · {{value}}"
|
||||||
|
tenant = "Tenant · {{value}}"
|
||||||
|
|
||||||
|
[ui.common.audit.filters]
|
||||||
|
action = "액션으로 검색 (예: ROTATE_SECRET)"
|
||||||
|
client_id = "클라이언트 ID로 검색"
|
||||||
|
status_all = "전체 상태"
|
||||||
|
user_id = "사용자 ID로 검색"
|
||||||
|
|
||||||
[ui.common.audit.registry]
|
[ui.common.audit.registry]
|
||||||
title = "감사 로그 레지스트리"
|
title = "감사 로그 레지스트리"
|
||||||
|
|
||||||
[ui.common.audit.table]
|
[ui.common.audit.table]
|
||||||
no_logs = "표시할 로그가 없습니다."
|
|
||||||
action = "작업"
|
action = "작업"
|
||||||
actor = "사용자 ID"
|
actor = "사용자 ID"
|
||||||
client_id = "클라이언트 ID"
|
client_id = "클라이언트 ID"
|
||||||
user_id = "사용자 ID"
|
no_logs = "표시할 로그가 없습니다."
|
||||||
status = "상태"
|
status = "상태"
|
||||||
target = "클라이언트 ID"
|
target = "클라이언트 ID"
|
||||||
time = "시간"
|
time = "시간"
|
||||||
|
user_id = "사용자 ID"
|
||||||
|
|
||||||
[ui.common.overview]
|
[ui.common.badge]
|
||||||
title = "운영 현황"
|
admin_only = "Admin only"
|
||||||
|
command_only = "Command only"
|
||||||
|
system = "System"
|
||||||
|
|
||||||
|
[ui.common.chart]
|
||||||
|
|
||||||
|
[ui.common.chart.axis]
|
||||||
|
x = "X축: 기간"
|
||||||
|
y = "Y축: 로그인 요청 수"
|
||||||
|
|
||||||
[ui.common.chart.period]
|
[ui.common.chart.period]
|
||||||
day = "일"
|
day = "일"
|
||||||
@@ -162,29 +201,12 @@ week = "주"
|
|||||||
[ui.common.chart.series_summary]
|
[ui.common.chart.series_summary]
|
||||||
login_users = "로그인 {{login}} / 사용자 {{subjects}}"
|
login_users = "로그인 {{login}} / 사용자 {{subjects}}"
|
||||||
|
|
||||||
[ui.common.chart.axis]
|
[ui.common.custom_claim_permission]
|
||||||
x = "X축: 기간"
|
admin_only = "관리자만 가능"
|
||||||
y = "Y축: 로그인 요청 수"
|
user_and_admin = "사용자와 관리자"
|
||||||
|
|
||||||
[ui.admin.integrity]
|
[ui.common.overview]
|
||||||
fetch_error = "정합성 최종 검증 결과를 불러오지 못했습니다."
|
title = "운영 현황"
|
||||||
|
|
||||||
[ui.admin.integrity.summary]
|
|
||||||
failures_text = "실패 {{count}}건"
|
|
||||||
title = "정합성 최종 검증"
|
|
||||||
|
|
||||||
[ui.admin.integrity.section]
|
|
||||||
tenant_integrity = "테넌트 정합성"
|
|
||||||
user_integrity = "사용자 정합성"
|
|
||||||
|
|
||||||
[ui.admin.overview.chart]
|
|
||||||
description = "전체 또는 선택한 조직 기준으로 그래프를 확인합니다."
|
|
||||||
title = "회사별 앱별 로그인 요청 현황"
|
|
||||||
|
|
||||||
[ui.common.badge]
|
|
||||||
admin_only = "Admin only"
|
|
||||||
command_only = "Command only"
|
|
||||||
system = "System"
|
|
||||||
|
|
||||||
[ui.common.status]
|
[ui.common.status]
|
||||||
active = "활성"
|
active = "활성"
|
||||||
@@ -197,10 +219,3 @@ pending = "준비 중"
|
|||||||
success = "성공"
|
success = "성공"
|
||||||
unchanged = "동일"
|
unchanged = "동일"
|
||||||
updated = "수정"
|
updated = "수정"
|
||||||
|
|
||||||
[ui.common]
|
|
||||||
searching = "검색 중..."
|
|
||||||
|
|
||||||
[ui.common.custom_claim_permission]
|
|
||||||
admin_only = "관리자만 가능"
|
|
||||||
user_and_admin = "사용자와 관리자"
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
|
||||||
|
[msg]
|
||||||
|
|
||||||
|
[msg.admin]
|
||||||
|
|
||||||
|
[msg.admin.audit]
|
||||||
|
subtitle = ""
|
||||||
|
|
||||||
[msg.common]
|
[msg.common]
|
||||||
loading_more = ""
|
|
||||||
copied = ""
|
copied = ""
|
||||||
error = ""
|
error = ""
|
||||||
forbidden = ""
|
forbidden = ""
|
||||||
loading = ""
|
loading = ""
|
||||||
no_results = ""
|
loading_more = ""
|
||||||
no_description = ""
|
no_description = ""
|
||||||
|
no_results = ""
|
||||||
parsing = ""
|
parsing = ""
|
||||||
requesting = ""
|
requesting = ""
|
||||||
saving = ""
|
saving = ""
|
||||||
@@ -20,20 +28,38 @@ loading = ""
|
|||||||
[msg.common.audit.registry]
|
[msg.common.audit.registry]
|
||||||
count = ""
|
count = ""
|
||||||
|
|
||||||
[msg.admin.audit]
|
[msg.dev]
|
||||||
subtitle = ""
|
|
||||||
|
|
||||||
[msg.dev.audit]
|
[msg.dev.audit]
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
|
||||||
|
[ui.admin]
|
||||||
|
|
||||||
|
[ui.admin.integrity]
|
||||||
|
fetch_error = ""
|
||||||
|
|
||||||
|
[ui.admin.integrity.section]
|
||||||
|
tenant_integrity = ""
|
||||||
|
user_integrity = ""
|
||||||
|
|
||||||
|
[ui.admin.integrity.summary]
|
||||||
|
failures_text = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
[ui.admin.overview]
|
||||||
|
|
||||||
|
[ui.admin.overview.chart]
|
||||||
|
description = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
[ui.common]
|
[ui.common]
|
||||||
no_results = ""
|
action = ""
|
||||||
apply = "Apply"
|
|
||||||
actions = ""
|
actions = ""
|
||||||
add = ""
|
add = ""
|
||||||
all = ""
|
|
||||||
apply = ""
|
|
||||||
admin_only = ""
|
admin_only = ""
|
||||||
|
all = ""
|
||||||
apply = ""
|
apply = ""
|
||||||
approve = ""
|
approve = ""
|
||||||
assign = ""
|
assign = ""
|
||||||
@@ -61,42 +87,45 @@ export_with_ids = ""
|
|||||||
export_without_ids = ""
|
export_without_ids = ""
|
||||||
fail = ""
|
fail = ""
|
||||||
go_home = ""
|
go_home = ""
|
||||||
info = ""
|
|
||||||
view = ""
|
|
||||||
hyphen = ""
|
hyphen = ""
|
||||||
|
info = ""
|
||||||
|
language = ""
|
||||||
|
language_en = ""
|
||||||
|
language_ko = ""
|
||||||
|
load_more = ""
|
||||||
loading = ""
|
loading = ""
|
||||||
manage = ""
|
manage = ""
|
||||||
move = ""
|
move = ""
|
||||||
move_org = ""
|
move_org = ""
|
||||||
na = ""
|
na = ""
|
||||||
|
name = ""
|
||||||
never = ""
|
never = ""
|
||||||
next = ""
|
next = ""
|
||||||
|
no_results = ""
|
||||||
none = ""
|
none = ""
|
||||||
page_of = ""
|
page_of = ""
|
||||||
prev = ""
|
prev = ""
|
||||||
previous = ""
|
previous = ""
|
||||||
qr = ""
|
qr = ""
|
||||||
reject = ""
|
read = ""
|
||||||
rejected = ""
|
|
||||||
reset = ""
|
|
||||||
read_only = ""
|
read_only = ""
|
||||||
refresh = ""
|
refresh = ""
|
||||||
|
reject = ""
|
||||||
|
rejected = ""
|
||||||
remove = ""
|
remove = ""
|
||||||
remove_org = ""
|
remove_org = ""
|
||||||
resend = ""
|
resend = ""
|
||||||
|
reset = ""
|
||||||
retry = ""
|
retry = ""
|
||||||
row = ""
|
row = ""
|
||||||
save = ""
|
save = ""
|
||||||
search = ""
|
search = ""
|
||||||
search_group = ""
|
search_group = ""
|
||||||
|
searching = ""
|
||||||
select = ""
|
select = ""
|
||||||
select_file = ""
|
select_file = ""
|
||||||
select_placeholder = ""
|
select_placeholder = ""
|
||||||
load_more = ""
|
|
||||||
show_more = ""
|
show_more = ""
|
||||||
language = ""
|
|
||||||
language_ko = ""
|
|
||||||
language_en = ""
|
|
||||||
submit = ""
|
submit = ""
|
||||||
submitting = ""
|
submitting = ""
|
||||||
success = ""
|
success = ""
|
||||||
@@ -105,6 +134,8 @@ theme_light = ""
|
|||||||
theme_toggle = ""
|
theme_toggle = ""
|
||||||
unassigned = ""
|
unassigned = ""
|
||||||
unknown = ""
|
unknown = ""
|
||||||
|
view = ""
|
||||||
|
write = ""
|
||||||
|
|
||||||
[ui.common.audit]
|
[ui.common.audit]
|
||||||
load_more = ""
|
load_more = ""
|
||||||
@@ -114,12 +145,6 @@ title = ""
|
|||||||
actor_id = ""
|
actor_id = ""
|
||||||
target = ""
|
target = ""
|
||||||
|
|
||||||
[ui.common.audit.filters]
|
|
||||||
user_id = ""
|
|
||||||
client_id = ""
|
|
||||||
action = ""
|
|
||||||
status_all = ""
|
|
||||||
|
|
||||||
[ui.common.audit.details]
|
[ui.common.audit.details]
|
||||||
actor = ""
|
actor = ""
|
||||||
actor_id = ""
|
actor_id = ""
|
||||||
@@ -135,24 +160,38 @@ path = ""
|
|||||||
request = ""
|
request = ""
|
||||||
request_id = ""
|
request_id = ""
|
||||||
result = ""
|
result = ""
|
||||||
tenant = ""
|
|
||||||
target = ""
|
target = ""
|
||||||
|
tenant = ""
|
||||||
|
|
||||||
|
[ui.common.audit.filters]
|
||||||
|
action = ""
|
||||||
|
client_id = ""
|
||||||
|
status_all = ""
|
||||||
|
user_id = ""
|
||||||
|
|
||||||
[ui.common.audit.registry]
|
[ui.common.audit.registry]
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.common.audit.table]
|
[ui.common.audit.table]
|
||||||
no_logs = ""
|
|
||||||
action = ""
|
action = ""
|
||||||
actor = ""
|
actor = ""
|
||||||
client_id = ""
|
client_id = ""
|
||||||
user_id = ""
|
no_logs = ""
|
||||||
status = ""
|
status = ""
|
||||||
target = ""
|
target = ""
|
||||||
time = ""
|
time = ""
|
||||||
|
user_id = ""
|
||||||
|
|
||||||
[ui.common.overview]
|
[ui.common.badge]
|
||||||
title = ""
|
admin_only = ""
|
||||||
|
command_only = ""
|
||||||
|
system = ""
|
||||||
|
|
||||||
|
[ui.common.chart]
|
||||||
|
|
||||||
|
[ui.common.chart.axis]
|
||||||
|
x = ""
|
||||||
|
y = ""
|
||||||
|
|
||||||
[ui.common.chart.period]
|
[ui.common.chart.period]
|
||||||
day = ""
|
day = ""
|
||||||
@@ -162,29 +201,12 @@ week = ""
|
|||||||
[ui.common.chart.series_summary]
|
[ui.common.chart.series_summary]
|
||||||
login_users = ""
|
login_users = ""
|
||||||
|
|
||||||
[ui.common.chart.axis]
|
[ui.common.custom_claim_permission]
|
||||||
x = ""
|
|
||||||
y = ""
|
|
||||||
|
|
||||||
[ui.admin.integrity]
|
|
||||||
fetch_error = ""
|
|
||||||
|
|
||||||
[ui.admin.integrity.summary]
|
|
||||||
failures_text = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.admin.integrity.section]
|
|
||||||
tenant_integrity = ""
|
|
||||||
user_integrity = ""
|
|
||||||
|
|
||||||
[ui.admin.overview.chart]
|
|
||||||
description = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.common.badge]
|
|
||||||
admin_only = ""
|
admin_only = ""
|
||||||
command_only = ""
|
user_and_admin = ""
|
||||||
system = ""
|
|
||||||
|
[ui.common.overview]
|
||||||
|
title = ""
|
||||||
|
|
||||||
[ui.common.status]
|
[ui.common.status]
|
||||||
active = ""
|
active = ""
|
||||||
@@ -197,10 +219,3 @@ pending = ""
|
|||||||
success = ""
|
success = ""
|
||||||
unchanged = ""
|
unchanged = ""
|
||||||
updated = ""
|
updated = ""
|
||||||
|
|
||||||
[ui.common]
|
|
||||||
searching = ""
|
|
||||||
|
|
||||||
[ui.common.custom_claim_permission]
|
|
||||||
admin_only = ""
|
|
||||||
user_and_admin = ""
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 805 KiB After Width: | Height: | Size: 809 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 815 KiB After Width: | Height: | Size: 821 KiB |
@@ -2798,6 +2798,7 @@ function ClientGeneralPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
key={claim.valueType}
|
||||||
type={claimDefaultInputType(claim.valueType)}
|
type={claimDefaultInputType(claim.valueType)}
|
||||||
inputMode={claimDefaultInputMode(
|
inputMode={claimDefaultInputMode(
|
||||||
claim.valueType,
|
claim.valueType,
|
||||||
|
|||||||
@@ -328,7 +328,11 @@ test.describe("DevFront RP claim cache", () => {
|
|||||||
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
|
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
|
||||||
.first()
|
.first()
|
||||||
.fill("3.14");
|
.fill("3.14");
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
"**/api/v1/dev/clients/client-claims",
|
||||||
|
);
|
||||||
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||||
|
await responsePromise;
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
|
|||||||
1381
locales/en.toml
1381
locales/en.toml
File diff suppressed because it is too large
Load Diff
1866
locales/ko.toml
1866
locales/ko.toml
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -74,13 +74,25 @@ function parseTomlKeys(filePath) {
|
|||||||
|
|
||||||
if (line.startsWith('[[') && line.endsWith(']]')) {
|
if (line.startsWith('[[') && line.endsWith(']]')) {
|
||||||
const sectionName = line.slice(2, -2).trim();
|
const sectionName = line.slice(2, -2).trim();
|
||||||
currentSection = sectionName ? sectionName.split('.').map((p) => p.trim()).filter(Boolean) : [];
|
currentSection = sectionName ? sectionName.split('.').map((p) => {
|
||||||
|
p = p.trim();
|
||||||
|
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
||||||
|
p = p.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).filter(Boolean) : [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.startsWith('[') && line.endsWith(']')) {
|
if (line.startsWith('[') && line.endsWith(']')) {
|
||||||
const sectionName = line.slice(1, -1).trim();
|
const sectionName = line.slice(1, -1).trim();
|
||||||
currentSection = sectionName ? sectionName.split('.').map((p) => p.trim()).filter(Boolean) : [];
|
currentSection = sectionName ? sectionName.split('.').map((p) => {
|
||||||
|
p = p.trim();
|
||||||
|
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
||||||
|
p = p.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).filter(Boolean) : [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +106,8 @@ function parseTomlKeys(filePath) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.startsWith('"') && key.endsWith('"')) {
|
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
||||||
key = key.slice(1, -1);
|
key = key.slice(1, -1).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullKey = [...currentSection, key].join('.');
|
const fullKey = [...currentSection, key].join('.');
|
||||||
|
|||||||
@@ -5,10 +5,42 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const ROOT = process.cwd();
|
const ROOT = process.cwd();
|
||||||
const LOCALES_DIR = path.join(ROOT, 'locales');
|
|
||||||
const TEMPLATE_PATH = path.join(LOCALES_DIR, 'template.toml');
|
const LOCALE_SPECS = [
|
||||||
const KO_PATH = path.join(LOCALES_DIR, 'ko.toml');
|
{
|
||||||
const EN_PATH = path.join(LOCALES_DIR, 'en.toml');
|
name: 'root',
|
||||||
|
label: 'root locales',
|
||||||
|
dir: path.join(ROOT, 'locales'),
|
||||||
|
template: 'template.toml',
|
||||||
|
langs: ['ko.toml', 'en.toml'],
|
||||||
|
ownsKey: (key) => !key.startsWith('ui.common.') && !key.startsWith('msg.common.'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'common',
|
||||||
|
label: 'common locales',
|
||||||
|
dir: path.join(ROOT, 'common', 'locales'),
|
||||||
|
template: 'template.toml',
|
||||||
|
langs: ['ko.toml', 'en.toml'],
|
||||||
|
ownsKey: (key) => key.startsWith('ui.common.') || key.startsWith('msg.common.'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldIgnoreCodeKey(key) {
|
||||||
|
return (
|
||||||
|
key.includes('.msg.') ||
|
||||||
|
key.includes('.ui.') ||
|
||||||
|
key.includes('.err.') ||
|
||||||
|
key.includes('.test.') ||
|
||||||
|
key.includes('.non.') ||
|
||||||
|
key.startsWith('ui.admin.users.list.table.') ||
|
||||||
|
key.startsWith('msg.admin.users.detail.') ||
|
||||||
|
key.startsWith('msg.dev.clients.') ||
|
||||||
|
key.startsWith('ui.admin.users.create.') ||
|
||||||
|
key.startsWith('ui.admin.users.detail.') ||
|
||||||
|
key.startsWith('ui.dev.clients.') ||
|
||||||
|
key.startsWith('ui.dev.session.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SKIP_DIRS = new Set([
|
const SKIP_DIRS = new Set([
|
||||||
'.git',
|
'.git',
|
||||||
@@ -53,18 +85,33 @@ function parseToml(filePath) {
|
|||||||
if (!line || line.startsWith('#')) continue;
|
if (!line || line.startsWith('#')) continue;
|
||||||
if (line.startsWith('[[') && line.endsWith(']]')) {
|
if (line.startsWith('[[') && line.endsWith(']]')) {
|
||||||
const name = line.slice(2, -2).trim();
|
const name = line.slice(2, -2).trim();
|
||||||
section = name ? name.split('.').map((p) => p.trim()).filter(Boolean) : [];
|
section = name ? name.split('.').map((p) => {
|
||||||
|
p = p.trim();
|
||||||
|
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
||||||
|
p = p.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).filter(Boolean) : [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.startsWith('[') && line.endsWith(']')) {
|
if (line.startsWith('[') && line.endsWith(']')) {
|
||||||
const name = line.slice(1, -1).trim();
|
const name = line.slice(1, -1).trim();
|
||||||
section = name ? name.split('.').map((p) => p.trim()).filter(Boolean) : [];
|
section = name ? name.split('.').map((p) => {
|
||||||
|
p = p.trim();
|
||||||
|
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
||||||
|
p = p.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).filter(Boolean) : [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const eqIndex = line.indexOf('=');
|
const eqIndex = line.indexOf('=');
|
||||||
if (eqIndex === -1) continue;
|
if (eqIndex === -1) continue;
|
||||||
const key = line.slice(0, eqIndex).trim();
|
let key = line.slice(0, eqIndex).trim();
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
|
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
||||||
|
key = key.slice(1, -1).trim();
|
||||||
|
}
|
||||||
let valueRaw = line.slice(eqIndex + 1).trim();
|
let valueRaw = line.slice(eqIndex + 1).trim();
|
||||||
let value = '';
|
let value = '';
|
||||||
if (
|
if (
|
||||||
@@ -88,12 +135,20 @@ function buildTree(keys, valuesMap) {
|
|||||||
let node = root;
|
let node = root;
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
if (!node[part]) node[part] = {};
|
if (node[part] === undefined) {
|
||||||
|
node[part] = {};
|
||||||
|
} else if (typeof node[part] === 'string') {
|
||||||
|
node[part] = { "": node[part] };
|
||||||
|
}
|
||||||
node = node[part];
|
node = node[part];
|
||||||
}
|
}
|
||||||
const leaf = parts[parts.length - 1];
|
const leaf = parts[parts.length - 1];
|
||||||
const value = valuesMap ? (valuesMap.get(key) ?? '') : '';
|
const value = valuesMap ? (valuesMap.get(key) ?? '') : '';
|
||||||
node[leaf] = value;
|
if (node[leaf] !== undefined && typeof node[leaf] === 'object') {
|
||||||
|
node[leaf][""] = value;
|
||||||
|
} else {
|
||||||
|
node[leaf] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
@@ -105,12 +160,34 @@ function renderToml(tree) {
|
|||||||
lines.push(`[${path.join('.')}]`);
|
lines.push(`[${path.join('.')}]`);
|
||||||
}
|
}
|
||||||
const keys = Object.keys(node).sort();
|
const keys = Object.keys(node).sort();
|
||||||
const leafKeys = keys.filter((k) => typeof node[k] === 'string');
|
const leafKeys = [];
|
||||||
const childKeys = keys.filter((k) => typeof node[k] === 'object');
|
const childKeys = [];
|
||||||
for (const key of leafKeys) {
|
|
||||||
const value = node[key];
|
for (const key of keys) {
|
||||||
lines.push(`${key} = ${JSON.stringify(value)}`);
|
if (typeof node[key] === 'string') {
|
||||||
|
leafKeys.push(key);
|
||||||
|
} else if (typeof node[key] === 'object') {
|
||||||
|
if (node[key][""] !== undefined) {
|
||||||
|
leafKeys.push(key);
|
||||||
|
} else {
|
||||||
|
childKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of leafKeys) {
|
||||||
|
const val = node[key];
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
lines.push(`${key} = ${JSON.stringify(val)}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`${key} = ${JSON.stringify(val[""])}`);
|
||||||
|
const subKeys = Object.keys(val).filter((k) => k !== "").sort();
|
||||||
|
for (const subKey of subKeys) {
|
||||||
|
lines.push(`${key}.${subKey} = ${JSON.stringify(val[subKey])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of childKeys) {
|
for (const key of childKeys) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
walk(node[key], [...path, key]);
|
walk(node[key], [...path, key]);
|
||||||
@@ -389,56 +466,68 @@ function keyToEnglish(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const templateMap = parseToml(TEMPLATE_PATH);
|
|
||||||
const koMap = parseToml(KO_PATH);
|
|
||||||
const enMap = parseToml(EN_PATH);
|
|
||||||
const fallbacks = extractFallbacks();
|
const fallbacks = extractFallbacks();
|
||||||
|
|
||||||
const allKeys = new Set([
|
for (const spec of LOCALE_SPECS) {
|
||||||
...templateMap.keys(),
|
const templatePath = path.join(spec.dir, spec.template);
|
||||||
...koMap.keys(),
|
const koPath = path.join(spec.dir, 'ko.toml');
|
||||||
...enMap.keys(),
|
const enPath = path.join(spec.dir, 'en.toml');
|
||||||
]);
|
|
||||||
|
|
||||||
for (const key of allKeys) {
|
const templateMap = parseToml(templatePath);
|
||||||
const fallback = fallbacks.get(key);
|
const koMap = parseToml(koPath);
|
||||||
const currentKo = koMap.get(key) ?? '';
|
const enMap = parseToml(enPath);
|
||||||
const currentEn = enMap.get(key) ?? '';
|
|
||||||
|
|
||||||
let nextKo = currentKo;
|
const ownedFallbackKeys = Array.from(fallbacks.keys()).filter(
|
||||||
if (!nextKo && fallback) {
|
(key) => spec.ownsKey(key) && !shouldIgnoreCodeKey(key)
|
||||||
nextKo = fallback;
|
);
|
||||||
}
|
|
||||||
if (!nextKo) {
|
|
||||||
nextKo = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextEn = currentEn;
|
const allKeys = new Set([
|
||||||
if (!nextEn) {
|
...templateMap.keys(),
|
||||||
const source = fallback || nextKo || key;
|
...koMap.keys(),
|
||||||
if (isLongText(source)) {
|
...enMap.keys(),
|
||||||
nextEn = source;
|
...ownedFallbackKeys,
|
||||||
} else if (isMostlyAscii(source)) {
|
]);
|
||||||
nextEn = source;
|
|
||||||
} else {
|
for (const key of allKeys) {
|
||||||
nextEn = translateKorean(source);
|
const fallback = fallbacks.get(key);
|
||||||
|
const currentKo = koMap.get(key) ?? '';
|
||||||
|
const currentEn = enMap.get(key) ?? '';
|
||||||
|
|
||||||
|
let nextKo = currentKo;
|
||||||
|
if (!nextKo && fallback) {
|
||||||
|
nextKo = fallback;
|
||||||
}
|
}
|
||||||
}
|
if (!nextKo) {
|
||||||
if (!nextEn) {
|
nextKo = key;
|
||||||
nextEn = key;
|
}
|
||||||
}
|
|
||||||
if (!isLongText(nextEn) && containsHangul(nextEn)) {
|
let nextEn = currentEn;
|
||||||
nextEn = keyToEnglish(key);
|
if (!nextEn) {
|
||||||
|
const source = fallback || nextKo || key;
|
||||||
|
if (isLongText(source)) {
|
||||||
|
nextEn = source;
|
||||||
|
} else if (isMostlyAscii(source)) {
|
||||||
|
nextEn = source;
|
||||||
|
} else {
|
||||||
|
nextEn = translateKorean(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nextEn) {
|
||||||
|
nextEn = key;
|
||||||
|
}
|
||||||
|
if (!isLongText(nextEn) && containsHangul(nextEn)) {
|
||||||
|
nextEn = keyToEnglish(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
koMap.set(key, nextKo);
|
||||||
|
enMap.set(key, nextEn);
|
||||||
}
|
}
|
||||||
|
|
||||||
koMap.set(key, nextKo);
|
const keys = Array.from(allKeys).sort();
|
||||||
enMap.set(key, nextEn);
|
fs.writeFileSync(koPath, renderToml(buildTree(keys, koMap)));
|
||||||
|
fs.writeFileSync(enPath, renderToml(buildTree(keys, enMap)));
|
||||||
|
fs.writeFileSync(templatePath, renderToml(buildTree(keys, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Array.from(allKeys).sort();
|
|
||||||
fs.writeFileSync(KO_PATH, renderToml(buildTree(keys, koMap)));
|
|
||||||
fs.writeFileSync(EN_PATH, renderToml(buildTree(keys, enMap)));
|
|
||||||
fs.writeFileSync(TEMPLATE_PATH, renderToml(buildTree(keys, null)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -56,11 +56,14 @@ result = "Result: {value}"
|
|||||||
session_id = "Session ID: {value}"
|
session_id = "Session ID: {value}"
|
||||||
status = "Status: pending"
|
status = "Status: pending"
|
||||||
|
|
||||||
|
[msg.userfront.audit.filter]
|
||||||
|
description = "Toggle to view only active sessions."
|
||||||
|
|
||||||
[msg.userfront.consent]
|
[msg.userfront.consent]
|
||||||
accept_error = "Failed to process consent: {error}"
|
accept_error = "Failed to process consent: {error}"
|
||||||
client_id = "Client ID: {id}"
|
client_id = "Client ID: {id}"
|
||||||
client_unknown = "Unknown application"
|
client_unknown = "Unknown application"
|
||||||
description = "The service below is requesting access to your account information.\\\\nPlease choose whether to continue."
|
description = "The service below is requesting access to your account information.\\\\\\\\nPlease choose whether to continue."
|
||||||
load_error = "Failed to load consent information: {error}"
|
load_error = "Failed to load consent information: {error}"
|
||||||
missing_redirect = "Consent was processed, but the redirect URL was missing."
|
missing_redirect = "Consent was processed, but the redirect URL was missing."
|
||||||
redirect_notice = "After consent, you will be redirected automatically."
|
redirect_notice = "After consent, you will be redirected automatically."
|
||||||
@@ -82,15 +85,15 @@ approved_device = "Approved device: {device}"
|
|||||||
approved_ip = "Approved IP: {ip}"
|
approved_ip = "Approved IP: {ip}"
|
||||||
audit_empty = "No recent sign-in activity."
|
audit_empty = "No recent sign-in activity."
|
||||||
audit_load_error = "Could not load sign-in history."
|
audit_load_error = "Could not load sign-in history."
|
||||||
auto_login_supported = "You can sign in without an extra login when opening this linked app."
|
|
||||||
auth_method = "Auth method: {method}"
|
auth_method = "Auth method: {method}"
|
||||||
|
auto_login_supported = "You can sign in without an extra login when opening this linked app."
|
||||||
client_id = "Client ID: {id}"
|
client_id = "Client ID: {id}"
|
||||||
client_id_missing = "No client ID available."
|
client_id_missing = "No client ID available."
|
||||||
current_status = "Current status: {status}"
|
current_status = "Current status: {status}"
|
||||||
last_auth = "Last signed in: {value}"
|
last_auth = "Last signed in: {value}"
|
||||||
link_status = "Link status: {status}"
|
|
||||||
link_missing = "This app does not have a launch URL configured."
|
link_missing = "This app does not have a launch URL configured."
|
||||||
link_open_error = "Could not open the app link."
|
link_open_error = "Could not open the app link."
|
||||||
|
link_status = "Link status: {status}"
|
||||||
render_error = "Dashboard render error: {error}"
|
render_error = "Dashboard render error: {error}"
|
||||||
session_id_copied = "Session ID copied."
|
session_id_copied = "Session ID copied."
|
||||||
|
|
||||||
@@ -99,6 +102,19 @@ empty = "No linked apps yet."
|
|||||||
empty_detail = "Linked apps and their latest activity will appear here."
|
empty_detail = "Linked apps and their latest activity will appear here."
|
||||||
error = "Could not load linked apps."
|
error = "Could not load linked apps."
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.approved_session]
|
||||||
|
copy_click = "{label}: {id}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nClick to copy."
|
||||||
|
copy_tap = "{label}: {id}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nTap to copy."
|
||||||
|
none = "No {label}"
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.revoke]
|
||||||
|
confirm = "Disconnect {app}?\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in."
|
||||||
|
error = "Could not disconnect the app: {error}"
|
||||||
|
success = "{app} has been disconnected."
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.scopes]
|
||||||
|
empty = "No scopes were requested."
|
||||||
|
|
||||||
[msg.userfront.dashboard.sessions]
|
[msg.userfront.dashboard.sessions]
|
||||||
browser = "Browser: {value}"
|
browser = "Browser: {value}"
|
||||||
empty = "No active sessions."
|
empty = "No active sessions."
|
||||||
@@ -109,23 +125,10 @@ recent_app = "Recent app: {app}"
|
|||||||
session_id = "Session ID: {id}"
|
session_id = "Session ID: {id}"
|
||||||
|
|
||||||
[msg.userfront.dashboard.sessions.revoke]
|
[msg.userfront.dashboard.sessions.revoke]
|
||||||
confirm = "End the session for {target}?\nThat device will need to sign in again."
|
confirm = "End the session for {target}?\\nThat device will need to sign in again."
|
||||||
error = "Could not end the session: {error}"
|
error = "Could not end the session: {error}"
|
||||||
success = "The session has been ended."
|
success = "The session has been ended."
|
||||||
|
|
||||||
[msg.userfront.dashboard.approved_session]
|
|
||||||
copy_click = "{label}: {id}\\\\\\\\\\\\\\\\nClick to copy."
|
|
||||||
copy_tap = "{label}: {id}\\\\\\\\\\\\\\\\nTap to copy."
|
|
||||||
none = "No {label}"
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.revoke]
|
|
||||||
confirm = "Disconnect {app}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in."
|
|
||||||
error = "Could not disconnect the app: {error}"
|
|
||||||
success = "{app} has been disconnected."
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.scopes]
|
|
||||||
empty = "No scopes were requested."
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.timeline]
|
[msg.userfront.dashboard.timeline]
|
||||||
load_error = "Could not load sign-in history."
|
load_error = "Could not load sign-in history."
|
||||||
|
|
||||||
@@ -139,6 +142,22 @@ title_generic = "An error occurred."
|
|||||||
title_with_code = "Error: {code}"
|
title_with_code = "Error: {code}"
|
||||||
type = "Error type: {type}"
|
type = "Error type: {type}"
|
||||||
|
|
||||||
|
[msg.userfront.error.ory]
|
||||||
|
$normalizedCode = "{error}"
|
||||||
|
access_denied = "The user denied the consent request."
|
||||||
|
consent_required = "Consent is required to continue."
|
||||||
|
interaction_required = "Additional interaction is required. Please try again."
|
||||||
|
invalid_client = "Client authentication failed."
|
||||||
|
invalid_grant = "The authorization grant is invalid or expired."
|
||||||
|
invalid_request = "The request is invalid."
|
||||||
|
invalid_scope = "The requested scope is invalid."
|
||||||
|
login_required = "Login is required."
|
||||||
|
request_forbidden = "The request was forbidden."
|
||||||
|
server_error = "An authentication server error occurred."
|
||||||
|
temporarily_unavailable = "The authentication server is temporarily unavailable."
|
||||||
|
unauthorized_client = "The client is not authorized for this request."
|
||||||
|
unsupported_response_type = "The response type is not supported."
|
||||||
|
|
||||||
[msg.userfront.error.tenant]
|
[msg.userfront.error.tenant]
|
||||||
account = "Account"
|
account = "Account"
|
||||||
account_unknown = "Unknown"
|
account_unknown = "Unknown"
|
||||||
@@ -155,24 +174,8 @@ tenant = "Tenant"
|
|||||||
tenant_unknown = "Unknown"
|
tenant_unknown = "Unknown"
|
||||||
title = "Access restriction details"
|
title = "Access restriction details"
|
||||||
|
|
||||||
[msg.userfront.error.ory]
|
|
||||||
"$normalizedCode" = "{error}"
|
|
||||||
access_denied = "The user denied the consent request."
|
|
||||||
consent_required = "Consent is required to continue."
|
|
||||||
interaction_required = "Additional interaction is required. Please try again."
|
|
||||||
invalid_client = "Client authentication failed."
|
|
||||||
invalid_grant = "The authorization grant is invalid or expired."
|
|
||||||
invalid_request = "The request is invalid."
|
|
||||||
invalid_scope = "The requested scope is invalid."
|
|
||||||
login_required = "Login is required."
|
|
||||||
request_forbidden = "The request was forbidden."
|
|
||||||
server_error = "An authentication server error occurred."
|
|
||||||
temporarily_unavailable = "The authentication server is temporarily unavailable."
|
|
||||||
unauthorized_client = "The client is not authorized for this request."
|
|
||||||
unsupported_response_type = "The response type is not supported."
|
|
||||||
|
|
||||||
[msg.userfront.error.whitelist]
|
[msg.userfront.error.whitelist]
|
||||||
"$normalizedCode" = "{error}"
|
$normalizedCode = "{error}"
|
||||||
bad_request = "Please check your input."
|
bad_request = "Please check your input."
|
||||||
invalid_session = "Your session has expired. Please sign in again."
|
invalid_session = "Your session has expired. Please sign in again."
|
||||||
not_found = "The requested page could not be found."
|
not_found = "The requested page could not be found."
|
||||||
@@ -226,14 +229,14 @@ scan_hint = "Scan it with the mobile app."
|
|||||||
invalid = "Enter the 2 letters and 6 digits from your code."
|
invalid = "Enter the 2 letters and 6 digits from your code."
|
||||||
|
|
||||||
[msg.userfront.login.unregistered]
|
[msg.userfront.login.unregistered]
|
||||||
body = "We could not find an account for that information.\\\\\\\\\\\\\\\\nPlease sign up before continuing."
|
body = "We could not find an account for that information.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nPlease sign up before continuing."
|
||||||
|
|
||||||
[msg.userfront.login.verification]
|
[msg.userfront.login.verification]
|
||||||
approved = "Approved. Complete sign-in in the original window."
|
approved = "Approved. Complete sign-in in the original window."
|
||||||
approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly."
|
approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly."
|
||||||
approved_remote = "Your requested sign-in is complete."
|
approved_remote = "Your requested sign-in is complete."
|
||||||
pending_remote = "Checking the sign-in approval request. Please wait."
|
|
||||||
close_hint = "You can close this window now."
|
close_hint = "You can close this window now."
|
||||||
|
pending_remote = "Checking the sign-in approval request. Please wait."
|
||||||
success = "Sign-in approval completed."
|
success = "Sign-in approval completed."
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
[msg.userfront.login_success]
|
||||||
@@ -465,6 +468,10 @@ dev_console = "Dev Console"
|
|||||||
|
|
||||||
[ui.userfront.audit]
|
[ui.userfront.audit]
|
||||||
|
|
||||||
|
[ui.userfront.audit.filter]
|
||||||
|
title = "Manage My Activity"
|
||||||
|
toggle_label = "Show active sessions only"
|
||||||
|
|
||||||
[ui.userfront.audit.table]
|
[ui.userfront.audit.table]
|
||||||
action = "Action"
|
action = "Action"
|
||||||
app = "App"
|
app = "App"
|
||||||
@@ -499,17 +506,6 @@ status_history = "Link details"
|
|||||||
[ui.userfront.dashboard.activity]
|
[ui.userfront.dashboard.activity]
|
||||||
linked = "Linked"
|
linked = "Linked"
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions]
|
|
||||||
active_badge = "Active"
|
|
||||||
current_badge = "Current"
|
|
||||||
current_disabled = "Current session"
|
|
||||||
unknown_device = "Unknown device"
|
|
||||||
unknown_session = "Session"
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions.revoke]
|
|
||||||
action = "End session"
|
|
||||||
title = "End session"
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.approved_session]
|
[ui.userfront.dashboard.approved_session]
|
||||||
default = "Default"
|
default = "Default"
|
||||||
userfront = "Approved UserFront session ID"
|
userfront = "Approved UserFront session ID"
|
||||||
@@ -521,6 +517,17 @@ title = "Disconnect app"
|
|||||||
[ui.userfront.dashboard.scopes]
|
[ui.userfront.dashboard.scopes]
|
||||||
title = "Consent scopes"
|
title = "Consent scopes"
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions]
|
||||||
|
active_badge = "Active"
|
||||||
|
current_badge = "Current"
|
||||||
|
current_disabled = "Current session"
|
||||||
|
unknown_device = "Unknown device"
|
||||||
|
unknown_session = "Session"
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions.revoke]
|
||||||
|
action = "End session"
|
||||||
|
title = "End session"
|
||||||
|
|
||||||
[ui.userfront.dashboard.status]
|
[ui.userfront.dashboard.status]
|
||||||
revoked = "Revoked"
|
revoked = "Revoked"
|
||||||
|
|
||||||
@@ -583,8 +590,8 @@ title = "Account not found"
|
|||||||
|
|
||||||
[ui.userfront.login.verification]
|
[ui.userfront.login.verification]
|
||||||
action_label = "Done"
|
action_label = "Done"
|
||||||
action_label_remote = "Go to sign-in window"
|
|
||||||
action_label_close = "Close Window"
|
action_label_close = "Close Window"
|
||||||
|
action_label_remote = "Go to sign-in window"
|
||||||
page_title = "Baron SW Portal"
|
page_title = "Baron SW Portal"
|
||||||
title = "Approval complete"
|
title = "Approval complete"
|
||||||
title_pending = "Checking approval"
|
title_pending = "Checking approval"
|
||||||
@@ -698,12 +705,3 @@ verify = "Verification"
|
|||||||
|
|
||||||
[ui.userfront.signup.success]
|
[ui.userfront.signup.success]
|
||||||
action = "Go to sign-in"
|
action = "Go to sign-in"
|
||||||
|
|
||||||
|
|
||||||
[ui.userfront.audit.filter]
|
|
||||||
title = "Manage My Activity"
|
|
||||||
toggle_label = "Show active sessions only"
|
|
||||||
|
|
||||||
[msg.userfront.audit.filter]
|
|
||||||
description = "Toggle to view only active sessions."
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,231 +41,6 @@ verify_code_failed = "인증 실패: {error}"
|
|||||||
[err.userfront.session]
|
[err.userfront.session]
|
||||||
missing = "활성 세션이 없습니다."
|
missing = "활성 세션이 없습니다."
|
||||||
|
|
||||||
[msg.userfront.audit]
|
|
||||||
browser = "브라우저: {value}"
|
|
||||||
date = "접속일자: {value}"
|
|
||||||
device = "접속환경: {value}"
|
|
||||||
end = "더 이상 항목이 없습니다."
|
|
||||||
filtered_empty = "활성 세션으로 필터링된 접속 이력이 없습니다."
|
|
||||||
ip = "접속 IP: {value}"
|
|
||||||
load_more_error = "더 불러오지 못했습니다."
|
|
||||||
result = "인증결과: {value}"
|
|
||||||
session_id = "Session ID: {value}"
|
|
||||||
status = "현황: (준비중)"
|
|
||||||
|
|
||||||
[msg.userfront.dashboard]
|
|
||||||
approved_device = "승인 기기: {device}"
|
|
||||||
approved_ip = "승인 IP: {ip}"
|
|
||||||
audit_empty = "최근 접속 이력이 없습니다."
|
|
||||||
audit_load_error = "접속이력을 불러오지 못했습니다."
|
|
||||||
auth_method = "인증수단: {method}"
|
|
||||||
client_id = "Client ID: {id}"
|
|
||||||
client_id_missing = "Client ID 없음"
|
|
||||||
current_status = "현재 상태: {status}"
|
|
||||||
last_auth = "최근 인증: {value}"
|
|
||||||
link_status = "연동 상태: {status}"
|
|
||||||
link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
|
|
||||||
link_open_error = "해당 링크를 열 수 없습니다."
|
|
||||||
render_error = "대시보드 렌더링 오류: {error}"
|
|
||||||
session_id_copied = "세션 ID가 복사되었습니다."
|
|
||||||
|
|
||||||
[msg.userfront.error]
|
|
||||||
detail_contact = "관리자에게 문의해 주세요."
|
|
||||||
detail_generic = "오류가 발생했습니다."
|
|
||||||
detail_request = "요청을 처리하는 중 문제가 발생했습니다."
|
|
||||||
id = "오류 ID: {id}"
|
|
||||||
title = "인증 과정에서 오류가 발생했습니다"
|
|
||||||
title_generic = "오류가 발생했습니다"
|
|
||||||
title_with_code = "오류: {code}"
|
|
||||||
type = "오류 종류: {type}"
|
|
||||||
|
|
||||||
[msg.userfront.error.tenant]
|
|
||||||
account = "계정"
|
|
||||||
account_unknown = "알 수 없음"
|
|
||||||
affiliated_tenants = "전체 소속 테넌트"
|
|
||||||
allowed_box_title = "접속 가능 테넌트"
|
|
||||||
allowed_tenants = "접속 가능 테넌트"
|
|
||||||
detail = "현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다."
|
|
||||||
load_failed = "계정 정보를 확인하지 못했습니다. 다시 시도해 주세요."
|
|
||||||
loading = "현재 계정 정보를 불러오는 중입니다."
|
|
||||||
lookup_fallback = "표시 정보가 충분하지 않아 일부 항목은 확인되지 않을 수 있습니다."
|
|
||||||
page_title = "애플리케이션 접근이 제한되었습니다"
|
|
||||||
primary_tenant = "대표 소속 테넌트"
|
|
||||||
tenant = "소속 테넌트"
|
|
||||||
tenant_unknown = "알 수 없음"
|
|
||||||
title = "접근 제한 정보"
|
|
||||||
|
|
||||||
[msg.userfront.forgot]
|
|
||||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
|
||||||
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
|
|
||||||
error = "전송에 실패했습니다: {error}"
|
|
||||||
input_required = "이메일 또는 휴대폰 번호를 입력해주세요."
|
|
||||||
sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."
|
|
||||||
|
|
||||||
[msg.userfront.login]
|
|
||||||
cookie_check_failed = "로그인 확인 실패: {error}"
|
|
||||||
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
|
|
||||||
link_failed = "오류: {error}"
|
|
||||||
link_send_failed = "전송 실패: {error}"
|
|
||||||
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
|
|
||||||
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
|
|
||||||
link_timeout = "시간이 경과되었습니다."
|
|
||||||
no_account = "계정이 없으신가요?"
|
|
||||||
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
|
|
||||||
qr_expired = "시간이 경과되었습니다."
|
|
||||||
qr_init_failed = "QR 초기화에 실패했습니다: {error}"
|
|
||||||
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
|
|
||||||
token_missing = "로그인 토큰을 확인할 수 없습니다."
|
|
||||||
verification_failed = "승인 처리에 실패했습니다: {error}"
|
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
|
||||||
subtitle = "성공적으로 로그인되었습니다."
|
|
||||||
|
|
||||||
[msg.userfront.consent]
|
|
||||||
accept_error = "동의 처리에 실패했습니다: {error}"
|
|
||||||
client_id = "클라이언트 ID: {id}"
|
|
||||||
client_unknown = "알 수 없는 앱"
|
|
||||||
description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\n계속 진행하려면 동의 여부를 선택해 주세요."
|
|
||||||
load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
|
|
||||||
missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
|
|
||||||
redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
|
|
||||||
scope_count = "총 {count}개"
|
|
||||||
|
|
||||||
[msg.userfront.profile]
|
|
||||||
department_missing = "소속 정보 없음"
|
|
||||||
department_required = "소속을 입력해주세요."
|
|
||||||
email_missing = "이메일 없음"
|
|
||||||
greeting = "안녕하세요, {name}님"
|
|
||||||
load_failed = "정보를 불러올 수 없습니다."
|
|
||||||
name_missing = "이름 없음"
|
|
||||||
name_required = "이름을 입력해주세요."
|
|
||||||
phone_required = "휴대폰 번호를 입력해주세요."
|
|
||||||
phone_verify_required = "휴대폰 번호 인증이 필요합니다."
|
|
||||||
update_failed = "수정 실패: {error}"
|
|
||||||
update_success = "정보가 수정되었습니다."
|
|
||||||
|
|
||||||
[msg.userfront.qr]
|
|
||||||
camera_error = "카메라 오류: {error}"
|
|
||||||
permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요."
|
|
||||||
permission_required = "카메라 권한이 필요합니다."
|
|
||||||
|
|
||||||
[msg.userfront.reset]
|
|
||||||
invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요."
|
|
||||||
invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)"
|
|
||||||
invalid_title = "유효하지 않은 링크입니다."
|
|
||||||
policy_loading = "비밀번호 정책을 불러오는 중입니다..."
|
|
||||||
success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요."
|
|
||||||
|
|
||||||
[msg.userfront.sections]
|
|
||||||
apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
|
|
||||||
audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
|
|
||||||
sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다."
|
|
||||||
|
|
||||||
[msg.userfront.settings]
|
|
||||||
disabled = "현재 계정 설정 화면은 준비 중입니다."
|
|
||||||
|
|
||||||
[msg.userfront.signup]
|
|
||||||
failed = "가입 실패: {error}"
|
|
||||||
privacy_full = "개인정보 수집 및 이용 동의 전문..."
|
|
||||||
tos_full = "서비스 이용약관 전문..."
|
|
||||||
|
|
||||||
[ui.common.badge]
|
|
||||||
admin_only = "Admin only"
|
|
||||||
command_only = "Command only"
|
|
||||||
system = "System"
|
|
||||||
|
|
||||||
[ui.common.status]
|
|
||||||
active = "활성"
|
|
||||||
blocked = "차단됨"
|
|
||||||
failure = "실패"
|
|
||||||
inactive = "비활성"
|
|
||||||
ok = "정상"
|
|
||||||
pending = "준비 중"
|
|
||||||
success = "성공"
|
|
||||||
|
|
||||||
[ui.userfront.app_label]
|
|
||||||
admin_console = "Admin Console"
|
|
||||||
baron = "Baron 로그인"
|
|
||||||
dev_console = "Dev Console"
|
|
||||||
|
|
||||||
[ui.userfront.auth_method]
|
|
||||||
ory = "Ory 세션"
|
|
||||||
session = "세션"
|
|
||||||
|
|
||||||
[ui.userfront.dashboard]
|
|
||||||
last_auth_label = "최근 인증"
|
|
||||||
link_status_label = "연동 상태"
|
|
||||||
status_history = "연동 정보"
|
|
||||||
|
|
||||||
[ui.userfront.device]
|
|
||||||
android = "Mobile(Android)"
|
|
||||||
ios = "Mobile(iOS)"
|
|
||||||
linux = "Desktop(Linux)"
|
|
||||||
macos = "Desktop(macOS)"
|
|
||||||
windows = "Desktop(Windows)"
|
|
||||||
|
|
||||||
[ui.userfront.error]
|
|
||||||
go_home = "홈으로 이동"
|
|
||||||
go_login = "로그인으로 이동"
|
|
||||||
switch_account = "다른 계정으로 로그인"
|
|
||||||
|
|
||||||
[ui.userfront.forgot]
|
|
||||||
heading = "비밀번호를 잊으셨나요?"
|
|
||||||
input_label = "이메일 또는 휴대폰 번호"
|
|
||||||
submit = "재설정 링크 전송"
|
|
||||||
title = "비밀번호 재설정"
|
|
||||||
|
|
||||||
[ui.userfront.login]
|
|
||||||
forgot_password = "비밀번호를 잊으셨나요?"
|
|
||||||
signup = "회원가입"
|
|
||||||
|
|
||||||
[ui.userfront.login_success]
|
|
||||||
later = "나중에 하기 (대시보드로 이동)"
|
|
||||||
qr = "QR 인증 (카메라 켜기)"
|
|
||||||
title = "로그인 완료"
|
|
||||||
|
|
||||||
[ui.userfront.consent]
|
|
||||||
accept = "동의하고 계속하기"
|
|
||||||
requested_scopes = "요청된 권한"
|
|
||||||
title = "접근 권한 요청"
|
|
||||||
|
|
||||||
[ui.userfront.nav]
|
|
||||||
dashboard = "대시보드"
|
|
||||||
logout = "로그아웃"
|
|
||||||
profile = "내 정보"
|
|
||||||
qr_scan = "QR 스캔"
|
|
||||||
|
|
||||||
[ui.userfront.profile]
|
|
||||||
department_empty = "소속 정보 없음"
|
|
||||||
manage = "프로필 관리"
|
|
||||||
user_fallback = "사용자"
|
|
||||||
|
|
||||||
[ui.userfront.qr]
|
|
||||||
rescan = "다시 스캔"
|
|
||||||
result_success = "승인 완료"
|
|
||||||
title = "Scan QR Code"
|
|
||||||
|
|
||||||
[ui.userfront.reset]
|
|
||||||
confirm_password = "새 비밀번호 확인"
|
|
||||||
new_password = "새 비밀번호"
|
|
||||||
submit = "비밀번호 변경"
|
|
||||||
subtitle = "새로운 비밀번호 설정"
|
|
||||||
title = "새 비밀번호 설정"
|
|
||||||
|
|
||||||
[ui.userfront.sections]
|
|
||||||
apps = "나의 App 현황"
|
|
||||||
audit = "접속이력"
|
|
||||||
sessions = "활성 세션"
|
|
||||||
|
|
||||||
[ui.userfront.session]
|
|
||||||
active = "세션 활성"
|
|
||||||
unknown = "알 수 없음"
|
|
||||||
|
|
||||||
[ui.userfront.signup]
|
|
||||||
complete = "가입 완료"
|
|
||||||
next_step = "다음 단계"
|
|
||||||
title = "회원가입"
|
|
||||||
|
|
||||||
[msg.userfront]
|
[msg.userfront]
|
||||||
greeting = "안녕하세요, {name}님"
|
greeting = "안녕하세요, {name}님"
|
||||||
|
|
||||||
@@ -281,11 +56,14 @@ result = "인증결과: {value}"
|
|||||||
session_id = "Session ID: {value}"
|
session_id = "Session ID: {value}"
|
||||||
status = "현황: (준비중)"
|
status = "현황: (준비중)"
|
||||||
|
|
||||||
|
[msg.userfront.audit.filter]
|
||||||
|
description = "활성화된 세션만 보려면 토글을 켜주세요."
|
||||||
|
|
||||||
[msg.userfront.consent]
|
[msg.userfront.consent]
|
||||||
accept_error = "동의 처리에 실패했습니다: {error}"
|
accept_error = "동의 처리에 실패했습니다: {error}"
|
||||||
client_id = "클라이언트 ID: {id}"
|
client_id = "클라이언트 ID: {id}"
|
||||||
client_unknown = "알 수 없는 앱"
|
client_unknown = "알 수 없는 앱"
|
||||||
description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요."
|
description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\\\\\n계속 진행하려면 동의 여부를 선택해 주세요."
|
||||||
load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
|
load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
|
||||||
missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
|
missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
|
||||||
redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
|
redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
|
||||||
@@ -307,14 +85,15 @@ approved_device = "승인 기기: {device}"
|
|||||||
approved_ip = "승인 IP: {ip}"
|
approved_ip = "승인 IP: {ip}"
|
||||||
audit_empty = "최근 접속 이력이 없습니다."
|
audit_empty = "최근 접속 이력이 없습니다."
|
||||||
audit_load_error = "접속이력을 불러오지 못했습니다."
|
audit_load_error = "접속이력을 불러오지 못했습니다."
|
||||||
auto_login_supported = "연동앱 클릭 시 별도 로그인 없이 로그인할 수 있습니다."
|
|
||||||
auth_method = "인증수단: {method}"
|
auth_method = "인증수단: {method}"
|
||||||
|
auto_login_supported = "연동앱 클릭 시 별도 로그인 없이 로그인할 수 있습니다."
|
||||||
client_id = "Client ID: {id}"
|
client_id = "Client ID: {id}"
|
||||||
client_id_missing = "Client ID 없음"
|
client_id_missing = "Client ID 없음"
|
||||||
current_status = "현재 상태: {status}"
|
current_status = "현재 상태: {status}"
|
||||||
last_auth = "최근 인증: {value}"
|
last_auth = "최근 인증: {value}"
|
||||||
link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
|
link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
|
||||||
link_open_error = "해당 링크를 열 수 없습니다."
|
link_open_error = "해당 링크를 열 수 없습니다."
|
||||||
|
link_status = "연동 상태: {status}"
|
||||||
render_error = "대시보드 렌더링 오류: {error}"
|
render_error = "대시보드 렌더링 오류: {error}"
|
||||||
session_id_copied = "세션 ID가 복사되었습니다."
|
session_id_copied = "세션 ID가 복사되었습니다."
|
||||||
|
|
||||||
@@ -323,6 +102,19 @@ empty = "연동된 앱이 없습니다."
|
|||||||
empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
|
empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
|
||||||
error = "연동 정보를 불러오지 못했습니다."
|
error = "연동 정보를 불러오지 못했습니다."
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.approved_session]
|
||||||
|
copy_click = "{label}: {id}\\\\\\\\n클릭하면 복사됩니다."
|
||||||
|
copy_tap = "{label}: {id}\\\\\\\\n탭하면 복사됩니다."
|
||||||
|
none = "{label} 없음"
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.revoke]
|
||||||
|
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
|
||||||
|
error = "해지 실패: {error}"
|
||||||
|
success = "{app} 연동이 해지되었습니다."
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.scopes]
|
||||||
|
empty = "요청된 권한이 없습니다."
|
||||||
|
|
||||||
[msg.userfront.dashboard.sessions]
|
[msg.userfront.dashboard.sessions]
|
||||||
browser = "브라우저: {value}"
|
browser = "브라우저: {value}"
|
||||||
empty = "활성 세션이 없습니다."
|
empty = "활성 세션이 없습니다."
|
||||||
@@ -333,23 +125,10 @@ recent_app = "최근 접속 앱: {app}"
|
|||||||
session_id = "세션 ID: {id}"
|
session_id = "세션 ID: {id}"
|
||||||
|
|
||||||
[msg.userfront.dashboard.sessions.revoke]
|
[msg.userfront.dashboard.sessions.revoke]
|
||||||
confirm = "{target} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다."
|
confirm = "{target} 세션을 종료하시겠습니까?\\n대상 기기에서는 다시 로그인이 필요합니다."
|
||||||
error = "세션 종료 실패: {error}"
|
error = "세션 종료 실패: {error}"
|
||||||
success = "세션이 종료되었습니다."
|
success = "세션이 종료되었습니다."
|
||||||
|
|
||||||
[msg.userfront.dashboard.approved_session]
|
|
||||||
copy_click = "{label}: {id}\\\\n클릭하면 복사됩니다."
|
|
||||||
copy_tap = "{label}: {id}\\\\n탭하면 복사됩니다."
|
|
||||||
none = "{label} 없음"
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.revoke]
|
|
||||||
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
|
|
||||||
error = "해지 실패: {error}"
|
|
||||||
success = "{app} 연동이 해지되었습니다."
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.scopes]
|
|
||||||
empty = "요청된 권한이 없습니다."
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.timeline]
|
[msg.userfront.dashboard.timeline]
|
||||||
load_error = "접속이력을 불러오지 못했습니다."
|
load_error = "접속이력을 불러오지 못했습니다."
|
||||||
|
|
||||||
@@ -363,6 +142,22 @@ title_generic = "오류가 발생했습니다"
|
|||||||
title_with_code = "오류: {code}"
|
title_with_code = "오류: {code}"
|
||||||
type = "오류 종류: {type}"
|
type = "오류 종류: {type}"
|
||||||
|
|
||||||
|
[msg.userfront.error.ory]
|
||||||
|
$normalizedCode = "{error}"
|
||||||
|
access_denied = "사용자가 동의를 거부했습니다."
|
||||||
|
consent_required = "앱 접근 동의가 필요합니다."
|
||||||
|
interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
|
||||||
|
invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
|
||||||
|
invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
|
||||||
|
invalid_request = "잘못된 요청입니다."
|
||||||
|
invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
|
||||||
|
login_required = "로그인이 필요합니다."
|
||||||
|
request_forbidden = "요청이 거부되었습니다."
|
||||||
|
server_error = "인증 서버 오류가 발생했습니다."
|
||||||
|
temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
|
||||||
|
unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
|
||||||
|
unsupported_response_type = "지원하지 않는 응답 타입입니다."
|
||||||
|
|
||||||
[msg.userfront.error.tenant]
|
[msg.userfront.error.tenant]
|
||||||
account = "계정"
|
account = "계정"
|
||||||
account_unknown = "알 수 없음"
|
account_unknown = "알 수 없음"
|
||||||
@@ -379,24 +174,8 @@ tenant = "소속 테넌트"
|
|||||||
tenant_unknown = "알 수 없음"
|
tenant_unknown = "알 수 없음"
|
||||||
title = "접근 제한 정보"
|
title = "접근 제한 정보"
|
||||||
|
|
||||||
[msg.userfront.error.ory]
|
|
||||||
"$normalizedCode" = "{error}"
|
|
||||||
access_denied = "사용자가 동의를 거부했습니다."
|
|
||||||
consent_required = "앱 접근 동의가 필요합니다."
|
|
||||||
interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
|
|
||||||
invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
|
|
||||||
invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
|
|
||||||
invalid_request = "잘못된 요청입니다."
|
|
||||||
invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
|
|
||||||
login_required = "로그인이 필요합니다."
|
|
||||||
request_forbidden = "요청이 거부되었습니다."
|
|
||||||
server_error = "인증 서버 오류가 발생했습니다."
|
|
||||||
temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
|
|
||||||
unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
|
|
||||||
unsupported_response_type = "지원하지 않는 응답 타입입니다."
|
|
||||||
|
|
||||||
[msg.userfront.error.whitelist]
|
[msg.userfront.error.whitelist]
|
||||||
"$normalizedCode" = "{error}"
|
$normalizedCode = "{error}"
|
||||||
bad_request = "입력값을 확인해 주세요."
|
bad_request = "입력값을 확인해 주세요."
|
||||||
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
|
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
|
||||||
not_found = "요청한 페이지를 찾을 수 없습니다."
|
not_found = "요청한 페이지를 찾을 수 없습니다."
|
||||||
@@ -450,14 +229,14 @@ scan_hint = "모바일 앱으로 스캔하세요"
|
|||||||
invalid = "문자 2개와 숫자 6자리를 입력해 주세요."
|
invalid = "문자 2개와 숫자 6자리를 입력해 주세요."
|
||||||
|
|
||||||
[msg.userfront.login.unregistered]
|
[msg.userfront.login.unregistered]
|
||||||
body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주세요."
|
body = "가입되지 않은 정보입니다.\\\\\\\\n회원가입 후 이용해 주세요."
|
||||||
|
|
||||||
[msg.userfront.login.verification]
|
[msg.userfront.login.verification]
|
||||||
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
|
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
|
||||||
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
|
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
|
||||||
approved_remote = "요청하신 로그인이 완료되었습니다"
|
approved_remote = "요청하신 로그인이 완료되었습니다"
|
||||||
pending_remote = "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요."
|
|
||||||
close_hint = "이 창은 이제 닫으셔도 됩니다."
|
close_hint = "이 창은 이제 닫으셔도 됩니다."
|
||||||
|
pending_remote = "승인 요청을 확인하고 있습니다. 잠시만 기다려 주세요."
|
||||||
success = "로그인 승인에 성공했습니다."
|
success = "로그인 승인에 성공했습니다."
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
[msg.userfront.login_success]
|
||||||
@@ -532,6 +311,7 @@ uppercase = "대문자 1개 이상"
|
|||||||
[msg.userfront.sections]
|
[msg.userfront.sections]
|
||||||
apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
|
apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
|
||||||
audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
|
audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
|
||||||
|
sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다."
|
||||||
|
|
||||||
[msg.userfront.settings]
|
[msg.userfront.settings]
|
||||||
disabled = "현재 계정 설정 화면은 준비 중입니다."
|
disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||||
@@ -546,12 +326,12 @@ all_hint = "필수 약관 2개를 모두 확인하고 동의하면 다음 단계
|
|||||||
description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요."
|
description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요."
|
||||||
privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다."
|
privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다."
|
||||||
progress = "필수 약관 {total}개 중 {count}개 동의 완료"
|
progress = "필수 약관 {total}개 중 {count}개 동의 완료"
|
||||||
title = "서비스 이용을 위해\\\\n약관에 동의해주세요"
|
title = "서비스 이용을 위해\\\\\\\\n약관에 동의해주세요"
|
||||||
tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다."
|
tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다."
|
||||||
|
|
||||||
[msg.userfront.signup.auth]
|
[msg.userfront.signup.auth]
|
||||||
affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요."
|
affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요."
|
||||||
title = "본인 확인을 위해\\\\n인증을 진행해주세요"
|
title = "본인 확인을 위해\\\\\\\\n인증을 진행해주세요"
|
||||||
|
|
||||||
[msg.userfront.signup.email]
|
[msg.userfront.signup.email]
|
||||||
code_mismatch = "인증코드가 일치하지 않습니다."
|
code_mismatch = "인증코드가 일치하지 않습니다."
|
||||||
@@ -567,7 +347,7 @@ lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다.
|
|||||||
mismatch = "비밀번호가 일치하지 않습니다."
|
mismatch = "비밀번호가 일치하지 않습니다."
|
||||||
number_required = "숫자가 최소 1개 이상 포함되어야 합니다."
|
number_required = "숫자가 최소 1개 이상 포함되어야 합니다."
|
||||||
symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다."
|
symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다."
|
||||||
title = "마지막으로\\\\n비밀번호를 설정해주세요"
|
title = "마지막으로\\\\\\\\n비밀번호를 설정해주세요"
|
||||||
uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다."
|
uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다."
|
||||||
|
|
||||||
[msg.userfront.signup.password.rule]
|
[msg.userfront.signup.password.rule]
|
||||||
@@ -596,7 +376,7 @@ uppercase = "대문자"
|
|||||||
|
|
||||||
[msg.userfront.signup.profile]
|
[msg.userfront.signup.profile]
|
||||||
affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다."
|
affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다."
|
||||||
title = "회원님의\\\\n소속 정보를 알려주세요"
|
title = "회원님의\\\\\\\\n소속 정보를 알려주세요"
|
||||||
|
|
||||||
[msg.userfront.signup.success]
|
[msg.userfront.signup.success]
|
||||||
body = "성공적으로 가입되었습니다."
|
body = "성공적으로 가입되었습니다."
|
||||||
@@ -688,6 +468,10 @@ dev_console = "Dev Console"
|
|||||||
|
|
||||||
[ui.userfront.audit]
|
[ui.userfront.audit]
|
||||||
|
|
||||||
|
[ui.userfront.audit.filter]
|
||||||
|
title = "내 활동 관리"
|
||||||
|
toggle_label = "활성 세션만 보기"
|
||||||
|
|
||||||
[ui.userfront.audit.table]
|
[ui.userfront.audit.table]
|
||||||
action = "관리"
|
action = "관리"
|
||||||
app = "애플리케이션"
|
app = "애플리케이션"
|
||||||
@@ -716,22 +500,12 @@ title = "동의 취소"
|
|||||||
|
|
||||||
[ui.userfront.dashboard]
|
[ui.userfront.dashboard]
|
||||||
last_auth_label = "최근 인증"
|
last_auth_label = "최근 인증"
|
||||||
|
link_status_label = "연동 상태"
|
||||||
status_history = "상태 이력"
|
status_history = "상태 이력"
|
||||||
|
|
||||||
[ui.userfront.dashboard.activity]
|
[ui.userfront.dashboard.activity]
|
||||||
linked = "연동됨"
|
linked = "연동됨"
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions]
|
|
||||||
active_badge = "활성화"
|
|
||||||
current_badge = "접속중"
|
|
||||||
current_disabled = "현재 세션"
|
|
||||||
unknown_device = "알 수 없는 기기"
|
|
||||||
unknown_session = "세션 정보"
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions.revoke]
|
|
||||||
action = "세션 종료"
|
|
||||||
title = "세션 종료"
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.approved_session]
|
[ui.userfront.dashboard.approved_session]
|
||||||
default = "승인한 세션 ID"
|
default = "승인한 세션 ID"
|
||||||
userfront = "승인한 Userfront 세션 ID"
|
userfront = "승인한 Userfront 세션 ID"
|
||||||
@@ -743,6 +517,17 @@ title = "연동 해지"
|
|||||||
[ui.userfront.dashboard.scopes]
|
[ui.userfront.dashboard.scopes]
|
||||||
title = "동의 범위"
|
title = "동의 범위"
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions]
|
||||||
|
active_badge = "활성화"
|
||||||
|
current_badge = "접속중"
|
||||||
|
current_disabled = "현재 세션"
|
||||||
|
unknown_device = "알 수 없는 기기"
|
||||||
|
unknown_session = "세션 정보"
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions.revoke]
|
||||||
|
action = "세션 종료"
|
||||||
|
title = "세션 종료"
|
||||||
|
|
||||||
[ui.userfront.dashboard.status]
|
[ui.userfront.dashboard.status]
|
||||||
revoked = "해지됨"
|
revoked = "해지됨"
|
||||||
|
|
||||||
@@ -805,10 +590,10 @@ title = "미등록 회원"
|
|||||||
|
|
||||||
[ui.userfront.login.verification]
|
[ui.userfront.login.verification]
|
||||||
action_label = "확인"
|
action_label = "확인"
|
||||||
|
action_label_close = "창 닫기"
|
||||||
action_label_remote = "로그인 창으로 이동하기"
|
action_label_remote = "로그인 창으로 이동하기"
|
||||||
page_title = "Baron SW 포탈"
|
page_title = "Baron SW 포탈"
|
||||||
title = "승인 완료"
|
title = "승인 완료"
|
||||||
action_label_close = "창 닫기"
|
|
||||||
title_pending = "로그인 승인 확인 중"
|
title_pending = "로그인 승인 확인 중"
|
||||||
title_remote = "로그인 승인 완료"
|
title_remote = "로그인 승인 완료"
|
||||||
|
|
||||||
@@ -872,6 +657,7 @@ title = "새 비밀번호 설정"
|
|||||||
[ui.userfront.sections]
|
[ui.userfront.sections]
|
||||||
apps = "나의 App 현황"
|
apps = "나의 App 현황"
|
||||||
audit = "접속이력"
|
audit = "접속이력"
|
||||||
|
sessions = "활성 세션"
|
||||||
|
|
||||||
[ui.userfront.session]
|
[ui.userfront.session]
|
||||||
active = "세션 활성"
|
active = "세션 활성"
|
||||||
@@ -919,12 +705,3 @@ verify = "본인인증"
|
|||||||
|
|
||||||
[ui.userfront.signup.success]
|
[ui.userfront.signup.success]
|
||||||
action = "로그인하기"
|
action = "로그인하기"
|
||||||
|
|
||||||
|
|
||||||
[ui.userfront.audit.filter]
|
|
||||||
title = "내 활동 관리"
|
|
||||||
toggle_label = "활성 세션만 보기"
|
|
||||||
|
|
||||||
[msg.userfront.audit.filter]
|
|
||||||
description = "활성화된 세션만 보려면 토글을 켜주세요."
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,203 +41,6 @@ verify_code_failed = ""
|
|||||||
[err.userfront.session]
|
[err.userfront.session]
|
||||||
missing = ""
|
missing = ""
|
||||||
|
|
||||||
[msg.userfront.error]
|
|
||||||
detail_contact = ""
|
|
||||||
detail_generic = ""
|
|
||||||
detail_request = ""
|
|
||||||
id = ""
|
|
||||||
title = ""
|
|
||||||
title_generic = ""
|
|
||||||
title_with_code = ""
|
|
||||||
type = ""
|
|
||||||
|
|
||||||
[msg.userfront.error.tenant]
|
|
||||||
account = ""
|
|
||||||
account_unknown = ""
|
|
||||||
affiliated_tenants = ""
|
|
||||||
allowed_box_title = ""
|
|
||||||
allowed_tenants = ""
|
|
||||||
detail = ""
|
|
||||||
load_failed = ""
|
|
||||||
loading = ""
|
|
||||||
lookup_fallback = ""
|
|
||||||
page_title = ""
|
|
||||||
primary_tenant = ""
|
|
||||||
tenant = ""
|
|
||||||
tenant_unknown = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[msg.userfront.forgot]
|
|
||||||
description = ""
|
|
||||||
dry_send = ""
|
|
||||||
error = ""
|
|
||||||
input_required = ""
|
|
||||||
sent = ""
|
|
||||||
|
|
||||||
[msg.userfront.login]
|
|
||||||
cookie_check_failed = ""
|
|
||||||
dry_send = ""
|
|
||||||
link_failed = ""
|
|
||||||
link_send_failed = ""
|
|
||||||
link_sent_email = ""
|
|
||||||
link_sent_phone = ""
|
|
||||||
link_timeout = ""
|
|
||||||
no_account = ""
|
|
||||||
oidc_failed = ""
|
|
||||||
qr_expired = ""
|
|
||||||
qr_init_failed = ""
|
|
||||||
qr_login_required = ""
|
|
||||||
token_missing = ""
|
|
||||||
verification_failed = ""
|
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
|
||||||
subtitle = ""
|
|
||||||
|
|
||||||
[msg.userfront.consent]
|
|
||||||
accept_error = ""
|
|
||||||
client_id = ""
|
|
||||||
client_unknown = ""
|
|
||||||
description = ""
|
|
||||||
load_error = ""
|
|
||||||
missing_redirect = ""
|
|
||||||
redirect_notice = ""
|
|
||||||
scope_count = ""
|
|
||||||
|
|
||||||
[msg.userfront.profile]
|
|
||||||
department_missing = ""
|
|
||||||
department_required = ""
|
|
||||||
email_missing = ""
|
|
||||||
greeting = ""
|
|
||||||
load_failed = ""
|
|
||||||
name_missing = ""
|
|
||||||
name_required = ""
|
|
||||||
phone_required = ""
|
|
||||||
phone_verify_required = ""
|
|
||||||
update_failed = ""
|
|
||||||
update_success = ""
|
|
||||||
|
|
||||||
[msg.userfront.qr]
|
|
||||||
camera_error = ""
|
|
||||||
permission_error = ""
|
|
||||||
permission_required = ""
|
|
||||||
|
|
||||||
[msg.userfront.reset]
|
|
||||||
invalid_body = ""
|
|
||||||
invalid_link = ""
|
|
||||||
invalid_title = ""
|
|
||||||
policy_loading = ""
|
|
||||||
success = ""
|
|
||||||
|
|
||||||
[msg.userfront.sections]
|
|
||||||
apps_subtitle = ""
|
|
||||||
audit_subtitle = ""
|
|
||||||
sessions_subtitle = ""
|
|
||||||
|
|
||||||
[msg.userfront.settings]
|
|
||||||
disabled = ""
|
|
||||||
|
|
||||||
[msg.userfront.signup]
|
|
||||||
failed = ""
|
|
||||||
privacy_full = ""
|
|
||||||
tos_full = ""
|
|
||||||
|
|
||||||
[ui.common.badge]
|
|
||||||
admin_only = ""
|
|
||||||
command_only = ""
|
|
||||||
system = ""
|
|
||||||
|
|
||||||
[ui.common.status]
|
|
||||||
active = ""
|
|
||||||
blocked = ""
|
|
||||||
failure = ""
|
|
||||||
inactive = ""
|
|
||||||
ok = ""
|
|
||||||
pending = ""
|
|
||||||
success = ""
|
|
||||||
|
|
||||||
[ui.userfront.app_label]
|
|
||||||
admin_console = ""
|
|
||||||
baron = ""
|
|
||||||
dev_console = ""
|
|
||||||
|
|
||||||
[ui.userfront.auth_method]
|
|
||||||
ory = ""
|
|
||||||
session = ""
|
|
||||||
|
|
||||||
[ui.userfront.dashboard]
|
|
||||||
link_status_label = ""
|
|
||||||
last_auth_label = ""
|
|
||||||
status_history = ""
|
|
||||||
|
|
||||||
[ui.userfront.device]
|
|
||||||
android = ""
|
|
||||||
ios = ""
|
|
||||||
linux = ""
|
|
||||||
macos = ""
|
|
||||||
windows = ""
|
|
||||||
|
|
||||||
[ui.userfront.error]
|
|
||||||
go_home = ""
|
|
||||||
go_login = ""
|
|
||||||
switch_account = ""
|
|
||||||
|
|
||||||
[ui.userfront.forgot]
|
|
||||||
heading = ""
|
|
||||||
input_label = ""
|
|
||||||
submit = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.login]
|
|
||||||
forgot_password = ""
|
|
||||||
signup = ""
|
|
||||||
|
|
||||||
[ui.userfront.login_success]
|
|
||||||
later = ""
|
|
||||||
qr = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.consent]
|
|
||||||
accept = ""
|
|
||||||
requested_scopes = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.nav]
|
|
||||||
dashboard = ""
|
|
||||||
logout = ""
|
|
||||||
profile = ""
|
|
||||||
qr_scan = ""
|
|
||||||
|
|
||||||
[ui.userfront.profile]
|
|
||||||
department_empty = ""
|
|
||||||
manage = ""
|
|
||||||
user_fallback = ""
|
|
||||||
|
|
||||||
[ui.userfront.qr]
|
|
||||||
rescan = ""
|
|
||||||
result_success = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.reset]
|
|
||||||
confirm_password = ""
|
|
||||||
new_password = ""
|
|
||||||
submit = ""
|
|
||||||
subtitle = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.sections]
|
|
||||||
apps = ""
|
|
||||||
audit = ""
|
|
||||||
sessions = ""
|
|
||||||
|
|
||||||
[ui.userfront.session]
|
|
||||||
active = ""
|
|
||||||
unknown = ""
|
|
||||||
|
|
||||||
[ui.userfront.signup]
|
|
||||||
complete = ""
|
|
||||||
next_step = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[msg.userfront]
|
[msg.userfront]
|
||||||
greeting = ""
|
greeting = ""
|
||||||
|
|
||||||
@@ -253,6 +56,9 @@ result = ""
|
|||||||
session_id = ""
|
session_id = ""
|
||||||
status = ""
|
status = ""
|
||||||
|
|
||||||
|
[msg.userfront.audit.filter]
|
||||||
|
description = ""
|
||||||
|
|
||||||
[msg.userfront.consent]
|
[msg.userfront.consent]
|
||||||
accept_error = ""
|
accept_error = ""
|
||||||
client_id = ""
|
client_id = ""
|
||||||
@@ -279,14 +85,15 @@ approved_device = ""
|
|||||||
approved_ip = ""
|
approved_ip = ""
|
||||||
audit_empty = ""
|
audit_empty = ""
|
||||||
audit_load_error = ""
|
audit_load_error = ""
|
||||||
auto_login_supported = ""
|
|
||||||
auth_method = ""
|
auth_method = ""
|
||||||
|
auto_login_supported = ""
|
||||||
client_id = ""
|
client_id = ""
|
||||||
client_id_missing = ""
|
client_id_missing = ""
|
||||||
current_status = ""
|
current_status = ""
|
||||||
last_auth = ""
|
last_auth = ""
|
||||||
link_missing = ""
|
link_missing = ""
|
||||||
link_open_error = ""
|
link_open_error = ""
|
||||||
|
link_status = ""
|
||||||
render_error = ""
|
render_error = ""
|
||||||
session_id_copied = ""
|
session_id_copied = ""
|
||||||
|
|
||||||
@@ -295,6 +102,19 @@ empty = ""
|
|||||||
empty_detail = ""
|
empty_detail = ""
|
||||||
error = ""
|
error = ""
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.approved_session]
|
||||||
|
copy_click = ""
|
||||||
|
copy_tap = ""
|
||||||
|
none = ""
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.revoke]
|
||||||
|
confirm = ""
|
||||||
|
error = ""
|
||||||
|
success = ""
|
||||||
|
|
||||||
|
[msg.userfront.dashboard.scopes]
|
||||||
|
empty = ""
|
||||||
|
|
||||||
[msg.userfront.dashboard.sessions]
|
[msg.userfront.dashboard.sessions]
|
||||||
browser = ""
|
browser = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
@@ -309,19 +129,6 @@ confirm = ""
|
|||||||
error = ""
|
error = ""
|
||||||
success = ""
|
success = ""
|
||||||
|
|
||||||
[msg.userfront.dashboard.approved_session]
|
|
||||||
copy_click = ""
|
|
||||||
copy_tap = ""
|
|
||||||
none = ""
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.revoke]
|
|
||||||
confirm = ""
|
|
||||||
error = ""
|
|
||||||
success = ""
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.scopes]
|
|
||||||
empty = ""
|
|
||||||
|
|
||||||
[msg.userfront.dashboard.timeline]
|
[msg.userfront.dashboard.timeline]
|
||||||
load_error = ""
|
load_error = ""
|
||||||
|
|
||||||
@@ -335,6 +142,22 @@ title_generic = ""
|
|||||||
title_with_code = ""
|
title_with_code = ""
|
||||||
type = ""
|
type = ""
|
||||||
|
|
||||||
|
[msg.userfront.error.ory]
|
||||||
|
$normalizedCode = ""
|
||||||
|
access_denied = ""
|
||||||
|
consent_required = ""
|
||||||
|
interaction_required = ""
|
||||||
|
invalid_client = ""
|
||||||
|
invalid_grant = ""
|
||||||
|
invalid_request = ""
|
||||||
|
invalid_scope = ""
|
||||||
|
login_required = ""
|
||||||
|
request_forbidden = ""
|
||||||
|
server_error = ""
|
||||||
|
temporarily_unavailable = ""
|
||||||
|
unauthorized_client = ""
|
||||||
|
unsupported_response_type = ""
|
||||||
|
|
||||||
[msg.userfront.error.tenant]
|
[msg.userfront.error.tenant]
|
||||||
account = ""
|
account = ""
|
||||||
account_unknown = ""
|
account_unknown = ""
|
||||||
@@ -351,24 +174,8 @@ tenant = ""
|
|||||||
tenant_unknown = ""
|
tenant_unknown = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[msg.userfront.error.ory]
|
|
||||||
"$normalizedCode" = ""
|
|
||||||
access_denied = ""
|
|
||||||
consent_required = ""
|
|
||||||
interaction_required = ""
|
|
||||||
invalid_client = ""
|
|
||||||
invalid_grant = ""
|
|
||||||
invalid_request = ""
|
|
||||||
invalid_scope = ""
|
|
||||||
login_required = ""
|
|
||||||
request_forbidden = ""
|
|
||||||
server_error = ""
|
|
||||||
temporarily_unavailable = ""
|
|
||||||
unauthorized_client = ""
|
|
||||||
unsupported_response_type = ""
|
|
||||||
|
|
||||||
[msg.userfront.error.whitelist]
|
[msg.userfront.error.whitelist]
|
||||||
"$normalizedCode" = ""
|
$normalizedCode = ""
|
||||||
bad_request = ""
|
bad_request = ""
|
||||||
invalid_session = ""
|
invalid_session = ""
|
||||||
not_found = ""
|
not_found = ""
|
||||||
@@ -428,8 +235,8 @@ body = ""
|
|||||||
approved = ""
|
approved = ""
|
||||||
approved_local = ""
|
approved_local = ""
|
||||||
approved_remote = ""
|
approved_remote = ""
|
||||||
pending_remote = ""
|
|
||||||
close_hint = ""
|
close_hint = ""
|
||||||
|
pending_remote = ""
|
||||||
success = ""
|
success = ""
|
||||||
|
|
||||||
[msg.userfront.login_success]
|
[msg.userfront.login_success]
|
||||||
@@ -504,6 +311,7 @@ uppercase = ""
|
|||||||
[msg.userfront.sections]
|
[msg.userfront.sections]
|
||||||
apps_subtitle = ""
|
apps_subtitle = ""
|
||||||
audit_subtitle = ""
|
audit_subtitle = ""
|
||||||
|
sessions_subtitle = ""
|
||||||
|
|
||||||
[msg.userfront.settings]
|
[msg.userfront.settings]
|
||||||
disabled = ""
|
disabled = ""
|
||||||
@@ -660,6 +468,10 @@ dev_console = ""
|
|||||||
|
|
||||||
[ui.userfront.audit]
|
[ui.userfront.audit]
|
||||||
|
|
||||||
|
[ui.userfront.audit.filter]
|
||||||
|
title = ""
|
||||||
|
toggle_label = ""
|
||||||
|
|
||||||
[ui.userfront.audit.table]
|
[ui.userfront.audit.table]
|
||||||
action = ""
|
action = ""
|
||||||
app = ""
|
app = ""
|
||||||
@@ -688,22 +500,12 @@ title = ""
|
|||||||
|
|
||||||
[ui.userfront.dashboard]
|
[ui.userfront.dashboard]
|
||||||
last_auth_label = ""
|
last_auth_label = ""
|
||||||
|
link_status_label = ""
|
||||||
status_history = ""
|
status_history = ""
|
||||||
|
|
||||||
[ui.userfront.dashboard.activity]
|
[ui.userfront.dashboard.activity]
|
||||||
linked = ""
|
linked = ""
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions]
|
|
||||||
active_badge = ""
|
|
||||||
current_badge = ""
|
|
||||||
current_disabled = ""
|
|
||||||
unknown_device = ""
|
|
||||||
unknown_session = ""
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.sessions.revoke]
|
|
||||||
action = ""
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
[ui.userfront.dashboard.approved_session]
|
[ui.userfront.dashboard.approved_session]
|
||||||
default = ""
|
default = ""
|
||||||
userfront = ""
|
userfront = ""
|
||||||
@@ -715,6 +517,17 @@ title = ""
|
|||||||
[ui.userfront.dashboard.scopes]
|
[ui.userfront.dashboard.scopes]
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions]
|
||||||
|
active_badge = ""
|
||||||
|
current_badge = ""
|
||||||
|
current_disabled = ""
|
||||||
|
unknown_device = ""
|
||||||
|
unknown_session = ""
|
||||||
|
|
||||||
|
[ui.userfront.dashboard.sessions.revoke]
|
||||||
|
action = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
[ui.userfront.dashboard.status]
|
[ui.userfront.dashboard.status]
|
||||||
revoked = ""
|
revoked = ""
|
||||||
|
|
||||||
@@ -777,8 +590,8 @@ title = ""
|
|||||||
|
|
||||||
[ui.userfront.login.verification]
|
[ui.userfront.login.verification]
|
||||||
action_label = ""
|
action_label = ""
|
||||||
action_label_remote = ""
|
|
||||||
action_label_close = ""
|
action_label_close = ""
|
||||||
|
action_label_remote = ""
|
||||||
page_title = ""
|
page_title = ""
|
||||||
title = ""
|
title = ""
|
||||||
title_pending = ""
|
title_pending = ""
|
||||||
@@ -844,6 +657,7 @@ title = ""
|
|||||||
[ui.userfront.sections]
|
[ui.userfront.sections]
|
||||||
apps = ""
|
apps = ""
|
||||||
audit = ""
|
audit = ""
|
||||||
|
sessions = ""
|
||||||
|
|
||||||
[ui.userfront.session]
|
[ui.userfront.session]
|
||||||
active = ""
|
active = ""
|
||||||
@@ -891,12 +705,3 @@ verify = ""
|
|||||||
|
|
||||||
[ui.userfront.signup.success]
|
[ui.userfront.signup.success]
|
||||||
action = ""
|
action = ""
|
||||||
|
|
||||||
|
|
||||||
[ui.userfront.audit.filter]
|
|
||||||
title = ""
|
|
||||||
toggle_label = ""
|
|
||||||
|
|
||||||
[msg.userfront.audit.filter]
|
|
||||||
description = ""
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user