1
0
forked from baron/baron-sso

권한부여 및 정합성 검사 추가

This commit is contained in:
2026-05-14 08:45:48 +09:00
parent f6f8e88342
commit 9ca73e8774
36 changed files with 1772 additions and 105 deletions

View File

@@ -48,6 +48,7 @@ import {
fetchTenant,
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles";
import {
type OrgChartTenantSelection,
buildAuthenticatedOrgChartTenantPickerUrl,
@@ -642,7 +643,7 @@ function UserCreatePage() {
id="role"
className="flex h-9 w-full 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:cursor-not-allowed disabled:opacity-50"
{...register("role")}
disabled={profile?.role !== "super_admin"}
disabled={!isSuperAdminRole(profile?.role)}
>
<option value="super_admin">
{t("ui.admin.role.super_admin", "시스템 관리자")}

View File

@@ -67,6 +67,7 @@ import {
} from "../../lib/adminApi";
import type { PasswordPolicyResponse } from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { isSuperAdminRole, normalizeAdminRole } from "../../lib/roles";
import { generateSecurePassword } from "../../lib/utils";
import {
type OrgChartTenantSelection,
@@ -387,8 +388,9 @@ function UserDetailPage() {
},
});
const profileRole = normalizeAdminRole(profile?.role);
const isAdmin =
profile?.role === "super_admin" || profile?.role === "tenant_admin";
profileRole === "super_admin" || profileRole === "tenant_admin";
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
const watchedStatus = watch("status");
@@ -1070,7 +1072,7 @@ function UserDetailPage() {
className="flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50"
{...register("role")}
disabled={
profile?.role !== "super_admin" ||
!isSuperAdminRole(profile?.role) ||
profile?.id === user?.id
}
>

View File

@@ -70,7 +70,13 @@ import {
updateUser,
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles";
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
import {
type UserStatusValue,
userStatusLabel,
userStatusValues,
} from "./userStatus";
type UserSchemaField = {
key: string;
@@ -80,6 +86,29 @@ type UserSchemaField = {
type UserSortKey = string;
const bulkPermissionOptions = [
{
value: "super_admin",
labelKey: "ui.admin.role.super_admin",
fallback: "시스템 관리자",
},
{
value: "tenant_admin",
labelKey: "ui.admin.role.tenant_admin",
fallback: "테넌트 관리자",
},
{
value: "rp_admin",
labelKey: "ui.admin.role.rp_admin",
fallback: "서비스 관리자",
},
{
value: "user",
labelKey: "ui.admin.role.user",
fallback: "일반 사용자",
},
] as const;
function UserListPage() {
const navigate = useNavigate();
const [page, setPage] = React.useState(1);
@@ -90,6 +119,11 @@ function UserListPage() {
Record<string, boolean>
>({});
const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>([]);
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState<
UserStatusValue | ""
>("");
const [selectedBulkPermission, setSelectedBulkPermission] =
React.useState("");
const [sortConfig, setSortConfig] =
React.useState<SortConfig<UserSortKey> | null>(null);
@@ -270,7 +304,7 @@ function UserListPage() {
const total = query.data?.total ?? 0;
const totalPages = Math.ceil(total / limit);
const canPromoteSuperAdmin = profile?.role === "super_admin";
const canPromoteSuperAdmin = isSuperAdminRole(profile?.role);
const toggleSelectAll = () => {
if (selectedUserIds.length === items.length) {
@@ -306,6 +340,8 @@ function UserListPage() {
onSuccess: () => {
query.refetch();
setSelectedUserIds([]);
setSelectedBulkStatus("");
setSelectedBulkPermission("");
toast.success(
t(
"msg.admin.users.bulk.update_success",
@@ -315,22 +351,20 @@ function UserListPage() {
},
});
const handleBulkStatusChange = (status: string) => {
if (selectedUserIds.length === 0) return;
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
};
const handlePromoteSuperAdmin = () => {
if (selectedUserIds.length === 0) return;
const handleApplyBulkStatus = () => {
if (selectedUserIds.length === 0 || !selectedBulkStatus) return;
bulkUpdateMutation.mutate({
userIds: selectedUserIds,
role: "super_admin",
status: selectedBulkStatus,
});
};
const handleBulkRoleChange = (role: string) => {
if (selectedUserIds.length === 0) return;
bulkUpdateMutation.mutate({ userIds: selectedUserIds, role });
const handleApplyBulkPermission = () => {
if (selectedUserIds.length === 0 || !selectedBulkPermission) return;
bulkUpdateMutation.mutate({
userIds: selectedUserIds,
role: selectedBulkPermission,
});
};
const handleBulkDelete = () => {
@@ -746,7 +780,7 @@ function UserListPage() {
}
disabled={
bulkUpdateMutation.isPending ||
profile?.role !== "super_admin" ||
!isSuperAdminRole(profile?.role) ||
user.id === profile?.id
}
>
@@ -754,7 +788,7 @@ function UserListPage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
{profile?.role === "super_admin" && (
{isSuperAdminRole(profile?.role) && (
<SelectItem value="super_admin">
{t(
"ui.admin.role.super_admin",
@@ -819,63 +853,80 @@ function UserListPage() {
})}
</span>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8"
onClick={() => handleBulkStatusChange("active")}
data-testid="bulk-active-btn"
<Select
value={selectedBulkStatus}
onValueChange={(value) =>
setSelectedBulkStatus(value as UserStatusValue)
}
>
{t("ui.common.status.active", "활성화")}
</Button>
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8"
onClick={() => handleBulkStatusChange("inactive")}
data-testid="bulk-inactive-btn"
>
{t("ui.common.status.inactive", "비활성화")}
</Button>
{canPromoteSuperAdmin && (
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8 gap-1.5"
onClick={handlePromoteSuperAdmin}
data-testid="bulk-promote-super-admin-btn"
<SelectTrigger
className="h-8 w-[150px] bg-transparent border-background/20 text-background text-xs"
data-testid="bulk-status-select"
>
<ShieldCheck size={14} />
{t("ui.admin.users.bulk.promote_admin", "Admin으로 만들기")}
</Button>
)}
<SelectValue
placeholder={t(
"ui.admin.users.bulk.status_placeholder",
"상태 선택",
)}
/>
</SelectTrigger>
<SelectContent>
{userStatusValues.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8"
onClick={handleApplyBulkStatus}
disabled={!selectedBulkStatus || bulkUpdateMutation.isPending}
data-testid="bulk-apply-status-btn"
>
{t("ui.common.apply", "적용")}
</Button>
<div className="w-px h-4 bg-background/20 mx-1" />
{profile?.role === "super_admin" && (
{canPromoteSuperAdmin && (
<>
<Select onValueChange={handleBulkRoleChange}>
<SelectTrigger className="h-8 w-[140px] bg-transparent border-background/20 text-background text-xs">
<Select
value={selectedBulkPermission}
onValueChange={setSelectedBulkPermission}
>
<SelectTrigger
className="h-8 w-[150px] bg-transparent border-background/20 text-background text-xs"
data-testid="bulk-permission-select"
>
<SelectValue
placeholder={t(
"ui.admin.users.list.table.role",
"ROLE",
"ui.admin.users.bulk.permission_placeholder",
"권한 선택",
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="super_admin">
{t("ui.admin.role.super_admin", "시스템 관리자")}
</SelectItem>
<SelectItem value="tenant_admin">
{t("ui.admin.role.tenant_admin", "테넌트 관리자")}
</SelectItem>
<SelectItem value="rp_admin">
{t("ui.admin.role.rp_admin", "서비스 관리자")}
</SelectItem>
<SelectItem value="user">
{t("ui.admin.role.user", "일반 사용자")}
</SelectItem>
{bulkPermissionOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.labelKey, option.fallback)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
className="text-background hover:bg-background/10 h-8 gap-1.5"
onClick={handleApplyBulkPermission}
disabled={
!selectedBulkPermission || bulkUpdateMutation.isPending
}
data-testid="bulk-apply-permission-btn"
>
<ShieldCheck size={14} />
{t("ui.common.apply", "적용")}
</Button>
<div className="w-px h-4 bg-background/20 mx-1" />
</>
)}