fix(users): admin can never strip their own admin permission
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Both backend (PATCH /users/{id}) and frontend (Edit User modal) now block
an admin from changing their own primary role away from admin or removing
admin from their own extra_roles, closing off a self-lockout scenario.
This commit is contained in:
@@ -268,6 +268,7 @@ export default function UsersPage() {
|
||||
{editingUser && (
|
||||
<EditUserModal
|
||||
user={editingUser}
|
||||
isSelfAdmin={editingUser.id === currentUser?.id && currentUser?.role === "admin"}
|
||||
onClose={() => setEditingUser(null)}
|
||||
onSubmit={(payload) =>
|
||||
updateMutation.mutate({ userId: editingUser.id, payload })
|
||||
@@ -415,13 +416,16 @@ function CreateUserModal({ onClose, onSubmit, isSubmitting, error }: CreateUserM
|
||||
|
||||
interface EditUserModalProps {
|
||||
user: UserOut;
|
||||
/** True when the account being edited is the logged-in admin's own —
|
||||
* an admin must never be able to strip their own admin permission. */
|
||||
isSelfAdmin: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: Parameters<typeof updateUser>[1]) => void;
|
||||
isSubmitting: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
function EditUserModal({ user, onClose, onSubmit, isSubmitting, error }: EditUserModalProps) {
|
||||
function EditUserModal({ user, isSelfAdmin, onClose, onSubmit, isSubmitting, error }: EditUserModalProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
full_name: user.full_name || "",
|
||||
email: user.email || "",
|
||||
@@ -489,8 +493,9 @@ function EditUserModal({ user, onClose, onSubmit, isSubmitting, error }: EditUse
|
||||
<label className="block text-sm font-medium text-gray-300">Role</label>
|
||||
<select
|
||||
value={formData.role}
|
||||
disabled={isSelfAdmin}
|
||||
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
||||
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-gray-200"
|
||||
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-gray-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{ROLES.map((role) => (
|
||||
<option key={role.value} value={role.value}>
|
||||
@@ -498,6 +503,11 @@ function EditUserModal({ user, onClose, onSubmit, isSubmitting, error }: EditUse
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isSelfAdmin && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
You can't change your own role away from admin.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -505,20 +515,24 @@ function EditUserModal({ user, onClose, onSubmit, isSubmitting, error }: EditUse
|
||||
Additional Roles <span className="text-gray-500">(switchable via the top-bar role switcher)</span>
|
||||
</label>
|
||||
<div className="mt-1.5 grid grid-cols-2 gap-1.5">
|
||||
{ROLES.filter((r) => r.value !== formData.role).map((role) => (
|
||||
<label
|
||||
key={role.value}
|
||||
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1.5 text-sm text-gray-300"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.extra_roles.includes(role.value)}
|
||||
onChange={() => toggleExtraRole(role.value)}
|
||||
className="h-3.5 w-3.5 rounded border-gray-600 bg-gray-900 text-cyan-500 focus:ring-cyan-500"
|
||||
/>
|
||||
{role.label}
|
||||
</label>
|
||||
))}
|
||||
{ROLES.filter((r) => r.value !== formData.role).map((role) => {
|
||||
const lockAdmin = isSelfAdmin && role.value === "admin" && formData.extra_roles.includes("admin");
|
||||
return (
|
||||
<label
|
||||
key={role.value}
|
||||
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1.5 text-sm text-gray-300"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.extra_roles.includes(role.value)}
|
||||
disabled={lockAdmin}
|
||||
onChange={() => toggleExtraRole(role.value)}
|
||||
className="h-3.5 w-3.5 rounded border-gray-600 bg-gray-900 text-cyan-500 focus:ring-cyan-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
{role.label}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user