- 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.
233 lines
7.8 KiB
TypeScript
233 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Eye, EyeOff } from "lucide-react";
|
|
import { userAPI } from "@/lib/api/user";
|
|
import { AlertModal, AlertType } from "@/components/common/AlertModal";
|
|
|
|
interface UserPasswordResetModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
userId: string | null;
|
|
userName: string | null;
|
|
onSuccess?: () => void;
|
|
}
|
|
|
|
export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSuccess }: UserPasswordResetModalProps) {
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// 알림 모달 상태
|
|
const [alertState, setAlertState] = useState<{
|
|
isOpen: boolean;
|
|
type: AlertType;
|
|
title: string;
|
|
message: string;
|
|
}>({
|
|
isOpen: false,
|
|
type: "info",
|
|
title: "",
|
|
message: "",
|
|
});
|
|
|
|
// 알림 모달 표시 헬퍼 함수
|
|
const showAlert = (type: AlertType, title: string, message: string) => {
|
|
setAlertState({
|
|
isOpen: true,
|
|
type,
|
|
title,
|
|
message,
|
|
});
|
|
};
|
|
|
|
// 알림 모달 닫기
|
|
const closeAlert = () => {
|
|
setAlertState((prev) => ({ ...prev, isOpen: false }));
|
|
|
|
// 성공 알림이 닫힐 때 메인 모달도 닫기
|
|
if (alertState.type === "success") {
|
|
handleClose();
|
|
onSuccess?.();
|
|
}
|
|
};
|
|
|
|
// 비밀번호 유효성 검사 (영문, 숫자, 특수문자만 허용)
|
|
const validatePassword = (password: string) => {
|
|
const regex = /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]*$/;
|
|
return regex.test(password);
|
|
};
|
|
|
|
// 비밀번호 일치 여부 확인
|
|
const isPasswordMatch = newPassword && confirmPassword && newPassword === confirmPassword;
|
|
const showMismatchError = confirmPassword && newPassword !== confirmPassword;
|
|
|
|
// 초기화 핸들러
|
|
const handleReset = useCallback(async () => {
|
|
if (!userId || !newPassword.trim()) {
|
|
showAlert("warning", "입력 필요", "새 비밀번호를 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (!validatePassword(newPassword)) {
|
|
showAlert("warning", "형식 오류", "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.");
|
|
return;
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
showAlert("warning", "비밀번호 불일치", "비밀번호가 일치하지 않습니다.");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await userAPI.resetPassword({
|
|
userId: userId,
|
|
newPassword: newPassword,
|
|
});
|
|
|
|
if (response.success) {
|
|
showAlert("success", "초기화 완료", "비밀번호가 성공적으로 초기화되었습니다.");
|
|
// 성공 알림은 사용자가 확인 버튼을 눌러서 닫도록 함
|
|
} else {
|
|
showAlert("error", "초기화 실패", response.message || "비밀번호 초기화에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("비밀번호 초기화 오류:", error);
|
|
showAlert("error", "시스템 오류", "비밀번호 초기화 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [userId, newPassword, confirmPassword, onSuccess]);
|
|
|
|
// 모달 닫기 핸들러
|
|
const handleClose = useCallback(() => {
|
|
setNewPassword("");
|
|
setConfirmPassword("");
|
|
setShowPassword(false);
|
|
setShowConfirmPassword(false);
|
|
setIsLoading(false);
|
|
onClose();
|
|
}, [onClose]);
|
|
|
|
// Enter 키 처리
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && !isLoading) {
|
|
handleReset();
|
|
}
|
|
};
|
|
|
|
if (!userId) return null;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>비밀번호 초기화</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4" onKeyDown={handleKeyDown}>
|
|
{/* 대상 사용자 정보 */}
|
|
<div>
|
|
<Label className="text-sm font-medium text-foreground">
|
|
대상 사용자:{" "}
|
|
<span className="font-semibold">
|
|
{userName} ({userId})
|
|
</span>
|
|
</Label>
|
|
</div>
|
|
|
|
{/* 새 비밀번호 입력 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="new-password">새 비밀번호</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="new-password"
|
|
type={showPassword ? "text" : "password"}
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
placeholder="새 비밀번호를 입력하세요"
|
|
disabled={isLoading}
|
|
autoComplete="new-password"
|
|
className="pr-10"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
disabled={isLoading}
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 비밀번호 재확인 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirm-password">비밀번호 재확인</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="confirm-password"
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder="비밀번호를 다시 입력하세요"
|
|
disabled={isLoading}
|
|
autoComplete="new-password"
|
|
className="pr-10"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
disabled={isLoading}
|
|
>
|
|
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 비밀번호 일치 여부 표시 */}
|
|
{showMismatchError && <p className="text-sm text-destructive">비밀번호가 일치하지 않습니다.</p>}
|
|
{isPasswordMatch && <p className="text-sm text-emerald-600">비밀번호가 일치합니다.</p>}
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground">* 비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end space-x-2">
|
|
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleReset}
|
|
disabled={isLoading || !newPassword.trim() || !isPasswordMatch}
|
|
className="min-w-[80px]"
|
|
>
|
|
{isLoading ? "처리중..." : "초기화"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
|
|
{/* 알림 모달 */}
|
|
<AlertModal
|
|
isOpen={alertState.isOpen}
|
|
onClose={closeAlert}
|
|
type={alertState.type}
|
|
title={alertState.title}
|
|
message={alertState.message}
|
|
/>
|
|
</Dialog>
|
|
);
|
|
}
|