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:
MARTIN Julien 2026-02-24 22:05:17 +01:00
parent e2ebc6a0a1
commit 2645cf1cd6
2 changed files with 39 additions and 11 deletions

View File

@ -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');

View File

@ -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'),
); );
} }