diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index 34c112d0..1c994bed 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -11,11 +11,6 @@ import TenantDetailPage from "../features/tenants/routes/TenantDetailPage"; import TenantListPage from "../features/tenants/routes/TenantListPage"; import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage"; import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage"; -import TenantRelyingPartyListPage from "../features/tenants/routes/TenantRelyingPartyListPage"; -import TenantRelyingPartyCreatePage from "../features/tenants/routes/TenantRelyingPartyCreatePage"; -import TenantRelyingPartyDetailPage from "../features/tenants/routes/TenantRelyingPartyDetailPage"; -import RelyingPartyListPage from "../features/relying-parties/RelyingPartyListPage"; -import RelyingPartyCreatePage from "../features/relying-parties/RelyingPartyCreatePage"; import UserCreatePage from "../features/users/UserCreatePage"; import UserDetailPage from "../features/users/UserDetailPage"; import UserListPage from "../features/users/UserListPage"; @@ -33,8 +28,6 @@ export const router = createBrowserRouter( { path: "users", element: }, { path: "users/new", element: }, { path: "users/:id", element: }, - { path: "relying-parties", element: }, - { path: "relying-parties/new", element: }, { path: "tenants", element: }, { path: "tenants/new", element: }, { @@ -43,9 +36,6 @@ export const router = createBrowserRouter( children: [ { index: true, element: }, { path: "schema", element: }, - { path: "relying-parties", element: }, - { path: "relying-parties/new", element: }, - { path: "relying-parties/:id", element: }, ], }, { path: "api-keys", element: }, diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index b29a3cd4..03f37346 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -9,7 +9,6 @@ import { ShieldHalf, Sun, Users, - Share2, } from "lucide-react"; import { useEffect, useState } from "react"; import { NavLink, Outlet } from "react-router-dom"; @@ -20,7 +19,6 @@ const navItems = [ { label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf }, { label: "Tenants", to: "/tenants", icon: Building2 }, { label: "Users", to: "/users", icon: Users }, - { label: "Applications", to: "/relying-parties", icon: Share2 }, { label: "API Keys (M2M)", to: "/api-keys", icon: Key }, { label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs }, { label: "Auth Guard", to: "/auth", icon: KeyRound }, diff --git a/adminfront/src/features/relying-parties/RelyingPartyCreatePage.tsx b/adminfront/src/features/relying-parties/RelyingPartyCreatePage.tsx deleted file mode 100644 index ccdaf99d..00000000 --- a/adminfront/src/features/relying-parties/RelyingPartyCreatePage.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import type { AxiosError } from "axios"; -import { ArrowLeft, Save } from "lucide-react"; -import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import { Button } from "../../components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../../components/ui/card"; -import { Input } from "../../components/ui/input"; -import { Label } from "../../components/ui/label"; -import { - createRelyingParty, - fetchTenants, -} from "../../lib/adminApi"; -import type { HydraClientReq } from "../../lib/adminApi"; -import { Badge } from "../../components/ui/badge"; - -function RelyingPartyCreatePage() { - const navigate = useNavigate(); - const queryClient = useQueryClient(); - - const [selectedTenantId, setSelectedTenantId] = useState(""); - const [formData, setFormData] = useState({ - client_name: "", - redirect_uris: [], - scope: "openid profile email", - grant_types: ["authorization_code", "refresh_token"], - response_types: ["code"], - token_endpoint_auth_method: "client_secret_basic", - }); - const [redirectUriInput, setRedirectUriInput] = useState(""); - - // 테넌트 목록 조회 (선택용) - const { data: tenantsData } = useQuery({ - queryKey: ["tenants", { limit: 100 }], - queryFn: () => fetchTenants(100, 0), - }); - const tenants = tenantsData?.items ?? []; - - const createMutation = useMutation({ - mutationFn: (data: HydraClientReq) => createRelyingParty(selectedTenantId, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["relyingParties"] }); - navigate("/relying-parties"); - }, - }); - - const errorMsg = (createMutation.error as AxiosError<{ error?: string }>) - ?.response?.data?.error; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!selectedTenantId) { - alert("소속될 테넌트를 선택해주세요."); - return; - } - createMutation.mutate(formData); - }; - - const addRedirectUri = () => { - if (!redirectUriInput.trim()) return; - setFormData((prev) => ({ - ...prev, - redirect_uris: [...prev.redirect_uris, redirectUriInput.trim()], - })); - setRedirectUriInput(""); - }; - - const removeRedirectUri = (index: number) => { - setFormData((prev) => ({ - ...prev, - redirect_uris: prev.redirect_uris.filter((_, i) => i !== index), - })); - }; - - return ( -
-
-
-
- - - Applications - - / - New App -
-

새 애플리케이션 등록

-

- 전체 시스템 차원에서 새로운 Relying Party를 등록합니다. -

-
-
- -
- - - App & Tenant Assignment - - 애플리케이션이 소속될 테넌트와 기본 정보를 설정합니다. - - - - {errorMsg && ( -
- {errorMsg} -
- )} - - {/* 테넌트 선택 추가 */} -
- - -

이 앱을 관리할 조직을 선택하세요.

-
- -
- - - setFormData({ ...formData, client_name: e.target.value }) - } - placeholder="My Awesome App" - required - /> -
- -
- -
- setRedirectUriInput(e.target.value)} - placeholder="https://myapp.com/callback" - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addRedirectUri(); - } - }} - /> - -
-
- {formData.redirect_uris.map((uri, idx) => ( - - {uri} - - - ))} -
-
- -
- - - setFormData({ ...formData, scope: e.target.value }) - } - placeholder="openid profile email" - /> -
- -
-
- - -
-
- -
- - - - -
-
-
- ); -} - -export default RelyingPartyCreatePage; diff --git a/adminfront/src/features/relying-parties/RelyingPartyListPage.tsx b/adminfront/src/features/relying-parties/RelyingPartyListPage.tsx deleted file mode 100644 index 29ade93c..00000000 --- a/adminfront/src/features/relying-parties/RelyingPartyListPage.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { AxiosError } from "axios"; -import { Pencil, Plus, RefreshCw, Trash2, Share2, Building2 } from "lucide-react"; -import { useNavigate, Link } from "react-router-dom"; -import { Button } from "../../components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../../components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "../../components/ui/table"; -import { deleteRelyingParty, fetchAllRelyingParties } from "../../lib/adminApi"; - -function RelyingPartyListPage() { - const navigate = useNavigate(); - - const query = useQuery({ - queryKey: ["relyingParties", "all"], - queryFn: fetchAllRelyingParties, - }); - - const deleteMutation = useMutation({ - mutationFn: (clientId: string) => deleteRelyingParty(clientId), - onSuccess: () => { - query.refetch(); - }, - }); - - const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response - ?.data?.error; - const fallbackError = - !errorMsg && query.isError ? "애플리케이션 목록 조회에 실패했습니다." : null; - - const items = query.data ?? []; - - const handleDelete = (clientId: string, name: string) => { - if (!window.confirm(`앱 "${name}"를 삭제할까요?`)) { - return; - } - deleteMutation.mutate(clientId); - }; - - return ( -
-
-
-
- Applications - / - List -
-

애플리케이션 관리

-

- 전체 테넌트의 Relying Party 목록을 확인하고 관리합니다. -

-
-
- - -
-
- - - -
- Application Registry - - 총 {items.length}개 앱 등록됨 - -
-
- - {(errorMsg || fallbackError) && ( -
- {errorMsg ?? fallbackError} -
- )} - - - - - NAME - TENANT ID - CLIENT ID - UPDATED - ACTIONS - - - - {query.isLoading && ( - - 로딩 중... - - )} - {!query.isLoading && items.length === 0 && ( - - - 등록된 애플리케이션이 없습니다. - - - )} - {items.map((rp) => ( - - -
- - {rp.name} -
-
- -
- - {rp.tenantId} -
-
- - {rp.clientId} - - - {rp.updatedAt - ? new Date(rp.updatedAt).toLocaleString("ko-KR") - : "-"} - - -
- - -
-
-
- ))} -
-
-
-
-
- ); -} - -export default RelyingPartyListPage; diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx index 0b8bc825..dccf8a27 100644 --- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx @@ -70,16 +70,6 @@ function TenantDetailPage() { > Schema - - Relying Parties - {/* Outlet for nested routes */} diff --git a/adminfront/src/features/tenants/routes/TenantRelyingPartyCreatePage.tsx b/adminfront/src/features/tenants/routes/TenantRelyingPartyCreatePage.tsx deleted file mode 100644 index 69ff5e9c..00000000 --- a/adminfront/src/features/tenants/routes/TenantRelyingPartyCreatePage.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { AxiosError } from "axios"; -import { ArrowLeft, Save } from "lucide-react"; -import { useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; -import { Button } from "../../../components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../../../components/ui/card"; -import { Input } from "../../../components/ui/input"; -import { Label } from "../../../components/ui/label"; -import { - createRelyingParty, -} from "../../../lib/adminApi"; -import type { HydraClientReq } from "../../../lib/adminApi"; -import { Badge } from "../../../components/ui/badge"; - -function TenantRelyingPartyCreatePage() { - const { tenantId } = useParams<{ tenantId: string }>(); - const navigate = useNavigate(); - const queryClient = useQueryClient(); - - const [formData, setFormData] = useState({ - client_name: "", - redirect_uris: [], - scope: "openid profile email", - grant_types: ["authorization_code", "refresh_token"], - response_types: ["code"], - token_endpoint_auth_method: "client_secret_basic", - }); - const [redirectUriInput, setRedirectUriInput] = useState(""); - - const createMutation = useMutation({ - mutationFn: (data: HydraClientReq) => createRelyingParty(tenantId!, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["relyingParties", tenantId] }); - navigate(`/tenants/${tenantId}/relying-parties`); - }, - }); - - const errorMsg = (createMutation.error as AxiosError<{ error?: string }>) - ?.response?.data?.error; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - createMutation.mutate(formData); - }; - - const addRedirectUri = () => { - if (!redirectUriInput.trim()) return; - setFormData((prev) => ({ - ...prev, - redirect_uris: [...prev.redirect_uris, redirectUriInput.trim()], - })); - setRedirectUriInput(""); - }; - - const removeRedirectUri = (index: number) => { - setFormData((prev) => ({ - ...prev, - redirect_uris: prev.redirect_uris.filter((_, i) => i !== index), - })); - }; - - return ( -
-
-
-
- - - Back to List - - / - New App -
-

새 애플리케이션 등록

-

- Ory Hydra OAuth2 Client를 생성하고 현재 테넌트에 연결합니다. -

-
-
- -
- - - Basic Information - - 애플리케이션의 기본 정보를 입력하세요. - - - - {errorMsg && ( -
- {errorMsg} -
- )} - -
- - - setFormData({ ...formData, client_name: e.target.value }) - } - placeholder="My Awesome App" - required - /> -
- -
- -
- setRedirectUriInput(e.target.value)} - placeholder="https://myapp.com/callback" - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addRedirectUri(); - } - }} - /> - -
-
- {formData.redirect_uris.map((uri, idx) => ( - - {uri} - - - ))} -
-

- OAuth2 인증 후 리디렉션될 URI 목록입니다. -

-
- -
- - - setFormData({ ...formData, scope: e.target.value }) - } - placeholder="openid profile email" - /> -

- Space-separated scopes. -

-
- -
-
- -
- - - -
-
- -
- - -
-
- -
- - - - -
-
-
- ); -} - -export default TenantRelyingPartyCreatePage; diff --git a/adminfront/src/features/tenants/routes/TenantRelyingPartyDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantRelyingPartyDetailPage.tsx deleted file mode 100644 index 5f0e4abb..00000000 --- a/adminfront/src/features/tenants/routes/TenantRelyingPartyDetailPage.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { ArrowLeft, Copy, ShieldCheck } from "lucide-react"; -import { Link, useParams } from "react-router-dom"; -import { Badge } from "../../../components/ui/badge"; -import { Button } from "../../../components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../../../components/ui/card"; -import { fetchRelyingParty } from "../../../lib/adminApi"; - -function TenantRelyingPartyDetailPage() { - const { tenantId, id } = useParams<{ tenantId: string; id: string }>(); - - const { data, isLoading, error } = useQuery({ - queryKey: ["relyingParty", id], - queryFn: () => fetchRelyingParty(id!), - enabled: !!id, - }); - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - alert("복사되었습니다."); - }; - - if (isLoading) return
Loading...
; - if (error) return
Error loading app details.
; - - const { relyingParty, oauth2Config } = data!; - - return ( -
-
-
-
- - - Relying Parties - - / - {relyingParty.name} -
-

{relyingParty.name}

-

- {relyingParty.description || "상세 설정을 확인하고 관리합니다."} -

-
- - - Keto Protected - -
- -
- - - OAuth2 Credentials - 연동에 필요한 클라이언트 정보입니다. - - -
-

Client ID

-
- {oauth2Config.client_id} - -
-
- -
-

Client Secret

-
- - {oauth2Config.client_secret || (oauth2Config.metadata?.client_secret as string) || "********"} - - {(oauth2Config.client_secret || oauth2Config.metadata?.client_secret) && ( - - )} -
-

- * Secret은 생성 시점에만 노출되거나 메타데이터에 암호화되어 저장될 수 있습니다. -

-
-
-
- - - - Configuration - OAuth2 동작 설정 - - -
-

Redirect URIs

-
    - {(oauth2Config.redirect_uris || []).map((uri, i) => ( -
  • {uri}
  • - ))} -
-
-
-

Allowed Scopes

-
- {(oauth2Config.scope || "").split(" ").filter(Boolean).map(s => ( - {s} - ))} -
-
-
-

Auth Method

- {oauth2Config.token_endpoint_auth_method} -
-
-
-
-
- ); -} - -export default TenantRelyingPartyDetailPage; diff --git a/adminfront/src/features/tenants/routes/TenantRelyingPartyListPage.tsx b/adminfront/src/features/tenants/routes/TenantRelyingPartyListPage.tsx deleted file mode 100644 index 11be121f..00000000 --- a/adminfront/src/features/tenants/routes/TenantRelyingPartyListPage.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { AxiosError } from "axios"; -import { Pencil, Plus, RefreshCw, Trash2, Share2 } from "lucide-react"; -import { Link, useNavigate, useParams } from "react-router-dom"; -import { Button } from "../../../components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../../../components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "../../../components/ui/table"; -import { deleteRelyingParty, fetchRelyingParties } from "../../../lib/adminApi"; - -function TenantRelyingPartyListPage() { - const { tenantId } = useParams<{ tenantId: string }>(); - const navigate = useNavigate(); - - const query = useQuery({ - queryKey: ["relyingParties", tenantId], - queryFn: () => fetchRelyingParties(tenantId!), - enabled: !!tenantId, - }); - - const deleteMutation = useMutation({ - mutationFn: (clientId: string) => deleteRelyingParty(clientId), - onSuccess: () => { - query.refetch(); - }, - }); - - const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response - ?.data?.error; - const fallbackError = - !errorMsg && query.isError ? "앱 목록 조회에 실패했습니다." : null; - - const items = query.data ?? []; - - const handleDelete = (clientId: string, name: string) => { - if (!window.confirm(`앱 "${name}"를 삭제할까요?`)) { - return; - } - deleteMutation.mutate(clientId); - }; - - return ( - - -
- Relying Parties (Apps) - - 이 테넌트에 등록된 OAuth2/OIDC 애플리케이션입니다. - -
-
- - -
-
- - {(errorMsg || fallbackError) && ( -
- {errorMsg ?? fallbackError} -
- )} - - - - - CLIENT ID - NAME - DESCRIPTION - UPDATED - ACTIONS - - - - {query.isLoading && ( - - 로딩 중... - - )} - {!query.isLoading && items.length === 0 && ( - - - 아직 등록된 앱이 없습니다. - - - )} - {items.map((rp) => ( - - {rp.clientId} - -
- - {rp.name} -
-
- {rp.description || "-"} - - {rp.updatedAt - ? new Date(rp.updatedAt).toLocaleString("ko-KR") - : "-"} - - -
- - -
-
-
- ))} -
-
-
-
- ); -} - -export default TenantRelyingPartyListPage;