forked from baron/baron-sso
feat: optimize tenant admin view and enhance user list with dynamic columns and metadata search
This commit is contained in:
36
adminfront/src/components/auth/RoleGuard.tsx
Normal file
36
adminfront/src/components/auth/RoleGuard.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchMe } from "../../lib/adminApi";
|
||||||
|
|
||||||
|
interface RoleGuardProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
roles: string[];
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoleGuard conditionally renders children based on the current user's role.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <RoleGuard roles={['super_admin']}>
|
||||||
|
* <button>System Only Action</button>
|
||||||
|
* </RoleGuard>
|
||||||
|
*/
|
||||||
|
export function RoleGuard({ children, roles, fallback = null }: RoleGuardProps) {
|
||||||
|
const { data: profile, isLoading } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return null;
|
||||||
|
|
||||||
|
const userRole = profile?.role || "user";
|
||||||
|
const hasAccess = roles.includes(userRole);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
return <>{fallback}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -22,18 +22,14 @@ import { t } from "../../lib/i18n";
|
|||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import RoleSwitcher from "./RoleSwitcher";
|
import RoleSwitcher from "./RoleSwitcher";
|
||||||
|
|
||||||
const navItems = [
|
const staticNavItems = [
|
||||||
{ label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard },
|
{ label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard },
|
||||||
{
|
|
||||||
label: "ui.admin.nav.tenants",
|
|
||||||
to: "/tenants",
|
|
||||||
icon: Building2,
|
|
||||||
},
|
|
||||||
{ label: "ui.admin.nav.users", to: "/users", icon: Users },
|
{ label: "ui.admin.nav.users", to: "/users", icon: Users },
|
||||||
{ label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key },
|
{ label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key },
|
||||||
{ label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs },
|
{ label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs },
|
||||||
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
|
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
|
||||||
];
|
];
|
||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -49,6 +45,32 @@ function AppLayout() {
|
|||||||
enabled: auth.isAuthenticated && !auth.isLoading,
|
enabled: auth.isAuthenticated && !auth.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navItems = React.useMemo(() => {
|
||||||
|
const items = [...staticNavItems];
|
||||||
|
const isSuperAdmin = profile?.role === "super_admin";
|
||||||
|
const isTenantAdmin = profile?.role === "tenant_admin";
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
// Insert Tenants at index 1 for Super Admin
|
||||||
|
items.splice(1, 0, {
|
||||||
|
label: "ui.admin.nav.tenants",
|
||||||
|
to: "/tenants",
|
||||||
|
icon: Building2,
|
||||||
|
});
|
||||||
|
} else if (isTenantAdmin && profile?.tenantId) {
|
||||||
|
// Insert My Tenant link for Tenant Admin
|
||||||
|
items.splice(1, 0, {
|
||||||
|
label: "ui.admin.nav.my_tenant",
|
||||||
|
to: `/tenants/${profile.tenantId}`,
|
||||||
|
icon: Building2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant Admin should not see global API keys or global audit logs (unless allowed)
|
||||||
|
// For now, let's keep them but they might return 403
|
||||||
|
return items;
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (
|
if (
|
||||||
window.confirm(t("msg.admin.logout_confirm", "로그아웃 하시겠습니까?"))
|
window.confirm(t("msg.admin.logout_confirm", "로그아웃 하시겠습니까?"))
|
||||||
|
|||||||
@@ -23,17 +23,32 @@ import {
|
|||||||
import {
|
import {
|
||||||
type TenantSummary,
|
type TenantSummary,
|
||||||
deleteTenant,
|
deleteTenant,
|
||||||
|
fetchMe,
|
||||||
fetchTenants,
|
fetchTenants,
|
||||||
} from "../../../lib/adminApi";
|
} from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
|
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
||||||
|
|
||||||
function TenantListPage() {
|
function TenantListPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect tenant_admin to their own tenant
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (profile?.role === "tenant_admin" && profile?.tenantId) {
|
||||||
|
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
||||||
|
}
|
||||||
|
}, [profile, navigate]);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["tenants", { limit: 1000, offset: 0 }],
|
queryKey: ["tenants", { limit: 1000, offset: 0 }],
|
||||||
queryFn: () => fetchTenants(1000, 0),
|
queryFn: () => fetchTenants(1000, 0),
|
||||||
|
enabled: profile?.role === "super_admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (tenantId: string) => deleteTenant(tenantId),
|
mutationFn: (tenantId: string) => deleteTenant(tenantId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -41,6 +56,20 @@ function TenantListPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (profile && profile.role !== "super_admin" && profile.role !== "tenant_admin") {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||||
|
<h3 className="text-xl font-bold">{t("msg.admin.common.forbidden", "접근 권한이 없습니다.")}</h3>
|
||||||
|
<Button onClick={() => navigate("/")}>{t("ui.common.go_home", "홈으로 이동")}</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// While redirecting
|
||||||
|
if (profile?.role === "tenant_admin") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
|
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
|
||||||
?.data?.error;
|
?.data?.error;
|
||||||
const fallbackError =
|
const fallbackError =
|
||||||
@@ -95,12 +124,14 @@ function TenantListPage() {
|
|||||||
<RefreshCw size={16} />
|
<RefreshCw size={16} />
|
||||||
{t("ui.common.refresh", "새로고침")}
|
{t("ui.common.refresh", "새로고침")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<RoleGuard roles={["super_admin"]}>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/tenants/new">
|
<Link to="/tenants/new">
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
{t("ui.admin.tenants.add", "테넌트 추가")}
|
{t("ui.admin.tenants.add", "테넌트 추가")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</RoleGuard>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -30,21 +30,68 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
import { deleteUser, fetchUsers } from "../../lib/adminApi";
|
import {
|
||||||
|
deleteUser,
|
||||||
|
fetchMe,
|
||||||
|
fetchTenant,
|
||||||
|
fetchTenants,
|
||||||
|
fetchUsers,
|
||||||
|
} from "../../lib/adminApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
|
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
|
||||||
|
|
||||||
|
type UserSchemaField = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
function UserListPage() {
|
function UserListPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [page, setPage] = React.useState(1);
|
const [page, setPage] = React.useState(1);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
const [searchDraft, setSearchDraft] = React.useState("");
|
const [searchDraft, setSearchDraft] = React.useState("");
|
||||||
|
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||||
const limit = 50;
|
const limit = 50;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: tenantsData } = useQuery({
|
||||||
|
queryKey: ["tenants", { limit: 100 }],
|
||||||
|
queryFn: () => fetchTenants(100, 0),
|
||||||
|
});
|
||||||
|
const tenants = tenantsData?.items ?? [];
|
||||||
|
|
||||||
|
// Lock company for tenant_admin
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (profile?.role === "tenant_admin" && profile.companyCode) {
|
||||||
|
setSelectedCompany(profile.companyCode);
|
||||||
|
}
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
|
const selectedTenantId = React.useMemo(() => {
|
||||||
|
return tenants.find((t) => t.slug === selectedCompany)?.id ?? "";
|
||||||
|
}, [tenants, selectedCompany]);
|
||||||
|
|
||||||
|
const { data: tenantDetail } = useQuery({
|
||||||
|
queryKey: ["tenant", selectedTenantId],
|
||||||
|
queryFn: () => fetchTenant(selectedTenantId),
|
||||||
|
enabled: selectedTenantId.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchema: UserSchemaField[] = Array.isArray(
|
||||||
|
tenantDetail?.config?.userSchema,
|
||||||
|
)
|
||||||
|
? (tenantDetail?.config?.userSchema as UserSchemaField[])
|
||||||
|
: [];
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["users", { limit, offset, search }],
|
queryKey: ["users", { limit, offset, search, companyCode: selectedCompany }],
|
||||||
queryFn: () => fetchUsers(limit, offset, search),
|
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,12 +127,6 @@ function UserListPage() {
|
|||||||
const total = query.data?.total ?? 0;
|
const total = query.data?.total ?? 0;
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (items.length > 0) {
|
|
||||||
console.log("User items:", items);
|
|
||||||
}
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const handleDelete = (userId: string, userName: string) => {
|
const handleDelete = (userId: string, userName: string) => {
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
@@ -118,7 +159,7 @@ function UserListPage() {
|
|||||||
<p className="text-sm text-[var(--color-muted)]">
|
<p className="text-sm text-[var(--color-muted)]">
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.users.list.subtitle",
|
"msg.admin.users.list.subtitle",
|
||||||
"시스템 사용자를 조회하고 관리합니다. (Local DB)",
|
"시스템 사용자를 조회하고 관리합니다.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,8 +198,8 @@ function UserListPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-6 flex flex-wrap items-center gap-4">
|
||||||
<div className="relative flex-1 max-w-sm">
|
<div className="relative flex-1 min-w-[240px] max-w-sm">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
@@ -171,6 +212,29 @@ function UserListPage() {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground whitespace-nowrap">
|
||||||
|
{t("ui.admin.users.list.filter.tenant", "테넌트 필터:")}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
className="flex h-9 w-[200px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||||
|
value={selectedCompany}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedCompany(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
disabled={profile?.role === "tenant_admin"}
|
||||||
|
>
|
||||||
|
<option value="">{t("ui.common.all", "전체")}</option>
|
||||||
|
{tenants.map((t) => (
|
||||||
|
<option key={t.id} value={t.slug}>
|
||||||
|
{t.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button variant="secondary" onClick={handleSearch}>
|
<Button variant="secondary" onClick={handleSearch}>
|
||||||
{t("ui.common.search", "검색")}
|
{t("ui.common.search", "검색")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -182,11 +246,11 @@ function UserListPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>
|
<TableHead className="min-w-[200px]">
|
||||||
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
{t("ui.admin.users.list.table.name_email", "NAME / EMAIL")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -201,12 +265,12 @@ function UserListPage() {
|
|||||||
"TENANT / DEPT",
|
"TENANT / DEPT",
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
{/* Dynamic Columns from Schema */}
|
||||||
{t(
|
{userSchema.map((field) => (
|
||||||
"ui.admin.users.list.table.position_job",
|
<TableHead key={field.key} className="uppercase">
|
||||||
"POSITION / JOB",
|
{field.label}
|
||||||
)}
|
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
))}
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.users.list.table.created", "CREATED")}
|
{t("ui.admin.users.list.table.created", "CREATED")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -218,14 +282,20 @@ function UserListPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{query.isLoading && (
|
{query.isLoading && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
<TableCell
|
||||||
|
colSpan={6 + userSchema.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
{t("msg.common.loading", "로딩 중...")}
|
{t("msg.common.loading", "로딩 중...")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{!query.isLoading && items.length === 0 && (
|
{!query.isLoading && items.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
<TableCell
|
||||||
|
colSpan={6 + userSchema.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
{t("msg.admin.users.list.empty", "검색 결과가 없습니다.")}
|
{t("msg.admin.users.list.empty", "검색 결과가 없습니다.")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -264,32 +334,17 @@ function UserListPage() {
|
|||||||
<span className="font-medium text-blue-600">
|
<span className="font-medium text-blue-600">
|
||||||
{user.tenant?.name || user.companyCode || "-"}
|
{user.tenant?.name || user.companyCode || "-"}
|
||||||
</span>
|
</span>
|
||||||
{user.tenant && (
|
|
||||||
<span className="text-[10px] text-muted-foreground uppercase">
|
|
||||||
{t(
|
|
||||||
"ui.admin.users.list.tenant_slug",
|
|
||||||
"Slug: {{slug}}",
|
|
||||||
{
|
|
||||||
slug: user.tenant.slug,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{user.department || "-"}
|
{user.department || "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
{/* Dynamic Metadata Cells */}
|
||||||
<div className="flex flex-col text-sm">
|
{userSchema.map((field) => (
|
||||||
<span className="font-medium">
|
<TableCell key={field.key} className="text-sm">
|
||||||
{user.position || "-"}
|
{String(user.metadata?.[field.key] ?? "-")}
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{user.jobTitle || "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
))}
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{new Date(user.createdAt).toLocaleDateString()}
|
{new Date(user.createdAt).toLocaleDateString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -299,11 +354,6 @@ function UserListPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => navigate(`/users/${user.id}`)}
|
onClick={() => navigate(`/users/${user.id}`)}
|
||||||
aria-label={t(
|
|
||||||
"ui.admin.users.list.edit_aria",
|
|
||||||
"사용자 수정: {{name}}",
|
|
||||||
{ name: user.name },
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Pencil size={16} />
|
<Pencil size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -313,11 +363,6 @@ function UserListPage() {
|
|||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
onClick={() => handleDelete(user.id, user.name)}
|
onClick={() => handleDelete(user.id, user.name)}
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
aria-label={t(
|
|
||||||
"ui.admin.users.list.delete_aria",
|
|
||||||
"사용자 삭제: {{name}}",
|
|
||||||
{ name: user.name },
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1392,6 +1392,7 @@ api_keys = "API Keys"
|
|||||||
audit_logs = "Audit Logs"
|
audit_logs = "Audit Logs"
|
||||||
auth_guard = "Auth Guard"
|
auth_guard = "Auth Guard"
|
||||||
logout = "Logout"
|
logout = "Logout"
|
||||||
|
my_tenant = "My Tenant Settings"
|
||||||
overview = "Overview"
|
overview = "Overview"
|
||||||
relying_parties = "Apps (RP)"
|
relying_parties = "Apps (RP)"
|
||||||
user_groups = "Organization"
|
user_groups = "Organization"
|
||||||
|
|||||||
@@ -1484,6 +1484,7 @@ api_keys = "API 키"
|
|||||||
audit_logs = "감사 로그"
|
audit_logs = "감사 로그"
|
||||||
auth_guard = "인증 가드"
|
auth_guard = "인증 가드"
|
||||||
logout = "로그아웃"
|
logout = "로그아웃"
|
||||||
|
my_tenant = "내 테넌트 설정"
|
||||||
overview = "개요"
|
overview = "개요"
|
||||||
relying_parties = "애플리케이션(RP)"
|
relying_parties = "애플리케이션(RP)"
|
||||||
user_groups = "조직 관리"
|
user_groups = "조직 관리"
|
||||||
|
|||||||
@@ -689,6 +689,7 @@ api_keys = ""
|
|||||||
audit_logs = ""
|
audit_logs = ""
|
||||||
auth_guard = ""
|
auth_guard = ""
|
||||||
logout = ""
|
logout = ""
|
||||||
|
my_tenant = ""
|
||||||
overview = ""
|
overview = ""
|
||||||
relying_parties = ""
|
relying_parties = ""
|
||||||
user_groups = ""
|
user_groups = ""
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ func (r *userRepository) List(ctx context.Context, offset, limit int, search str
|
|||||||
|
|
||||||
if search != "" {
|
if search != "" {
|
||||||
searchTerm := "%" + search + "%"
|
searchTerm := "%" + search + "%"
|
||||||
db = db.Where("(email LIKE ? OR name LIKE ? OR company_code LIKE ?)", searchTerm, searchTerm, searchTerm)
|
// Search in basic fields and metadata (PostgreSQL JSONB)
|
||||||
|
db = db.Where("(email LIKE ? OR name LIKE ? OR company_code LIKE ? OR metadata::text LIKE ?)",
|
||||||
|
searchTerm, searchTerm, searchTerm, searchTerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Count(&total).Error; err != nil {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user