fix(#96): protéger le super admin en édition et suppression
Empêche la suppression d'un super administrateur et fige son identité (nom/prénom) côté API, avec alignement de la modale frontend pour masquer la suppression et verrouiller ces champs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e2ebc6a0a1
commit
2645cf1cd6
@ -155,6 +155,16 @@ export class UserService {
|
|||||||
async updateUser(id: string, dto: UpdateUserDto, currentUser: Users): Promise<Users> {
|
async updateUser(id: string, dto: UpdateUserDto, currentUser: Users): Promise<Users> {
|
||||||
const user = await this.findOne(id);
|
const user = await this.findOne(id);
|
||||||
|
|
||||||
|
// Le super administrateur conserve une identité figée.
|
||||||
|
if (
|
||||||
|
user.role === RoleType.SUPER_ADMIN &&
|
||||||
|
(dto.nom !== undefined || dto.prenom !== undefined)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Le nom et le prénom du super administrateur ne peuvent pas être modifiés',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Interdire changement de rôle si pas super admin
|
// Interdire changement de rôle si pas super admin
|
||||||
if (dto.role && currentUser.role !== RoleType.SUPER_ADMIN) {
|
if (dto.role && currentUser.role !== RoleType.SUPER_ADMIN) {
|
||||||
throw new ForbiddenException('Accès réservé aux super admins');
|
throw new ForbiddenException('Accès réservé aux super admins');
|
||||||
@ -251,6 +261,12 @@ export class UserService {
|
|||||||
if (currentUser.role !== RoleType.SUPER_ADMIN) {
|
if (currentUser.role !== RoleType.SUPER_ADMIN) {
|
||||||
throw new ForbiddenException('Accès réservé aux super admins');
|
throw new ForbiddenException('Accès réservé aux super admins');
|
||||||
}
|
}
|
||||||
|
const user = await this.findOne(id);
|
||||||
|
if (user.role === RoleType.SUPER_ADMIN) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Le super administrateur ne peut pas être supprimé',
|
||||||
|
);
|
||||||
|
}
|
||||||
const result = await this.usersRepository.delete(id);
|
const result = await this.usersRepository.delete(id);
|
||||||
if (result.affected === 0) {
|
if (result.affected === 0) {
|
||||||
throw new NotFoundException('Utilisateur introuvable');
|
throw new NotFoundException('Utilisateur introuvable');
|
||||||
|
|||||||
@ -39,6 +39,10 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|||||||
List<RelaisModel> _relais = [];
|
List<RelaisModel> _relais = [];
|
||||||
String? _selectedRelaisId;
|
String? _selectedRelaisId;
|
||||||
bool get _isEditMode => widget.initialUser != null;
|
bool get _isEditMode => widget.initialUser != null;
|
||||||
|
bool get _isSuperAdminTarget =>
|
||||||
|
widget.initialUser?.role.toLowerCase() == 'super_admin';
|
||||||
|
bool get _isLockedAdminIdentity =>
|
||||||
|
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -212,10 +216,12 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|||||||
|
|
||||||
if (_isEditMode) {
|
if (_isEditMode) {
|
||||||
if (widget.adminMode) {
|
if (widget.adminMode) {
|
||||||
|
final lockedNom = _toTitleCase(widget.initialUser!.nom ?? '');
|
||||||
|
final lockedPrenom = _toTitleCase(widget.initialUser!.prenom ?? '');
|
||||||
await UserService.updateAdministrateur(
|
await UserService.updateAdministrateur(
|
||||||
adminId: widget.initialUser!.id,
|
adminId: widget.initialUser!.id,
|
||||||
nom: normalizedNom,
|
nom: _isLockedAdminIdentity ? lockedNom : normalizedNom,
|
||||||
prenom: normalizedPrenom,
|
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom,
|
||||||
email: _emailController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
telephone: normalizedPhone.isEmpty
|
telephone: normalizedPhone.isEmpty
|
||||||
? _normalizePhone(widget.initialUser!.telephone ?? '')
|
? _normalizePhone(widget.initialUser!.telephone ?? '')
|
||||||
@ -308,6 +314,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|||||||
|
|
||||||
Future<void> _delete() async {
|
Future<void> _delete() async {
|
||||||
if (widget.readOnly) return;
|
if (widget.readOnly) return;
|
||||||
|
if (_isSuperAdminTarget) return;
|
||||||
if (!_isEditMode || _isSubmitting) return;
|
if (!_isEditMode || _isSubmitting) return;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
@ -430,11 +437,12 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|||||||
child: const Text('Fermer'),
|
child: const Text('Fermer'),
|
||||||
),
|
),
|
||||||
] else if (_isEditMode) ...[
|
] else if (_isEditMode) ...[
|
||||||
OutlinedButton(
|
if (!_isSuperAdminTarget)
|
||||||
onPressed: _isSubmitting ? null : _delete,
|
OutlinedButton(
|
||||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
onPressed: _isSubmitting ? null : _delete,
|
||||||
child: const Text('Supprimer'),
|
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
||||||
),
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _isSubmitting ? null : _submit,
|
onPressed: _isSubmitting ? null : _submit,
|
||||||
icon: _isSubmitting
|
icon: _isSubmitting
|
||||||
@ -471,26 +479,30 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|||||||
Widget _buildNomField() {
|
Widget _buildNomField() {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: _nomController,
|
controller: _nomController,
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Nom',
|
labelText: 'Nom',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
validator: widget.readOnly ? null : (v) => _required(v, 'Nom'),
|
validator: (widget.readOnly || _isLockedAdminIdentity)
|
||||||
|
? null
|
||||||
|
: (v) => _required(v, 'Nom'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPrenomField() {
|
Widget _buildPrenomField() {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: _prenomController,
|
controller: _prenomController,
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Prénom',
|
labelText: 'Prénom',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
validator: widget.readOnly ? null : (v) => _required(v, 'Prénom'),
|
validator: (widget.readOnly || _isLockedAdminIdentity)
|
||||||
|
? null
|
||||||
|
: (v) => _required(v, 'Prénom'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user