Commit e0be2658 authored by ThinhNC's avatar ThinhNC

Merge branch 'feat/system-config-management-ui' into 'develop'

feat(system-config): add system configs and feature flags management UI

See merge request !15
parents 77b0973f 21546737
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
import { registerSchema, RegisterInput } from "@/schemas/auth.schema"; import { registerSchema, RegisterInput } from "@/schemas/auth.schema";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useLanguage } from "@/providers/language-provider"; import { useLanguage } from "@/providers/language-provider";
import { usePublicConfigs } from "@/hooks/use-system-config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
...@@ -27,6 +28,11 @@ import { Label } from "@/components/ui/label"; ...@@ -27,6 +28,11 @@ import { Label } from "@/components/ui/label";
export default function RegisterPage() { export default function RegisterPage() {
const { t } = useLanguage(); const { t } = useLanguage();
const { register: registerUser } = useAuth(); const { register: registerUser } = useAuth();
const { isFeatureEnabled } = usePublicConfigs();
const isRegistrationEnabled = isFeatureEnabled(
"feature.registration.enabled",
true,
);
const router = useRouter(); const router = useRouter();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
...@@ -135,7 +141,20 @@ export default function RegisterPage() { ...@@ -135,7 +141,20 @@ export default function RegisterPage() {
</div> </div>
)} )}
{/* Form */} {/* Registration Disabled Feature Flag Alert */}
{!isRegistrationEnabled && (
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1">
<div className="font-bold flex items-center gap-1.5">
<XCircle className="h-4 w-4 text-amber-500" />
<span>Đăng Ký Tài Khoản Đang Tạm Khóa</span>
</div>
<p className="text-[11px] leading-relaxed text-muted-foreground">
Quản trị viên hệ thống đã tạm thời khóa tính năng đăng ký người dùng mới. Vui lòng liên hệ quản trị viên hoặc quay lại sau.
</p>
</div>
)}
{/* Register Form */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Full Name */} {/* Full Name */}
<div className="space-y-1.5"> <div className="space-y-1.5">
...@@ -294,14 +313,16 @@ export default function RegisterPage() { ...@@ -294,14 +313,16 @@ export default function RegisterPage() {
{/* Submit Button */} {/* Submit Button */}
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting || !isRegistrationEnabled}
className="w-full rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2.5 shadow-md shadow-emerald-600/20 transition-all duration-200 cursor-pointer hover:scale-[1.01]" className="w-full rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2.5 shadow-md shadow-emerald-600/20 transition-all duration-200 cursor-pointer hover:scale-[1.01] disabled:opacity-50 disabled:cursor-not-allowed"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t.auth.register.submitting} {t.auth.register.submitting}
</> </>
) : !isRegistrationEnabled ? (
"Đăng Ký Đang Tạm Khóa"
) : ( ) : (
t.auth.register.submit t.auth.register.submit
)} )}
......
import type { Metadata } from "next";
import { SystemConfigsManagementView } from "@/components/system-config/system-configs-management-view";
export const metadata: Metadata = {
title: "Cấu Hình & Cờ Tính Năng | Nhà Phát Triển",
description:
"Quản trị tham số hệ thống, cờ tính năng Feature Flags đa tầng và phân quyền truy cập tập trung.",
};
export default function DeveloperSystemConfigsPage() {
return (
<div className="space-y-6 animate-in fade-in-50 duration-200">
<SystemConfigsManagementView showHeader={false} />
</div>
);
}
import type { Metadata } from "next";
import { SystemConfigsManagementView } from "@/components/system-config/system-configs-management-view";
export const metadata: Metadata = {
title: "Cấu Hình Hệ Thống",
description:
"Quản trị tham số hệ thống, cờ tính năng Feature Flags đa tầng và phân quyền truy cập tập trung.",
};
export default function SystemConfigsPage() {
return (
<div className="min-h-screen bg-background pb-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pt-8 space-y-8 animate-in fade-in-50 duration-200">
<SystemConfigsManagementView showHeader={true} />
</div>
</div>
);
}
...@@ -11,12 +11,14 @@ import { ...@@ -11,12 +11,14 @@ import {
Users, Users,
Shield, Shield,
History, History,
Sliders,
} from "lucide-react"; } from "lucide-react";
import { useLanguage } from "@/providers/language-provider"; import { useLanguage } from "@/providers/language-provider";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useApiKeysList, useWebhookConfigsList } from "@/hooks/use-developer"; import { useApiKeysList, useWebhookConfigsList } from "@/hooks/use-developer";
import { useUsersList } from "@/hooks/use-users"; import { useUsersList } from "@/hooks/use-users";
import { useRolesList } from "@/hooks/use-roles"; import { useRolesList } from "@/hooks/use-roles";
import { useSystemConfigsList } from "@/hooks/use-system-config";
interface DeveloperShellProps { interface DeveloperShellProps {
children: React.ReactNode; children: React.ReactNode;
...@@ -29,19 +31,29 @@ export function DeveloperShell({ children }: DeveloperShellProps) { ...@@ -29,19 +31,29 @@ export function DeveloperShell({ children }: DeveloperShellProps) {
const canManageUsers = hasPermission("users.read") || role === "ADMIN"; const canManageUsers = hasPermission("users.read") || role === "ADMIN";
const canManageRoles = hasPermission("roles.read") || role === "ADMIN"; const canManageRoles = hasPermission("roles.read") || role === "ADMIN";
const canReadAuditLogs = hasPermission("audit_logs.read") || role === "ADMIN"; const canReadAuditLogs = hasPermission("audit_logs.read") || role === "ADMIN";
const canManageConfigs =
hasPermission("system_configs.read") || role === "ADMIN";
// Counts for badges // Counts for badges
const { data: apiKeys = [] } = useApiKeysList(); const { data: apiKeys = [] } = useApiKeysList();
const { data: webhooks = [] } = useWebhookConfigsList(); const { data: webhooks = [] } = useWebhookConfigsList();
const { data: usersData } = useUsersList({ limit: 1 }, { enabled: canManageUsers }); const { data: usersData } = useUsersList({ limit: 1 }, { enabled: canManageUsers });
const { data: rolesData } = useRolesList({ limit: 1 }, { enabled: canManageRoles }); const { data: rolesData } = useRolesList({ limit: 1 }, { enabled: canManageRoles });
const { data: configsData } = useSystemConfigsList(
{ limit: 1 },
{ enabled: canManageConfigs },
);
const totalUsers = usersData?.meta?.total || 0; const totalUsers = usersData?.meta?.total || 0;
const totalRoles = rolesData?.meta?.total ?? rolesData?.items?.length ?? 0; const totalRoles = rolesData?.meta?.total ?? rolesData?.items?.length ?? 0;
const totalConfigs = configsData?.total || 0;
const isUsersTab = pathname.startsWith("/settings/developer/users"); const isUsersTab = pathname.startsWith("/settings/developer/users");
const isRolesTab = const isRolesTab =
pathname.startsWith("/settings/developer/roles") || pathname.startsWith("/settings/developer/roles") ||
pathname.startsWith("/roles"); pathname.startsWith("/roles");
const isSystemConfigsTab =
pathname.startsWith("/settings/developer/system-configs") ||
pathname.startsWith("/system-configs");
const isApiKeysTab = const isApiKeysTab =
pathname === "/settings/developer/api-keys" || pathname === "/settings/developer/api-keys" ||
pathname === "/settings/developer"; pathname === "/settings/developer";
...@@ -171,6 +183,26 @@ export function DeveloperShell({ children }: DeveloperShellProps) { ...@@ -171,6 +183,26 @@ export function DeveloperShell({ children }: DeveloperShellProps) {
<span>{t.nav.auditLogs}</span> <span>{t.nav.auditLogs}</span>
</Link> </Link>
)} )}
{/* 6. Tab Cấu Hình Hệ Thống & Feature Flags */}
{canManageConfigs && (
<Link
href="/settings/developer/system-configs"
className={`flex items-center gap-2 px-4 py-3 text-xs font-bold border-b-2 transition-all cursor-pointer whitespace-nowrap ${
isSystemConfigsTab
? "border-emerald-500 text-emerald-600 dark:text-emerald-400"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<Sliders className="h-4 w-4" />
<span>{t.developer.tabs.systemConfigs}</span>
{totalConfigs > 0 && (
<span className="rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[10px] px-2 py-0.5 border border-emerald-500/20 font-semibold">
{totalConfigs}
</span>
)}
</Link>
)}
</div> </div>
{/* Tab Page Content */} {/* Tab Page Content */}
......
"use client";
import React from "react";
import {
Edit2,
Trash2,
Globe,
Lock,
Calendar,
ToggleLeft,
Sparkles,
ShieldAlert,
Share2,
Sliders,
} from "lucide-react";
import { useLanguage } from "@/providers/language-provider";
import { SystemConfigItem } from "@/types/system-config";
import { CopyButton } from "@/components/common/copy-button";
import { formatDate } from "@/lib/utils";
interface SystemConfigCardProps {
config: SystemConfigItem;
canManage: boolean;
onEdit: (config: SystemConfigItem) => void;
onDelete: (config: SystemConfigItem) => void;
onToggle: (key: string) => Promise<void>;
isToggling: boolean;
}
export function SystemConfigCard({
config,
canManage,
onEdit,
onDelete,
onToggle,
isToggling,
}: SystemConfigCardProps) {
const { t, locale } = useLanguage();
const isBoolean = typeof config.value === "boolean";
const boolVal = Boolean(config.value);
const getCategoryBadge = () => {
switch (config.category) {
case "FEATURE_FLAG":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-[10px] font-bold text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">
<Sparkles className="h-3 w-3" />
{t.systemConfigs.tabs.featureFlag}
</span>
);
case "SECURITY":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-rose-500/10 px-2.5 py-0.5 text-[10px] font-bold text-rose-600 dark:text-rose-400 border border-rose-500/20">
<ShieldAlert className="h-3 w-3" />
{t.systemConfigs.tabs.security}
</span>
);
case "INTEGRATION":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/10 px-2.5 py-0.5 text-[10px] font-bold text-amber-600 dark:text-amber-400 border border-amber-500/20">
<Share2 className="h-3 w-3" />
{t.systemConfigs.tabs.integration}
</span>
);
default:
return (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-2.5 py-0.5 text-[10px] font-bold text-blue-600 dark:text-blue-400 border border-blue-500/20">
<Sliders className="h-3 w-3" />
{t.systemConfigs.tabs.general}
</span>
);
}
};
return (
<div className="group relative flex flex-col justify-between rounded-3xl border border-emerald-500/15 bg-card/70 p-5 shadow-sm backdrop-blur-sm transition-all duration-300 hover:border-emerald-500/30 hover:shadow-md hover:shadow-emerald-950/10 hover:scale-[1.01]">
<div className="space-y-3.5">
{/* Top Badges */}
<div className="flex items-center justify-between gap-2">
{getCategoryBadge()}
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold border ${
config.isPublic
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
: "bg-muted/70 text-muted-foreground border-border/80"
}`}
>
{config.isPublic ? (
<Globe className="h-2.5 w-2.5" />
) : (
<Lock className="h-2.5 w-2.5" />
)}
<span>
{config.isPublic
? t.systemConfigs.publicBadge
: t.systemConfigs.privateBadge}
</span>
</span>
</div>
{/* Key & Copy */}
<div className="flex items-start justify-between gap-2">
<div className="space-y-0.5">
<h3 className="font-mono text-sm font-bold tracking-tight text-foreground break-all">
{config.key}
</h3>
{config.description && (
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
{config.description}
</p>
)}
</div>
<CopyButton text={config.key} />
</div>
{/* Value Section */}
<div className="rounded-2xl border border-border/60 bg-muted/30 p-3">
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground mb-1.5">
{t.systemConfigs.table.value}
</div>
{isBoolean ? (
<div className="flex items-center justify-between">
<span
className={`text-xs font-bold ${
boolVal
? "text-emerald-600 dark:text-emerald-400"
: "text-muted-foreground"
}`}
>
{boolVal
? t.systemConfigs.booleanActive
: t.systemConfigs.booleanInactive}
</span>
<button
type="button"
disabled={!canManage || isToggling}
onClick={() => onToggle(config.key)}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed ${
boolVal ? "bg-emerald-500" : "bg-muted-foreground/30"
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
boolVal ? "translate-x-5" : "translate-x-0"
}`}
/>
</button>
</div>
) : (
<div className="overflow-x-auto max-h-24 font-mono text-xs text-foreground font-medium">
{typeof config.value === "object" ? (
<pre className="text-[11px] leading-tight">
{JSON.stringify(config.value, null, 2)}
</pre>
) : (
<span>{String(config.value)}</span>
)}
</div>
)}
</div>
</div>
{/* Card Footer: Timestamp & Actions */}
<div className="mt-4 pt-3 border-t border-border/60 flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1.5 text-[11px]">
<Calendar className="h-3 w-3 text-emerald-500" />
<span>{formatDate(config.updatedAt, locale)}</span>
</div>
{canManage && (
<div className="flex items-center gap-1">
<button
onClick={() => onEdit(config)}
className="flex h-7 w-7 items-center justify-center rounded-xl text-muted-foreground hover:bg-emerald-500/10 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors cursor-pointer"
title={t.systemConfigs.editBtn}
>
<Edit2 className="h-3.5 w-3.5" />
</button>
<button
onClick={() => onDelete(config)}
className="flex h-7 w-7 items-center justify-center rounded-xl text-muted-foreground hover:bg-rose-500/10 hover:text-rose-600 dark:hover:text-rose-400 transition-colors cursor-pointer"
title={t.systemConfigs.deleteBtn}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
)}
</div>
</div>
);
}
"use client";
import React from "react";
import { AlertTriangle, Loader2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useLanguage } from "@/providers/language-provider";
import { SystemConfigItem } from "@/types/system-config";
interface SystemConfigDeleteDialogProps {
config: SystemConfigItem | null;
isOpen: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
isDeleting: boolean;
}
export function SystemConfigDeleteDialog({
config,
isOpen,
onClose,
onConfirm,
isDeleting,
}: SystemConfigDeleteDialogProps) {
const { t } = useLanguage();
if (!config) return null;
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-[440px] rounded-3xl border border-rose-500/20 bg-card/95 backdrop-blur-xl p-6 shadow-2xl shadow-rose-950/20">
<DialogHeader className="space-y-3">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20">
<AlertTriangle className="h-6 w-6" />
</div>
<DialogTitle className="text-center text-lg font-bold text-foreground">
{t.systemConfigs.deleteDialog.title}
</DialogTitle>
<DialogDescription className="text-center text-xs text-muted-foreground leading-relaxed">
{t.systemConfigs.deleteDialog.description}
</DialogDescription>
</DialogHeader>
{/* Config Summary Card */}
<div className="my-2 rounded-2xl border border-border/70 bg-muted/40 p-3.5 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
{t.systemConfigs.table.key}:
</span>
<code className="text-xs font-bold text-emerald-600 dark:text-emerald-400">
{config.key}
</code>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
{t.systemConfigs.table.category}:
</span>
<span className="text-xs font-medium text-foreground">
{config.category}
</span>
</div>
</div>
<DialogFooter className="flex flex-col sm:flex-row gap-2 mt-4">
<Button
variant="outline"
onClick={onClose}
disabled={isDeleting}
className="w-full sm:w-1/2 rounded-2xl border-border/80 text-xs font-semibold hover:bg-muted/70 cursor-pointer"
>
{t.systemConfigs.deleteDialog.cancelBtn}
</Button>
<Button
variant="destructive"
onClick={onConfirm}
disabled={isDeleting}
className="w-full sm:w-1/2 rounded-2xl bg-rose-600 hover:bg-rose-700 text-white text-xs font-bold shadow-md shadow-rose-600/20 cursor-pointer"
>
{isDeleting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
t.systemConfigs.deleteDialog.confirmBtn
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
This diff is collapsed.
This diff is collapsed.
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { systemConfigService } from "@/services/system-config.service";
import {
CreateSystemConfigDto,
SystemConfigQueryParams,
UpdateSystemConfigDto,
} from "@/types/system-config";
export const SYSTEM_CONFIG_QUERY_KEYS = {
all: ["system-configs"] as const,
public: ["system-configs", "public"] as const,
list: (params?: SystemConfigQueryParams) =>
["system-configs", "list", params] as const,
detail: (key: string) => ["system-configs", "detail", key] as const,
};
/**
* Hook truy xuất cấu hình công khai và kiểm tra Feature Flags trên toàn client
*/
export function usePublicConfigs() {
const query = useQuery({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.public,
queryFn: () => systemConfigService.getPublicConfigs(),
staleTime: 60 * 1000, // 1 phút
refetchOnWindowFocus: false,
});
const isFeatureEnabled = (
featureKey: string,
defaultValue: boolean = false,
): boolean => {
if (!query.data?.map) return defaultValue;
const val = query.data.map[featureKey];
if (typeof val === "boolean") return val;
if (val === "true" || val === 1 || val === "1") return true;
if (val === "false" || val === 0 || val === "0") return false;
return Boolean(val);
};
return {
...query,
isFeatureEnabled,
};
}
/**
* Hook lấy danh sách cấu hình quản trị (Admin)
*/
export function useSystemConfigsList(
params?: SystemConfigQueryParams,
options?: { enabled?: boolean },
) {
return useQuery({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.list(params),
queryFn: () => systemConfigService.getConfigs(params),
enabled: options?.enabled ?? true,
});
}
/**
* Hook lấy chi tiết 1 cấu hình theo key
*/
export function useSystemConfigItem(
key: string,
options?: { enabled?: boolean },
) {
return useQuery({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.detail(key),
queryFn: () => systemConfigService.getConfigByKey(key),
enabled: Boolean(key) && (options?.enabled ?? true),
});
}
/**
* Hook tạo cấu hình mới
*/
export function useCreateSystemConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateSystemConfigDto) =>
systemConfigService.createConfig(dto),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.all,
});
},
});
}
/**
* Hook cập nhật cấu hình
*/
export function useUpdateSystemConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ key, dto }: { key: string; dto: UpdateSystemConfigDto }) =>
systemConfigService.updateConfig(key, dto),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.all,
});
},
});
}
/**
* Hook bật/tắt nhanh boolean Feature Flag
*/
export function useToggleFeatureFlag() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (key: string) => systemConfigService.toggleFeature(key),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.all,
});
},
});
}
/**
* Hook xóa cấu hình
*/
export function useDeleteSystemConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (key: string) => systemConfigService.deleteConfig(key),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.all,
});
},
});
}
...@@ -16,6 +16,7 @@ export const translations = { ...@@ -16,6 +16,7 @@ export const translations = {
users: "Quản Lý Người Dùng", users: "Quản Lý Người Dùng",
roles: "Vai Trò & Phân Quyền", roles: "Vai Trò & Phân Quyền",
auditLogs: "Nhật Ký Hệ Thống", auditLogs: "Nhật Ký Hệ Thống",
systemConfigs: "Cấu Hình Hệ Thống",
statusActive: "Động cơ Crawler Đang Chạy", statusActive: "Động cơ Crawler Đang Chạy",
reload: "Tải lại trang", reload: "Tải lại trang",
copyright: "Data Crawler © 2026 - Code by @hnihTyoB", copyright: "Data Crawler © 2026 - Code by @hnihTyoB",
...@@ -656,6 +657,7 @@ export const translations = { ...@@ -656,6 +657,7 @@ export const translations = {
apiKeys: "Khóa API", apiKeys: "Khóa API",
webhooks: "Webhooks", webhooks: "Webhooks",
docs: "Tích Hợp Nhanh", docs: "Tích Hợp Nhanh",
systemConfigs: "Cấu Hình & Cờ Tính Năng",
}, },
apiKeys: { apiKeys: {
title: "Quản Lý Khóa API", title: "Quản Lý Khóa API",
...@@ -1271,6 +1273,80 @@ export const translations = { ...@@ -1271,6 +1273,80 @@ export const translations = {
}, },
}, },
}, },
systemConfigs: {
title: "Cấu Hình & Cờ Tính Năng",
subtitle: "Quản trị tham số hệ thống, cờ tính năng Feature Flags đa tầng và phân quyền truy cập tập trung",
tabs: {
all: "Tất cả",
general: "Cấu hình chung",
featureFlag: "Cờ tính năng",
integration: "Tích hợp",
security: "Bảo mật",
},
stats: {
total: "Tổng cấu hình",
activeFlags: "Cờ đang kích hoạt",
publicConfigs: "Cấu hình công khai",
},
searchPlaceholder: "Tìm theo khóa cấu hình hoặc nội dung mô tả...",
createBtn: "Thêm cấu hình",
editBtn: "Chỉnh sửa",
deleteBtn: "Xóa cấu hình",
refreshBtn: "Làm mới",
table: {
key: "Khóa cấu hình",
value: "Giá trị",
category: "Phân loại",
description: "Mô tả",
isPublic: "Quyền truy cập",
updatedAt: "Cập nhật lúc",
actions: "Thao tác",
},
booleanActive: "Đang bật (true)",
booleanInactive: "Đang tắt (false)",
booleanOn: "Bật",
booleanOff: "Tắt",
modalBoolActive: "Kích hoạt (ON / True)",
modalBoolInactive: "Tắt tính năng (OFF / False)",
modalBoolHelp: "Cờ tính năng sẽ có giá trị",
publicBadge: "Công khai",
privateBadge: "Nội bộ",
modal: {
createTitle: "Thêm Cấu Hình Mới",
createDesc: "Thiết lập tham số hoặc cờ tính năng mới vào hệ thống mà không cần build lại máy chủ",
editTitle: "Chỉnh Sửa Cấu Hình",
editDesc: "Cập nhật giá trị cấu hình, hệ thống sẽ tự động đồng bộ và xóa bộ nhớ đệm tức thời",
keyLabel: "Khóa định danh",
keyPlaceholder: "Ví dụ: feature.ai.enabled hoặc app.name",
typeLabel: "Kiểu dữ liệu",
typeBoolean: "Boolean (Cờ bật tắt)",
typeString: "Chuỗi văn bản (String)",
typeNumber: "Số nguyên hoặc thập phân (Number)",
typeJson: "Dữ liệu cấu trúc (JSON)",
valueLabel: "Giá trị",
valuePlaceholder: "Nhập giá trị...",
categoryLabel: "Danh mục",
descriptionLabel: "Mô tả ý nghĩa",
descriptionPlaceholder: "Giải thích mục đích của cấu hình này cho Quản trị viên...",
isPublicLabel: "Cho phép đọc công khai",
isPublicDesc: "Cho phép giao diện khách đọc cấu hình này mà không cần đăng nhập",
saveBtn: "Lưu cấu hình",
cancelBtn: "Hủy bỏ",
},
deleteDialog: {
title: "Xác Nhận Xóa Cấu Hình",
description: "Bạn có chắc chắn muốn xóa cấu hình này không? Thao tác này sẽ xóa vĩnh viễn và đồng bộ ngay tới toàn bộ cụm máy chủ.",
confirmBtn: "Xóa vĩnh viễn",
cancelBtn: "Hủy bỏ",
},
toast: {
createSuccess: "Đã tạo cấu hình thành công",
updateSuccess: "Đã cập nhật cấu hình thành công",
toggleSuccess: "Đã thay đổi trạng thái cờ tính năng",
deleteSuccess: "Đã xóa cấu hình thành công",
error: "Đã có lỗi xảy ra, vui lòng thử lại",
},
},
}, },
en: { en: {
// Navigation & Common // Navigation & Common
...@@ -1287,6 +1363,7 @@ export const translations = { ...@@ -1287,6 +1363,7 @@ export const translations = {
users: "User Management", users: "User Management",
roles: "Roles & Permissions", roles: "Roles & Permissions",
auditLogs: "System Audit Logs", auditLogs: "System Audit Logs",
systemConfigs: "System Configurations",
statusActive: "Crawler Engine Active", statusActive: "Crawler Engine Active",
reload: "Reload page", reload: "Reload page",
copyright: "Data Crawler © 2026 - Code by @hnihTyoB", copyright: "Data Crawler © 2026 - Code by @hnihTyoB",
...@@ -1926,7 +2003,8 @@ export const translations = { ...@@ -1926,7 +2003,8 @@ export const translations = {
tabs: { tabs: {
apiKeys: "API Keys", apiKeys: "API Keys",
webhooks: "Webhooks", webhooks: "Webhooks",
docs: "Quick Integration", docs: "Integration Guide",
systemConfigs: "Configurations & Flags",
}, },
apiKeys: { apiKeys: {
title: "API Keys Management", title: "API Keys Management",
...@@ -2542,6 +2620,80 @@ export const translations = { ...@@ -2542,6 +2620,80 @@ export const translations = {
}, },
}, },
}, },
systemConfigs: {
title: "Configurations & Flags",
subtitle: "Manage operational parameters, centralized Feature Flags, and system access policies",
tabs: {
all: "All",
general: "General",
featureFlag: "Feature Flags",
integration: "Integration",
security: "Security",
},
stats: {
total: "Total Configs",
activeFlags: "Active Flags",
publicConfigs: "Public Configs",
},
searchPlaceholder: "Search by key or description...",
createBtn: "New Configuration",
editBtn: "Edit",
deleteBtn: "Delete configuration",
refreshBtn: "Refresh",
table: {
key: "Configuration Key",
value: "Current Value",
category: "Category",
description: "Description",
isPublic: "Access Level",
updatedAt: "Updated At",
actions: "Actions",
},
booleanActive: "Enabled (true)",
booleanInactive: "Disabled (false)",
booleanOn: "On",
booleanOff: "Off",
modalBoolActive: "Enabled (ON / True)",
modalBoolInactive: "Disabled (OFF / False)",
modalBoolHelp: "Feature flag value will be",
publicBadge: "Public",
privateBadge: "Internal",
modal: {
createTitle: "Create Configuration",
createDesc: "Add a new parameter or feature flag to the system without rebuilding the server",
editTitle: "Edit Configuration",
editDesc: "Update configuration value with instant cluster-wide cache invalidation",
keyLabel: "Configuration Key",
keyPlaceholder: "e.g. feature.ai.enabled or app.name",
typeLabel: "Data Type",
typeBoolean: "Boolean (Feature Flag)",
typeString: "String",
typeNumber: "Number",
typeJson: "Structured JSON",
valueLabel: "Value",
valuePlaceholder: "Enter value...",
categoryLabel: "Category",
descriptionLabel: "Description",
descriptionPlaceholder: "Explain the purpose of this config for administrators...",
isPublicLabel: "Allow Public Read",
isPublicDesc: "Allows frontend clients to read this configuration without authentication",
saveBtn: "Save Configuration",
cancelBtn: "Cancel",
},
deleteDialog: {
title: "Confirm Delete Configuration",
description: "Are you sure you want to delete this configuration? This action is permanent and synchronizes immediately across all cluster instances.",
confirmBtn: "Delete Permanently",
cancelBtn: "Cancel",
},
toast: {
createSuccess: "Configuration created successfully",
updateSuccess: "Configuration updated successfully",
toggleSuccess: "Feature flag state toggled successfully",
deleteSuccess: "Configuration deleted successfully",
error: "An error occurred, please try again",
},
},
}, },
}; };
......
...@@ -16,9 +16,11 @@ const ADMIN_ROUTES = [ ...@@ -16,9 +16,11 @@ const ADMIN_ROUTES = [
"/users", "/users",
"/audit-logs", "/audit-logs",
"/roles", "/roles",
"/system-configs",
"/settings/developer/users", "/settings/developer/users",
"/settings/developer/roles", "/settings/developer/roles",
"/settings/developer/audit-logs", "/settings/developer/audit-logs",
"/settings/developer/system-configs",
]; ];
// Open public pages that do NOT require login (e.g. 403 forbidden, 404 not-found) // Open public pages that do NOT require login (e.g. 403 forbidden, 404 not-found)
......
import { apiClient } from "@/lib/api-client";
import {
CreateSystemConfigDto,
PublicConfigsResponse,
SystemConfigItem,
SystemConfigQueryParams,
SystemConfigsListResponse,
UpdateSystemConfigDto,
} from "@/types/system-config";
export const systemConfigService = {
/**
* Lấy danh sách cấu hình công khai và Feature Flags cho client/guest
* GET /api/proxy/system/public
*/
async getPublicConfigs(): Promise<PublicConfigsResponse> {
const response = await apiClient.get<{
success: boolean;
data: PublicConfigsResponse;
}>("/system/public");
return response.data.data;
},
/**
* Lấy danh sách cấu hình hệ thống (Admin)
* GET /api/proxy/system/configs
*/
async getConfigs(
params?: SystemConfigQueryParams,
): Promise<SystemConfigsListResponse> {
const response = await apiClient.get<{
success: boolean;
data: SystemConfigsListResponse;
}>("/system/configs", { params });
return response.data.data;
},
/**
* Lấy chi tiết một cấu hình theo khóa
* GET /api/proxy/system/configs/:key
*/
async getConfigByKey(key: string): Promise<SystemConfigItem> {
const response = await apiClient.get<{
success: boolean;
data: SystemConfigItem;
}>(`/system/configs/${encodeURIComponent(key)}`);
return response.data.data;
},
/**
* Tạo cấu hình mới
* POST /api/proxy/system/configs
*/
async createConfig(dto: CreateSystemConfigDto): Promise<SystemConfigItem> {
const response = await apiClient.post<{
success: boolean;
data: SystemConfigItem;
message?: string;
}>("/system/configs", dto);
return response.data.data;
},
/**
* Cập nhật cấu hình
* PUT /api/proxy/system/configs/:key
*/
async updateConfig(
key: string,
dto: UpdateSystemConfigDto,
): Promise<SystemConfigItem> {
const response = await apiClient.put<{
success: boolean;
data: SystemConfigItem;
message?: string;
}>(`/system/configs/${encodeURIComponent(key)}`, dto);
return response.data.data;
},
/**
* Bật/tắt nhanh boolean Feature Flag
* PATCH /api/proxy/system/features/:key/toggle
*/
async toggleFeature(key: string): Promise<SystemConfigItem> {
const response = await apiClient.patch<{
success: boolean;
data: SystemConfigItem;
message?: string;
}>(`/system/features/${encodeURIComponent(key)}/toggle`);
return response.data.data;
},
/**
* Xóa cấu hình
* DELETE /api/proxy/system/configs/:key
*/
async deleteConfig(key: string): Promise<void> {
await apiClient.delete(`/system/configs/${encodeURIComponent(key)}`);
},
};
export type SystemConfigCategory =
| "GENERAL"
| "FEATURE_FLAG"
| "INTEGRATION"
| "SECURITY";
export interface SystemConfigItem {
id: string;
key: string;
value: unknown;
description: string | null;
category: SystemConfigCategory;
isPublic: boolean;
createdAt: string;
updatedAt: string;
}
export interface PublicConfigsResponse {
configs: Array<{
key: string;
value: unknown;
category: string;
description: string | null;
}>;
map: Record<string, unknown>;
}
export interface CreateSystemConfigDto {
key: string;
value: unknown;
description?: string;
category?: SystemConfigCategory;
isPublic?: boolean;
}
export interface UpdateSystemConfigDto {
value?: unknown;
description?: string;
category?: SystemConfigCategory;
isPublic?: boolean;
}
export interface SystemConfigQueryParams {
category?: SystemConfigCategory;
search?: string;
isPublic?: boolean | "true" | "false";
page?: number;
limit?: number;
}
export interface SystemConfigsListResponse {
items: SystemConfigItem[];
total: number;
page: number;
limit: number;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment