- Added validation to ensure that only existing SUPER_ADMIN users can grant or modify SUPER_ADMIN permissions. - Updated the user management page to reflect that both SUPER_ADMIN and COMPANY_ADMIN can access the user permissions, while COMPANY_ADMIN cannot grant SUPER_ADMIN rights. - Enhanced the user authorization modal to prevent COMPANY_ADMIN from changing SUPER_ADMIN permissions, ensuring proper access control. These changes improve the security and integrity of user role management within the application.
243 lines
8.5 KiB
TypeScript
243 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { userAPI } from "@/lib/api/user";
|
|
import { Shield, ShieldCheck, User, Users, Building2, AlertTriangle } from "lucide-react";
|
|
|
|
interface UserAuthEditModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess?: () => void;
|
|
user: any | null;
|
|
currentUser?: any | null;
|
|
}
|
|
|
|
/**
|
|
* 사용자 권한 변경 모달
|
|
*
|
|
* 권한 레벨만 변경 가능 (관리자 전용)
|
|
* 회사관리자는 SUPER_ADMIN 권한 부여 불가
|
|
*/
|
|
export function UserAuthEditModal({ isOpen, onClose, onSuccess, user, currentUser }: UserAuthEditModalProps) {
|
|
const [selectedUserType, setSelectedUserType] = useState<string>("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
|
|
|
// 모달 열릴 때 현재 권한 설정
|
|
useEffect(() => {
|
|
if (isOpen && user) {
|
|
setSelectedUserType(user.userType || "USER");
|
|
setShowConfirmation(false);
|
|
}
|
|
}, [isOpen, user]);
|
|
|
|
// 권한 정보
|
|
const userTypeOptions = [
|
|
{
|
|
value: "SUPER_ADMIN",
|
|
label: "최고 관리자",
|
|
description: "모든 회사 관리, DDL 실행, 회사 생성/삭제 가능",
|
|
icon: <ShieldCheck className="h-4 w-4" />,
|
|
color: "text-purple-600",
|
|
},
|
|
{
|
|
value: "COMPANY_ADMIN",
|
|
label: "회사 관리자",
|
|
description: "자기 회사 데이터 및 사용자 관리 가능",
|
|
icon: <Building2 className="h-4 w-4" />,
|
|
color: "text-primary",
|
|
},
|
|
{
|
|
value: "USER",
|
|
label: "일반 사용자",
|
|
description: "자기 회사 데이터 조회/수정만 가능",
|
|
icon: <User className="h-4 w-4" />,
|
|
color: "text-muted-foreground",
|
|
},
|
|
{
|
|
value: "GUEST",
|
|
label: "게스트",
|
|
description: "제한된 조회 권한",
|
|
icon: <Users className="h-4 w-4" />,
|
|
color: "text-emerald-600",
|
|
},
|
|
{
|
|
value: "PARTNER",
|
|
label: "협력업체",
|
|
description: "협력업체 전용 권한",
|
|
icon: <Shield className="h-4 w-4" />,
|
|
color: "text-amber-600",
|
|
},
|
|
];
|
|
|
|
// 현재 사용자가 SUPER_ADMIN인지 확인
|
|
const isCurrentUserSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
|
|
|
|
// COMPANY_ADMIN은 SUPER_ADMIN 옵션 제외
|
|
const availableOptions = isCurrentUserSuperAdmin
|
|
? userTypeOptions
|
|
: userTypeOptions.filter((opt) => opt.value !== "SUPER_ADMIN");
|
|
|
|
// 대상 사용자가 SUPER_ADMIN이면 COMPANY_ADMIN은 변경 불가
|
|
const isTargetSuperAdmin = user?.userType === "SUPER_ADMIN";
|
|
const canEdit = isCurrentUserSuperAdmin || !isTargetSuperAdmin;
|
|
|
|
const selectedOption = availableOptions.find((opt) => opt.value === selectedUserType);
|
|
|
|
// 권한 변경 여부 확인
|
|
const isUserTypeChanged = user && selectedUserType !== user.userType;
|
|
|
|
// 권한 변경 처리
|
|
const handleSave = async () => {
|
|
if (!user || !isUserTypeChanged) {
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
// SUPER_ADMIN 변경 시 확인
|
|
if (selectedUserType === "SUPER_ADMIN" && !showConfirmation) {
|
|
setShowConfirmation(true);
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await userAPI.update({
|
|
userId: user.userId,
|
|
userName: user.userName,
|
|
companyCode: user.companyCode,
|
|
deptCode: user.deptCode,
|
|
userType: selectedUserType,
|
|
});
|
|
|
|
if (response.success) {
|
|
onSuccess?.();
|
|
} else {
|
|
alert(response.message || "권한 변경에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("권한 변경 오류:", error);
|
|
alert("권한 변경 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">사용자 권한 변경</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 사용자 정보 */}
|
|
<div className="bg-muted/50 rounded-lg border p-4">
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">사용자 ID</span>
|
|
<span className="font-mono font-medium">{user.userId}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">사용자명</span>
|
|
<span className="font-medium">{user.userName}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">회사</span>
|
|
<span className="font-medium">{user.companyName || user.companyCode}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">현재 권한</span>
|
|
<span className="font-medium">
|
|
{userTypeOptions.find((opt) => opt.value === user.userType)?.label || user.userType}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 권한 선택 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userType" className="text-sm font-medium">
|
|
새로운 권한 <span className="text-destructive">*</span>
|
|
</Label>
|
|
{!canEdit ? (
|
|
<div className="rounded-lg border border-orange-300 bg-amber-50 p-3">
|
|
<p className="text-xs text-orange-800">
|
|
최고 관리자의 권한은 다른 최고 관리자만 변경할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Select value={selectedUserType} onValueChange={setSelectedUserType}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="권한 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
<div className="flex items-center gap-2">
|
|
<span className={option.color}>{option.icon}</span>
|
|
<span>{option.label}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{selectedOption && <p className="text-muted-foreground text-xs">{selectedOption.description}</p>}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* SUPER_ADMIN 경고 */}
|
|
{showConfirmation && selectedUserType === "SUPER_ADMIN" && (
|
|
<div className="rounded-lg border border-orange-300 bg-amber-50 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600" />
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-semibold text-orange-900">최고 관리자 권한 부여 경고</p>
|
|
<p className="text-xs text-orange-800">
|
|
최고 관리자 권한은 모든 회사 데이터에 접근하고, DDL을 실행하며, 회사를 생성/삭제할 수 있는 최상위
|
|
권한입니다. 신중하게 부여해주세요.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={isLoading || !isUserTypeChanged || !canEdit}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{isLoading ? "처리중..." : showConfirmation ? "확인 및 저장" : "저장"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|