forked from baron/baron-sso
adminfront 및 백엔드: 전 메뉴 및 탭 수준 ReBAC 기반 접근 제어(Admin Control) 기능 추가 구현 완료
This commit is contained in:
@@ -14,9 +14,9 @@ import {
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { toast } from "../../../components/ui/use-toast";
|
||||
import { fetchMe, fetchTenant, updateTenant } from "../../../lib/adminApi";
|
||||
import { fetchTenant, updateTenant } from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
import { normalizeAdminRole } from "../../../lib/roles";
|
||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||
import {
|
||||
createSchemaField,
|
||||
isSchemaFieldType,
|
||||
@@ -28,13 +28,9 @@ export function TenantSchemaPage() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: profile, isLoading: isProfileLoading } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: fetchMe,
|
||||
});
|
||||
|
||||
const profileRole = normalizeAdminRole(profile?.role);
|
||||
const canAccess = profileRole === "super_admin";
|
||||
const { hasPermission, isLoading: isPermissionLoading } = useTenantPermission(tenantId ?? "");
|
||||
const canView = hasPermission("view_schema") || hasPermission("view");
|
||||
const isWritable = hasPermission("manage_schema") || hasPermission("manage");
|
||||
|
||||
const tenantQuery = useQuery({
|
||||
queryKey: ["tenant", tenantId],
|
||||
@@ -42,7 +38,7 @@ export function TenantSchemaPage() {
|
||||
if (!tenantId) throw new Error("Tenant ID is required");
|
||||
return fetchTenant(tenantId);
|
||||
},
|
||||
enabled: !!tenantId && canAccess,
|
||||
enabled: !!tenantId && canView,
|
||||
});
|
||||
|
||||
const [fields, setFields] = useState<SchemaField[]>([]);
|
||||
@@ -85,7 +81,7 @@ export function TenantSchemaPage() {
|
||||
},
|
||||
});
|
||||
|
||||
if (isProfileLoading) {
|
||||
if (isPermissionLoading) {
|
||||
return (
|
||||
<div className="p-8 text-center animate-pulse text-muted-foreground">
|
||||
{t("msg.common.loading", "로딩 중...")}
|
||||
@@ -93,7 +89,7 @@ export function TenantSchemaPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!canAccess) {
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="p-12 text-center space-y-4 bg-destructive/5 rounded-2xl border border-destructive/20 mt-6">
|
||||
<h3 className="text-xl font-bold text-destructive">
|
||||
@@ -147,7 +143,7 @@ export function TenantSchemaPage() {
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button onClick={addField} size="sm">
|
||||
<Button onClick={addField} size="sm" disabled={!isWritable}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{t("ui.admin.tenants.schema.add_field", "필드 추가")}
|
||||
</Button>
|
||||
@@ -182,6 +178,7 @@ export function TenantSchemaPage() {
|
||||
"예: employee_id",
|
||||
)}
|
||||
className="h-10"
|
||||
disabled={!isWritable}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -198,6 +195,7 @@ export function TenantSchemaPage() {
|
||||
"예: 사번",
|
||||
)}
|
||||
className="h-10"
|
||||
disabled={!isWritable}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -207,8 +205,9 @@ export function TenantSchemaPage() {
|
||||
<select
|
||||
id={`tenant-schema-field-type-${field.key || index}`}
|
||||
name={`tenant-schema-field-type-${field.key || index}`}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:ring-1 focus:ring-primary"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:ring-1 focus:ring-primary disabled:opacity-60"
|
||||
value={field.type}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) => {
|
||||
const nextType = e.target.value;
|
||||
if (isSchemaFieldType(nextType)) {
|
||||
@@ -271,10 +270,11 @@ export function TenantSchemaPage() {
|
||||
name={`tenant-schema-field-required-${field.key || index}`}
|
||||
type="checkbox"
|
||||
checked={field.required}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, { required: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-60"
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{t("ui.admin.tenants.schema.field.required", "필수 입력")}
|
||||
@@ -285,10 +285,11 @@ export function TenantSchemaPage() {
|
||||
name={`tenant-schema-field-admin-only-${field.key || index}`}
|
||||
type="checkbox"
|
||||
checked={field.adminOnly}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, { adminOnly: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-60"
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
@@ -302,6 +303,7 @@ export function TenantSchemaPage() {
|
||||
name={`tenant-schema-field-login-id-${field.key || index}`}
|
||||
type="checkbox"
|
||||
checked={field.isLoginId || false}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, {
|
||||
isLoginId: e.target.checked,
|
||||
@@ -309,7 +311,7 @@ export function TenantSchemaPage() {
|
||||
type: e.target.checked ? "text" : field.type,
|
||||
})
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-60"
|
||||
/>
|
||||
<span className="text-sm font-medium text-blue-600">
|
||||
{t(
|
||||
@@ -323,7 +325,7 @@ export function TenantSchemaPage() {
|
||||
name={`tenant-schema-field-indexed-${field.key || index}`}
|
||||
type="checkbox"
|
||||
checked={field.indexed || field.isLoginId || false}
|
||||
disabled={field.isLoginId}
|
||||
disabled={field.isLoginId || !isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, { indexed: e.target.checked })
|
||||
}
|
||||
@@ -342,10 +344,11 @@ export function TenantSchemaPage() {
|
||||
name={`tenant-schema-field-unsigned-${field.key || index}`}
|
||||
type="checkbox"
|
||||
checked={field.unsigned}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, { unsigned: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-60"
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
@@ -359,6 +362,7 @@ export function TenantSchemaPage() {
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={field.validation}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
updateField(index, { validation: e.target.value })
|
||||
}
|
||||
@@ -375,6 +379,7 @@ export function TenantSchemaPage() {
|
||||
size="icon"
|
||||
className="text-destructive hover:bg-destructive/10 h-10 w-10"
|
||||
onClick={() => removeField(index)}
|
||||
disabled={!isWritable}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
@@ -388,7 +393,7 @@ export function TenantSchemaPage() {
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
onClick={() => updateMutation.mutate(fields)}
|
||||
disabled={updateMutation.isPending || tenantQuery.isLoading}
|
||||
disabled={updateMutation.isPending || tenantQuery.isLoading || !isWritable}
|
||||
className="px-8 h-11"
|
||||
>
|
||||
<Save size={18} className="mr-2" />
|
||||
|
||||
Reference in New Issue
Block a user