forked from baron/baron-sso
3단계 권한 모델 확장, keto 권한 정책
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import RoleSwitcher from "./RoleSwitcher";
|
||||
|
||||
const navItems = [
|
||||
{ label: "Overview", to: "/", icon: LayoutDashboard },
|
||||
@@ -132,6 +133,7 @@ function AppLayout() {
|
||||
<main className="px-5 py-6 md:px-10 md:py-10">
|
||||
<Outlet />
|
||||
</main>
|
||||
<RoleSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
68
adminfront/src/components/layout/RoleSwitcher.tsx
Normal file
68
adminfront/src/components/layout/RoleSwitcher.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const RoleSwitcher: React.FC = () => {
|
||||
const [currentRole, setCurrentRole] = useState<string>('super_admin');
|
||||
|
||||
useEffect(() => {
|
||||
// localStorage에서 역할 읽기
|
||||
const savedRole = window.localStorage.getItem('X-Mock-Role');
|
||||
if (savedRole) {
|
||||
setCurrentRole(savedRole);
|
||||
} else {
|
||||
// 기본값 설정
|
||||
window.localStorage.setItem('X-Mock-Role', 'super_admin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const switchRole = (role: string) => {
|
||||
// localStorage 설정
|
||||
window.localStorage.setItem('X-Mock-Role', role);
|
||||
setCurrentRole(role);
|
||||
// 페이지 새로고침하여 권한 적용
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
zIndex: 9999,
|
||||
background: '#1A1F2C',
|
||||
color: 'white',
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold', borderBottom: '1px solid #444', paddingBottom: '4px', marginBottom: '4px' }}>
|
||||
🛠 DEV Role Switcher
|
||||
</div>
|
||||
{(['super_admin', 'tenant_admin', 'user'] as const).map(role => (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => switchRole(role)}
|
||||
style={{
|
||||
background: currentRole === role ? '#3b82f6' : '#333',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
transition: 'background 0.2s'
|
||||
}}
|
||||
>
|
||||
{role.toUpperCase()} {currentRole === role ? '✅' : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleSwitcher;
|
||||
@@ -127,7 +127,18 @@ function TenantListPage() {
|
||||
<TableCell>{tenant.slug}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={tenant.status === "active" ? "default" : "muted"}
|
||||
variant={
|
||||
tenant.status === "active"
|
||||
? "default"
|
||||
: tenant.status === "pending"
|
||||
? "secondary"
|
||||
: "muted"
|
||||
}
|
||||
className={
|
||||
tenant.status === "pending"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{tenant.status}
|
||||
</Badge>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Save, Trash2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -15,6 +15,7 @@ import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import {
|
||||
approveTenant,
|
||||
deleteTenant,
|
||||
fetchTenant,
|
||||
updateTenant,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
export function TenantProfilePage() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
if (!tenantId) {
|
||||
return <div>Tenant ID is missing</div>;
|
||||
@@ -62,7 +64,18 @@ export function TenantProfilePage() {
|
||||
.filter((d) => d !== ""),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
navigate("/tenants");
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||
alert("Tenant updated successfully");
|
||||
},
|
||||
});
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: () => approveTenant(tenantId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||
alert("Tenant approved successfully");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -84,6 +97,12 @@ export function TenantProfilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = () => {
|
||||
if (window.confirm("Approve this tenant?")) {
|
||||
approveMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-[var(--color-panel)] mt-6">
|
||||
@@ -168,6 +187,16 @@ export function TenantProfilePage() {
|
||||
Delete
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{status === "pending" && (
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={handleApprove}
|
||||
disabled={approveMutation.isPending}
|
||||
>
|
||||
Approve Tenant
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => navigate("/tenants")}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -132,6 +132,13 @@ export async function deleteTenant(tenantId: string) {
|
||||
await apiClient.delete(`/v1/admin/tenants/${tenantId}`);
|
||||
}
|
||||
|
||||
export async function approveTenant(tenantId: string) {
|
||||
const { data } = await apiClient.post<TenantSummary>(
|
||||
`/v1/admin/tenants/${tenantId}/approve`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// API Key Management (M2M)
|
||||
export type ApiKeyCreateRequest = {
|
||||
name: string;
|
||||
|
||||
@@ -17,6 +17,12 @@ apiClient.interceptors.request.use((config) => {
|
||||
config.headers["X-Tenant-ID"] = tenantId;
|
||||
}
|
||||
|
||||
// [Development Only] Inject Mock Role from RoleSwitcher
|
||||
const mockRole = window.localStorage.getItem("X-Mock-Role");
|
||||
if (mockRole) {
|
||||
config.headers["X-Test-Role"] = mockRole;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user