fix: 确保Admin角色的唯一性

主要修复内容:
编辑用户页面角色限制:

在编辑用户页面,角色选择的下拉菜单中移除了 Admin 选项,现在只能选择 Manager 或 User。

为了保护唯一的 Admin 账户,当编辑 Admin 用户时,其角色字段将被禁用,防止被误修改。

后端逻辑强化:

在后端服务中增加了额外的安全校验,严格禁止通过API接口将任何用户(包括 Admin 自己)的角色修改为 Admin,也禁止将 Admin 降级为其他角色,确保了 Admin 角色的唯一性和稳定性。
This commit is contained in:
gujiangjiang committed 2025-10-08 03:49:59 +08:00
1 parent 2f126b9182
commit cdd577a871
2 files changed
+81 -36

No files matched your search

+52 -30
View File
@@ -1,6 +1,5 @@
import { compare, hash } from "bcryptjs";
import * as repositories from "../repositories";
import { error } from "console";
export async function getAllUsersService(userRole: string) {
try {
@@ -74,7 +73,11 @@ export async function createUserService(
// 检查角色是否有效,只能创建 manager 或 user
const validRoles = ["manager", "user"];
if (!validRoles.includes(userData.role)) {
return { success: false, message: "只能创建 manager 或 user 角色", status: 400 };
return {
success: false,
message: "只能创建 manager 或 user 角色",
status: 400,
};
}
// 哈希密码
@@ -114,7 +117,6 @@ export async function updateUserService(
// 检查用户是否存在
const user = await repositories.getFullUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
}
@@ -128,27 +130,49 @@ export async function updateUserService(
return { success: false, message: "无权修改用户角色", status: 403 };
}
// admin 角色不能被修改
if (user.role === 'admin' && updateData.role && updateData.role !== 'admin') {
return { success: false, message: "不能修改 admin 用户的角色", status: 403 };
// 关于角色的严格校验
if (updateData.role) {
// 1. 禁止将任何用户的角色设置为 'admin'
if (updateData.role === "admin") {
return {
success: false,
message: "不允许将用户角色设置为 Admin",
status: 403,
};
}
// 2. 禁止修改 'admin' 用户自身的角色
if (user.role === "admin") {
return {
success: false,
message: "Admin 用户的角色不能被修改",
status: 403,
};
}
// 3. 校验角色是否合法
const validRoles = ["manager", "user"];
if (!validRoles.includes(updateData.role)) {
return { success: false, message: "无效的用户角色", status: 400 };
}
}
// 准备更新数据
const updates: any = {};
const updates: {
username?: string;
email?: string | null;
role?: string;
password?: string;
} = {};
if (
updateData.username !== undefined &&
updateData.username !== user.username
) {
// 检查新用户名是否已存在
const existingUser = await repositories.checkUserExists(
updateData.username
);
if (existingUser) {
return { success: false, message: "用户名已存在", status: 400 };
}
updates.username = updateData.username;
}
@@ -156,25 +180,22 @@ export async function updateUserService(
updates.email = updateData.email;
}
if (updateData.role !== undefined) {
if (updateData.role !== undefined && updateData.role !== user.role) {
updates.role = updateData.role;
}
// 如果提供了新密码,则更新密码
if (updateData.password) {
updates.password = await hash(updateData.password, 10);
}
// 执行更新
try {
const updatedUser = await repositories.updateUser(id, updates);
return { success: true, user: updatedUser, status: 200 };
} catch (err) {
if (err instanceof Error && err.message === "没有提供要更新的字段") {
return { success: false, message: "没有提供要更新的字段", status: 400 };
}
throw err;
// 如果没有要更新的字段,则提前返回
if (Object.keys(updates).length === 0) {
return { success: true, user: user, message: "没有需要更新的字段", status: 200 };
}
// 执行更新
const updatedUser = await repositories.updateUser(id, updates);
return { success: true, user: updatedUser, status: 200 };
} catch (error) {
console.error("更新用户错误:", error);
return { success: false, message: "更新用户失败", status: 500 };
@@ -199,16 +220,18 @@ export async function deleteUserService(
// 检查用户是否存在
const user = await repositories.getUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
}
// admin 用户不能被删除
if (user.role === 'admin') {
return { success: false, message: "admin 用户不允许被删除", status: 403 };
}
// admin 用户不能被删除
if (user.role === "admin") {
return {
success: false,
message: "admin 用户不允许被删除",
status: 403,
};
}
// 执行删除
await repositories.deleteUser(id);
@@ -234,19 +257,18 @@ export async function changePasswordService(
// 获取用户
const user = await repositories.getFullUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
}
// 非管理员需要验证当前密码
// 如果提供了 currentPassword,则必须验证
if (passwordData.currentPassword) {
const isPasswordValid = await compare(
passwordData.currentPassword,
user.password
);
if (!isPasswordValid) {
return { success: false, message: "当前密码无效", status: 400 };
return { success: false, message: "当前密码无效", status: 400 };
}
}
+29 -6
View File
@@ -14,10 +14,12 @@ import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { getUser, updateUser } from "../../api/users";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { User } from "@/types";
const EditUser = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
username: "",
@@ -32,11 +34,14 @@ const EditUser = () => {
setLoading(true);
const response = await getUser(Number(id));
if (response.success && response.user) {
setUser(response.user); // 保存原始用户信息
setFormData({
username: response.user.username,
email: response.user.email || "",
role: response.user.role || "user",
});
} else {
toast.error(response.message || "获取用户信息失败");
}
setLoading(false);
};
@@ -76,6 +81,10 @@ const EditUser = () => {
}
};
if (loading) {
return <Text>{t("common.loading")}</Text>;
}
return (
<Box className="sm:px-6 lg:px-[8%]">
<Flex justify="between" align="center">
@@ -83,7 +92,9 @@ const EditUser = () => {
<Button variant="secondary" onClick={() => navigate("/users")}>
<ArrowLeftIcon />
</Button>
<Heading size="6">{t("common.edit")} {t("users.title")}</Heading>
<Heading size="6">
{t("common.edit")} {t("users.title")}
</Heading>
</Flex>
</Flex>
<Card className="my-4 pr-4">
@@ -120,14 +131,24 @@ const EditUser = () => {
<Text as="label" size="2">
{t("user.role")} *
</Text>
<Select value={formData.role} onValueChange={handleRoleChange}>
<Select
value={formData.role}
onValueChange={handleRoleChange}
disabled={user?.role === "admin"}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="user">User</SelectItem>
{/* 如果是 admin,只显示 admin 选项,否则显示 manager 和 user */}
{user?.role === "admin" ? (
<SelectItem value="admin">Admin</SelectItem>
) : (
<>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="user">User</SelectItem>
</>
)}
</SelectContent>
</Select>
</Box>
@@ -137,7 +158,9 @@ const EditUser = () => {
{t("common.cancel")}
</Button>
<Button type="submit" disabled={loading}>
{loading ? t("common.savingChanges") : t("common.saveChanges")}
{loading
? t("common.savingChanges")
: t("common.saveChanges")}
</Button>
</Flex>
</Flex>