diff --git a/backend/scripts/create-gitea-issue-gestionnaire-self-delete.js b/backend/scripts/create-gitea-issue-gestionnaire-self-delete.js new file mode 100644 index 0000000..0bc75cb --- /dev/null +++ b/backend/scripts/create-gitea-issue-gestionnaire-self-delete.js @@ -0,0 +1,111 @@ +/** + * Crée l'issue Gitea — gestionnaire ne doit pas pouvoir se supprimer. + * Usage: node backend/scripts/create-gitea-issue-gestionnaire-self-delete.js + */ +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.join(__dirname, '../..'); +const MILESTONE_0_1_0 = 10; + +let token = process.env.GITEA_TOKEN; +if (!token) { + try { + const tokenFile = path.join(repoRoot, '.gitea-token'); + if (fs.existsSync(tokenFile)) token = fs.readFileSync(tokenFile, 'utf8').trim(); + } catch (_) {} +} +if (!token) { + try { + const briefing = fs.readFileSync( + path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'), + 'utf8', + ); + const m = briefing.match(/Token:\s*(gitebu_[a-f0-9]+)/); + if (m) token = m[1].trim(); + } catch (_) {} +} +if (!token) { + console.error('Token non trouvé : .gitea-token ou GITEA_TOKEN'); + process.exit(1); +} + +const body = `## Contexte + +Quand un **gestionnaire** connecté ouvre sa propre fiche dans l'onglet **Gestionnaires** (Gestion des utilisateurs), la modale **Modifier un "Gestionnaire"** affiche le bouton **Supprimer**. + +Comportement actuel : le gestionnaire voit et peut tenter de supprimer son propre compte. + +## Comportement attendu + +Comme pour le **super administrateur** (bouton Supprimer masqué sur la fiche super admin) : + +- **Pas de bouton Supprimer** quand l'utilisateur édite **sa propre fiche** +- **Modification** des informations (prénom, nom, email, téléphone, relais, mot de passe) **toujours autorisée** + +## Périmètre + +- Frontend : \`AdminUserFormDialog\` (\`gestionnaires_create.dart\`) +- Comparer \`initialUser.id\` avec l'utilisateur connecté (\`AuthService.getCurrentUser\`) +- Masquer Supprimer si édition de soi-même ; conserver garde existante super admin + +## Critères d'acceptation + +- [ ] Gestionnaire connecté → ouvre sa fiche → **pas** de bouton Supprimer +- [ ] Gestionnaire connecté → peut **Modifier** ses informations +- [ ] Super admin / admin → peut toujours supprimer **un autre** gestionnaire (si droits API) +- [ ] Pas de régression sur fiche super administrateur (Supprimer toujours masqué) + +## Fichiers clés + +- \`frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart\` +- \`frontend/lib/widgets/admin/gestionnaire_management_widget.dart\` + +## Milestone + +**0.1.0** — correction UX / sécurité gestion utilisateurs.`; + +const payloadClean = JSON.stringify({ + title: '[Bug] Gestionnaire peut voir Supprimer sur sa propre fiche', + body, + milestone: MILESTONE_0_1_0, +}); + +const opts = { + hostname: 'git.ptits-pas.fr', + path: '/api/v1/repos/jmartin/petitspas/issues', + method: 'POST', + headers: { + Authorization: 'token ' + token, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payloadClean), + }, +}; + +const req = https.request(opts, (res) => { + let d = ''; + res.on('data', (c) => (d += c)); + res.on('end', () => { + try { + const o = JSON.parse(d); + if (o.number) { + console.log('NUMBER:', o.number); + console.log('URL:', o.html_url); + console.log('MILESTONE:', o.milestone?.title ?? '(aucun)'); + } else { + console.error('Réponse:', d); + process.exit(1); + } + } catch (e) { + console.error(d); + process.exit(1); + } + }); +}); +req.on('error', (e) => { + console.error(e); + process.exit(1); +}); +req.write(payloadClean); +req.end(); diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart index a1b5f92..4f89ab7 100644 --- a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -5,6 +5,7 @@ import 'package:p_tits_pas/utils/email_utils.dart'; import 'package:p_tits_pas/widgets/email_text_field.dart'; import 'package:p_tits_pas/widgets/french_phone_field.dart'; import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/services/auth_service.dart'; import 'package:p_tits_pas/services/relais_service.dart'; import 'package:p_tits_pas/services/user_service.dart'; @@ -41,9 +42,15 @@ class _AdminUserFormDialogState extends State { bool _isLoadingRelais = true; List _relais = []; String? _selectedRelaisId; + String? _currentUserId; bool get _isEditMode => widget.initialUser != null; bool get _isSuperAdminTarget => (widget.initialUser?.role ?? '').toLowerCase() == 'super_admin'; + bool get _isSelfTarget => + _isEditMode && + _currentUserId != null && + widget.initialUser!.id == _currentUserId; + bool get _canDeleteTarget => !_isSuperAdminTarget && !_isSelfTarget; bool get _isLockedAdminIdentity => _isEditMode && widget.adminMode && _isSuperAdminTarget; String get _targetRoleKey { @@ -109,6 +116,23 @@ class _AdminUserFormDialogState extends State { } else { _isLoadingRelais = false; } + _loadCurrentUserId(); + } + + Future _loadCurrentUserId() async { + final cached = await AuthService.getCurrentUser(); + if (!mounted) return; + if (cached != null) { + setState(() { + _currentUserId = cached.id; + }); + return; + } + final refreshed = await AuthService.refreshCurrentUser(); + if (!mounted || refreshed == null) return; + setState(() { + _currentUserId = refreshed.id; + }); } @override @@ -335,7 +359,7 @@ class _AdminUserFormDialogState extends State { Future _delete() async { if (widget.readOnly) return; - if (_isSuperAdminTarget) return; + if (!_canDeleteTarget) return; if (!_isEditMode || _isSubmitting) return; final confirmed = await showDialog( @@ -462,7 +486,7 @@ class _AdminUserFormDialogState extends State { child: const Text('Fermer'), ), ] else if (_isEditMode) ...[ - if (!_isSuperAdminTarget) + if (_canDeleteTarget) OutlinedButton( onPressed: _isSubmitting ? null : _delete, style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),