Compare commits

..

No commits in common. "2645cf1cd63ab85cdcd91ad9f806305e03087e87" and "d8572e7fd6a0690a6713160116c8607649c506f0" have entirely different histories.

4 changed files with 41 additions and 142 deletions

View File

@ -155,16 +155,6 @@ 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');
@ -261,12 +251,6 @@ 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

@ -9,14 +9,12 @@ class AdminUserFormDialog extends StatefulWidget {
final AppUser? initialUser; final AppUser? initialUser;
final bool withRelais; final bool withRelais;
final bool adminMode; final bool adminMode;
final bool readOnly;
const AdminUserFormDialog({ const AdminUserFormDialog({
super.key, super.key,
this.initialUser, this.initialUser,
this.withRelais = true, this.withRelais = true,
this.adminMode = false, this.adminMode = false,
this.readOnly = false,
}); });
@override @override
@ -39,10 +37,6 @@ 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() {
@ -200,7 +194,6 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
} }
Future<void> _submit() async { Future<void> _submit() async {
if (widget.readOnly) return;
if (_isSubmitting) return; if (_isSubmitting) return;
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
@ -216,12 +209,10 @@ 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: _isLockedAdminIdentity ? lockedNom : normalizedNom, nom: normalizedNom,
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom, prenom: normalizedPrenom,
email: _emailController.text.trim(), email: _emailController.text.trim(),
telephone: normalizedPhone.isEmpty telephone: normalizedPhone.isEmpty
? _normalizePhone(widget.initialUser!.telephone ?? '') ? _normalizePhone(widget.initialUser!.telephone ?? '')
@ -313,8 +304,6 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
} }
Future<void> _delete() async { Future<void> _delete() async {
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>(
@ -374,19 +363,15 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
Expanded( Expanded(
child: Text( child: Text(
_isEditMode _isEditMode
? (widget.readOnly ? (widget.adminMode
? (widget.adminMode ? 'Modifier un administrateur'
? 'Consulter un administrateur' : 'Modifier un gestionnaire')
: 'Consulter un gestionnaire')
: (widget.adminMode
? 'Modifier un administrateur'
: 'Modifier un gestionnaire'))
: (widget.adminMode : (widget.adminMode
? 'Créer un administrateur' ? 'Créer un administrateur'
: 'Créer un gestionnaire'), : 'Créer un gestionnaire'),
), ),
), ),
if (_isEditMode && !widget.readOnly) if (_isEditMode)
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
tooltip: 'Fermer', tooltip: 'Fermer',
@ -431,18 +416,12 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
), ),
), ),
actions: [ actions: [
if (widget.readOnly) ...[ if (_isEditMode) ...[
FilledButton( OutlinedButton(
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false), onPressed: _isSubmitting ? null : _delete,
child: const Text('Fermer'), style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
child: const Text('Supprimer'),
), ),
] else if (_isEditMode) ...[
if (!_isSuperAdminTarget)
OutlinedButton(
onPressed: _isSubmitting ? null : _delete,
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
@ -479,50 +458,42 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
Widget _buildNomField() { Widget _buildNomField() {
return TextFormField( return TextFormField(
controller: _nomController, controller: _nomController,
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 || _isLockedAdminIdentity) validator: (v) => _required(v, 'Nom'),
? null
: (v) => _required(v, 'Nom'),
); );
} }
Widget _buildPrenomField() { Widget _buildPrenomField() {
return TextFormField( return TextFormField(
controller: _prenomController, controller: _prenomController,
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 || _isLockedAdminIdentity) validator: (v) => _required(v, 'Prénom'),
? null
: (v) => _required(v, 'Prénom'),
); );
} }
Widget _buildEmailField() { Widget _buildEmailField() {
return TextFormField( return TextFormField(
controller: _emailController, controller: _emailController,
readOnly: widget.readOnly,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Email', labelText: 'Email',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: widget.readOnly ? null : _validateEmail, validator: _validateEmail,
); );
} }
Widget _buildPasswordField() { Widget _buildPasswordField() {
return TextFormField( return TextFormField(
controller: _passwordController, controller: _passwordController,
readOnly: widget.readOnly,
obscureText: _obscurePassword, obscureText: _obscurePassword,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
@ -534,43 +505,38 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
? 'Nouveau mot de passe' ? 'Nouveau mot de passe'
: 'Mot de passe', : 'Mot de passe',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: widget.readOnly suffixIcon: ExcludeFocus(
? null child: IconButton(
: ExcludeFocus( focusNode: _passwordToggleFocusNode,
child: IconButton( onPressed: () {
focusNode: _passwordToggleFocusNode, setState(() {
onPressed: () { _obscurePassword = !_obscurePassword;
setState(() { });
_obscurePassword = !_obscurePassword; },
}); icon: Icon(
}, _obscurePassword ? Icons.visibility_off : Icons.visibility,
icon: Icon( ),
_obscurePassword ? Icons.visibility_off : Icons.visibility, ),
), ),
),
),
), ),
validator: widget.readOnly ? null : _validatePassword, validator: _validatePassword,
); );
} }
Widget _buildTelephoneField() { Widget _buildTelephoneField() {
return TextFormField( return TextFormField(
controller: _telephoneController, controller: _telephoneController,
readOnly: widget.readOnly,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
inputFormatters: widget.readOnly inputFormatters: [
? null FilteringTextInputFormatter.digitsOnly,
: [ LengthLimitingTextInputFormatter(10),
FilteringTextInputFormatter.digitsOnly, _FrenchPhoneNumberFormatter(),
LengthLimitingTextInputFormatter(10), ],
_FrenchPhoneNumberFormatter(),
],
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Téléphone (ex: 06 12 34 56 78)', labelText: 'Téléphone (ex: 06 12 34 56 78)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: widget.readOnly ? null : _validatePhone, validator: _validatePhone,
); );
} }
@ -602,7 +568,7 @@ class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
), ),
), ),
], ],
onChanged: (_isLoadingRelais || widget.readOnly) onChanged: _isLoadingRelais
? null ? null
: (value) { : (value) {
setState(() { setState(() {

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/models/user.dart';
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart'; import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
import 'package:p_tits_pas/widgets/admin/common/user_list.dart'; import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
@ -22,12 +21,10 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
List<AppUser> _admins = []; List<AppUser> _admins = [];
String? _currentUserRole;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadCurrentUserRole();
_loadAdmins(); _loadAdmins();
} }
@ -55,31 +52,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
} }
} }
Future<void> _loadCurrentUserRole() async {
final cached = await AuthService.getCurrentUser();
if (!mounted) return;
if (cached != null) {
setState(() {
_currentUserRole = cached.role.toLowerCase();
});
return;
}
final refreshed = await AuthService.refreshCurrentUser();
if (!mounted || refreshed == null) return;
setState(() {
_currentUserRole = refreshed.role.toLowerCase();
});
}
bool _isSuperAdmin(AppUser user) => user.role.toLowerCase() == 'super_admin';
bool _canEditAdmin(AppUser target) {
if (!_isSuperAdmin(target)) return true;
return _currentUserRole == 'super_admin';
}
Future<void> _openAdminEditDialog(AppUser user) async { Future<void> _openAdminEditDialog(AppUser user) async {
final canEdit = _canEditAdmin(user);
final changed = await showDialog<bool>( final changed = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -88,11 +61,10 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
initialUser: user, initialUser: user,
adminMode: true, adminMode: true,
withRelais: false, withRelais: false,
readOnly: !canEdit,
); );
}, },
); );
if (changed == true && canEdit) { if (changed == true) {
await _loadAdmins(); await _loadAdmins();
} }
} }
@ -114,8 +86,6 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
itemCount: filteredAdmins.length, itemCount: filteredAdmins.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = filteredAdmins[index]; final user = filteredAdmins[index];
final isSuperAdmin = _isSuperAdmin(user);
final canEdit = _canEditAdmin(user);
return AdminUserCard( return AdminUserCard(
title: user.fullName, title: user.fullName,
subtitleLines: [ subtitleLines: [
@ -123,22 +93,10 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
'Rôle : ${user.role}', 'Rôle : ${user.role}',
], ],
avatarUrl: user.photoUrl, avatarUrl: user.photoUrl,
borderColor: isSuperAdmin
? const Color(0xFF8E6AC8)
: Colors.grey.shade300,
backgroundColor: isSuperAdmin
? const Color(0xFFF4EEFF)
: Colors.white,
titleColor: isSuperAdmin ? const Color(0xFF5D2F99) : null,
infoColor: isSuperAdmin
? const Color(0xFF6D4EA1)
: Colors.black54,
actions: [ actions: [
IconButton( IconButton(
icon: Icon( icon: const Icon(Icons.edit),
canEdit ? Icons.edit_outlined : Icons.visibility_outlined, tooltip: 'Modifier',
),
tooltip: canEdit ? 'Modifier' : 'Consulter',
onPressed: () { onPressed: () {
_openAdminEditDialog(user); _openAdminEditDialog(user);
}, },

View File

@ -6,10 +6,6 @@ class AdminUserCard extends StatefulWidget {
final String? avatarUrl; final String? avatarUrl;
final IconData fallbackIcon; final IconData fallbackIcon;
final List<Widget> actions; final List<Widget> actions;
final Color? borderColor;
final Color? backgroundColor;
final Color? titleColor;
final Color? infoColor;
const AdminUserCard({ const AdminUserCard({
super.key, super.key,
@ -18,10 +14,6 @@ class AdminUserCard extends StatefulWidget {
this.avatarUrl, this.avatarUrl,
this.fallbackIcon = Icons.person, this.fallbackIcon = Icons.person,
this.actions = const [], this.actions = const [],
this.borderColor,
this.backgroundColor,
this.titleColor,
this.infoColor,
}); });
@override @override
@ -51,10 +43,9 @@ class _AdminUserCardState extends State<AdminUserCard> {
child: Card( child: Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
elevation: 0, elevation: 0,
color: widget.backgroundColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
side: BorderSide(color: widget.borderColor ?? Colors.grey.shade300), side: BorderSide(color: Colors.grey.shade300),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
@ -85,7 +76,7 @@ class _AdminUserCardState extends State<AdminUserCard> {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 14, fontSize: 14,
).copyWith(color: widget.titleColor), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -97,7 +88,7 @@ class _AdminUserCardState extends State<AdminUserCard> {
style: const TextStyle( style: const TextStyle(
color: Colors.black54, color: Colors.black54,
fontSize: 12, fontSize: 12,
).copyWith(color: widget.infoColor), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),