From fcfda9d9be1b67bbb73aefc115e8853281779114 Mon Sep 17 00:00:00 2001 From: olli Date: Sat, 8 Nov 2025 10:50:00 +0100 Subject: [PATCH] feat: Implement user management functionality with CRUD operations - Added UserManagement.vue component for managing users with PrimeVue DataTable. - Integrated API endpoints for user CRUD operations in the backend. - Implemented user password hashing using a custom state processor. - Updated router to include user management route with admin access control. - Enhanced Dashboard.vue and app.scss for improved styling and responsiveness. - Documented user management features and API usage in USER-CRUD.md. --- README.md | 57 ++- assets/app.js | 7 +- assets/js/App.vue | 336 ++++++++++++++---- assets/js/router.js | 32 +- assets/js/views/Dashboard.vue | 29 +- assets/js/views/UserManagement.vue | 551 +++++++++++++++++++++++++++++ assets/styles/app.scss | 128 ++++++- config/services.yaml | 6 + docs/USER-CRUD.md | 275 ++++++++++++++ src/Entity/User.php | 45 +++ src/State/UserPasswordHasher.php | 36 ++ 11 files changed, 1390 insertions(+), 112 deletions(-) create mode 100644 assets/js/views/UserManagement.vue create mode 100644 docs/USER-CRUD.md create mode 100644 src/State/UserPasswordHasher.php diff --git a/README.md b/README.md index 63a070b..5f9c043 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ Eine moderne, modulare CRM-Anwendung basierend auf Symfony 7.1 LTS, Vue.js 3 und - **API Platform** - RESTful API mit OpenAPI-Dokumentation - **MariaDB** - Zuverlässige relationale Datenbank - **Webpack Encore** - Asset-Management und Hot Module Replacement +- **Modulares Berechtigungssystem** - Flexible Rollen mit Modul-basierter Rechteverwaltung +- **User CRUD** - Vollständige Benutzerverwaltung via API Platform +- **Login-System** - Form-basierte Authentifizierung mit Remember Me ## 📋 Voraussetzungen @@ -39,10 +42,20 @@ cp .env .env.local # 5. Datenbank erstellen php bin/console doctrine:database:create -# 6. Datenbank-Schema erstellen (wenn Migrations vorhanden) +# 6. Datenbank-Schema erstellen php bin/console doctrine:migrations:migrate + +# 7. Test-Daten laden (optional) +php bin/console doctrine:fixtures:load ``` +### Testbenutzer + +Nach dem Laden der Fixtures stehen folgende Testbenutzer zur Verfügung: + +- **Administrator**: admin@mycrm.local / admin123 +- **Vertriebsmitarbeiter**: sales@mycrm.local / sales123 + ## 🎯 Entwicklung ### Backend-Server starten @@ -127,17 +140,20 @@ npm run watch ## 📱 Module - **Dashboard** - Übersicht und KPIs -- **Kontakte** - Kontaktverwaltung mit Status-Tracking -- **Unternehmen** - Firmendatenbank -- **Deals** - Sales-Pipeline Management -- **Aktivitäten** - Interaktions-Historie +- **Kontakte** - Kontaktverwaltung mit Status-Tracking (in Entwicklung) +- **Unternehmen** - Firmendatenbank (in Entwicklung) +- **Deals** - Sales-Pipeline Management (in Entwicklung) +- **Aktivitäten** - Interaktions-Historie (in Entwicklung) +- **Benutzerverwaltung** - CRUD für User (✅ implementiert) ## 🔐 Sicherheit - Symfony Security Component mit Voter-Pattern -- CSRF-Schutz -- Password Hashing mit Symfony Password Hasher -- API-Authentifizierung (JWT/API Keys) +- CSRF-Schutz aktiviert +- Password Hashing mit Symfony PasswordHasher (bcrypt) +- Session-basierte Authentifizierung mit Remember Me (7 Tage) +- API-Sicherheit mit granularen Berechtigungen (ROLE_ADMIN, object == user) +- Modulares Berechtigungssystem mit 6 Aktionstypen (View, Create, Edit, Delete, Export, Manage) ## 🧪 Testing @@ -154,11 +170,14 @@ php bin/console doctrine:schema:validate ## 📚 Weitere Dokumentationen +- [LOGIN.md](docs/LOGIN.md) - Authentifizierungssystem +- [PERMISSIONS.md](docs/PERMISSIONS.md) - Modulares Berechtigungssystem +- [USER-CRUD.md](docs/USER-CRUD.md) - Benutzerverwaltung mit API Platform +- [AI Agent Instructions](.github/copilot-instructions.md) - Entwickler-Richtlinien - [Symfony Documentation](https://symfony.com/doc/current/index.html) - [API Platform Documentation](https://api-platform.com/docs/) - [Vue.js Guide](https://vuejs.org/guide/) - [PrimeVue Documentation](https://primevue.org/) -- [AI Agent Instructions](.github/copilot-instructions.md) ## 🤝 Entwicklungs-Konventionen @@ -178,10 +197,20 @@ Dein Team --- -**Status:** ✅ Projekt initialisiert und bereit für die Entwicklung! +**Status:** ✅ Grundsystem implementiert - Ready for CRM-Module! + +**Implementiert:** +- ✅ Projekt-Setup (Symfony 7.1 + Vue.js 3 + PrimeVue) +- ✅ Modulares Berechtigungssystem (User, Role, Module, RolePermission) +- ✅ Login-System mit Remember Me +- ✅ User-CRUD mit API Platform +- ✅ Vue.js Frontend mit PrimeVue DataTable, Dialogs, Forms +- ✅ Password-Hashing via State Processor +- ✅ Admin-Navigation und Schutz **Next Steps:** -1. Erste Entity erstellen: `php bin/console make:entity Contact` -2. Migration generieren: `php bin/console make:migration` -3. Migration ausführen: `php bin/console doctrine:migrations:migrate` -4. API Resource erstellen: `php bin/console make:entity --api-resource` +1. Contact-Entity erstellen: `php bin/console make:entity Contact` +2. Company-Entity erstellen: `php bin/console make:entity Company` +3. Deal-Entity mit Pipeline-Stages erstellen +4. Activity-Entity für Interaktionshistorie +5. Vue.js-Komponenten für Contact/Company/Deal-Management diff --git a/assets/app.js b/assets/app.js index 42493d4..7d3b5d9 100644 --- a/assets/app.js +++ b/assets/app.js @@ -11,6 +11,8 @@ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import PrimeVue from 'primevue/config'; import Aura from '@primevue/themes/aura'; +import ToastService from 'primevue/toastservice'; +import Tooltip from 'primevue/tooltip'; import router from './js/router'; import App from './js/App.vue'; import { useAuthStore } from './js/stores/auth'; @@ -29,10 +31,13 @@ app.use(PrimeVue, { theme: { preset: Aura, options: { - darkModeSelector: false, // Can be customized later + darkModeSelector: false, + cssLayer: false } } }); +app.use(ToastService); +app.directive('tooltip', Tooltip); app.mount('#app'); diff --git a/assets/js/App.vue b/assets/js/App.vue index 2a73149..38ed836 100644 --- a/assets/js/App.vue +++ b/assets/js/App.vue @@ -1,129 +1,329 @@ diff --git a/assets/js/router.js b/assets/js/router.js index 535b2f2..8311c35 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -1,33 +1,21 @@ import { createRouter, createWebHistory } from 'vue-router'; import Dashboard from './views/Dashboard.vue'; import ContactList from './views/ContactList.vue'; +import CompanyList from './views/CompanyList.vue'; +import DealList from './views/DealList.vue'; +import UserManagement from './views/UserManagement.vue'; const routes = [ - { - path: '/', - name: 'Dashboard', - component: Dashboard - }, - { - path: '/contacts', - name: 'ContactList', - component: ContactList - }, - { - path: '/companies', - name: 'CompanyList', - component: () => import('./views/CompanyList.vue') - }, - { - path: '/deals', - name: 'DealList', - component: () => import('./views/DealList.vue') - } + { path: '/', name: 'dashboard', component: Dashboard }, + { path: '/contacts', name: 'contacts', component: ContactList }, + { path: '/companies', name: 'companies', component: CompanyList }, + { path: '/deals', name: 'deals', component: DealList }, + { path: '/users', name: 'users', component: UserManagement, meta: { requiresAdmin: true } }, ]; const router = createRouter({ - history: createWebHistory(), - routes + history: createWebHistory(), + routes, }); export default router; diff --git a/assets/js/views/Dashboard.vue b/assets/js/views/Dashboard.vue index 1d2ace1..d5c0b23 100644 --- a/assets/js/views/Dashboard.vue +++ b/assets/js/views/Dashboard.vue @@ -43,13 +43,36 @@ import Card from 'primevue/card'; .dashboard { h2 { margin-bottom: 1rem; + font-size: 1.5rem; + + @media (min-width: 768px) { + font-size: 2rem; + } + } + + p { + font-size: 0.95rem; + + @media (min-width: 768px) { + font-size: 1rem; + } } } .dashboard-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-top: 2rem; + grid-template-columns: 1fr; + gap: 1rem; + margin-top: 1.5rem; + + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; + margin-top: 2rem; + } } diff --git a/assets/js/views/UserManagement.vue b/assets/js/views/UserManagement.vue new file mode 100644 index 0000000..c67f201 --- /dev/null +++ b/assets/js/views/UserManagement.vue @@ -0,0 +1,551 @@ + + + + + diff --git a/assets/styles/app.scss b/assets/styles/app.scss index da45935..9ca8ff1 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -10,6 +10,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background: #f9fafb; + color: #1f2937; + overflow-x: hidden; } #app { @@ -17,8 +19,126 @@ body { min-height: 100vh; } -/* PrimeVue Theme Overrides */ -:root { - --primary-color: #2563eb; - --primary-color-text: #ffffff; +/* PrimeVue Component Styling Fixes */ +.p-card { + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + + .p-card-title { + color: #1f2937; + font-weight: 600; + font-size: 1rem; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + .p-card-content { + color: #4b5563; + font-size: 0.875rem; + + @media (min-width: 768px) { + font-size: 1rem; + } + } +} + +.p-datatable { + .p-datatable-header { + background: white; + border: 1px solid #e5e7eb; + } + + .p-datatable-thead > tr > th { + background: #f9fafb; + color: #374151; + border: 1px solid #e5e7eb; + font-weight: 600; + } + + .p-datatable-tbody > tr { + background: white; + color: #1f2937; + + &:hover { + background: #f9fafb; + } + + > td { + border: 1px solid #e5e7eb; + } + } +} + +.p-button { + &.p-button-info { + background: #3b82f6; + border-color: #3b82f6; + + &:hover { + background: #2563eb; + border-color: #2563eb; + } + } + + &.p-button-danger { + background: #ef4444; + border-color: #ef4444; + + &:hover { + background: #dc2626; + border-color: #dc2626; + } + } +} + +.p-dialog { + .p-dialog-header { + background: white; + color: #1f2937; + } + + .p-dialog-content { + background: white; + color: #1f2937; + } +} + +.p-inputtext { + background: white; + color: #1f2937; + border: 1px solid #d1d5db; + + &:enabled:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25); + } +} + +.p-checkbox { + .p-checkbox-box { + background: white; + border: 1px solid #d1d5db; + + &.p-highlight { + background: #3b82f6; + border-color: #3b82f6; + } + } +} + +.p-tag { + &.p-tag-success { + background: #10b981; + } + + &.p-tag-danger { + background: #ef4444; + } + + &.p-tag-info { + background: #3b82f6; + } } diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..2b94e64 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -22,3 +22,9 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + # API Platform State Processor for User Password Hashing + App\State\UserPasswordHasher: + decorates: 'api_platform.doctrine.orm.state.persist_processor' + arguments: + $processor: '@.inner' diff --git a/docs/USER-CRUD.md b/docs/USER-CRUD.md new file mode 100644 index 0000000..6e44ea6 --- /dev/null +++ b/docs/USER-CRUD.md @@ -0,0 +1,275 @@ +# Benutzerverwaltung (User CRUD) + +## Übersicht + +Das User-CRUD-System ermöglicht Administratoren die vollständige Verwaltung von Benutzern über eine moderne Vue.js-Oberfläche mit PrimeVue-Komponenten. + +## Backend (API Platform) + +### API-Endpunkte + +- **GET /api/users** - Liste aller Benutzer (authentifiziert) +- **GET /api/users/{id}** - Einzelner Benutzer (authentifiziert) +- **POST /api/users** - Neuen Benutzer erstellen (nur ROLE_ADMIN) +- **PUT /api/users/{id}** - Benutzer bearbeiten (ROLE_ADMIN oder eigener Account) +- **DELETE /api/users/{id}** - Benutzer löschen (nur ROLE_ADMIN) + +### Sicherheitsregeln + +```php +#[ApiResource( + operations: [ + new GetCollection(), + new Get(), + new Post(security: "is_granted('ROLE_ADMIN')"), + new Put(security: "is_granted('ROLE_ADMIN') or object == user"), + new Delete(security: "is_granted('ROLE_ADMIN')") + ], + normalizationContext: ['groups' => ['user:read']], + denormalizationContext: ['groups' => ['user:write']] +)] +``` + +### Serialization Groups + +**user:read** (Ausgabe): +- id, email, firstName, lastName +- roles (array) +- isActive (boolean) +- createdAt, lastLoginAt (DateTimeImmutable) + +**user:write** (Eingabe): +- email, firstName, lastName +- plainPassword (wird automatisch gehasht) +- roles (array) +- isActive (boolean) + +### Passwort-Hashing + +Ein eigener State Processor (`App\State\UserPasswordHasher`) sorgt dafür, dass das `plainPassword`-Feld automatisch gehasht wird: + +```php +// config/services.yaml +App\State\UserPasswordHasher: + decorates: 'api_platform.doctrine.orm.state.persist_processor' + arguments: + $processor: '@.inner' +``` + +Der Processor: +1. Prüft, ob ein `plainPassword` gesetzt wurde +2. Hasht das Passwort mit `UserPasswordHasherInterface` +3. Setzt das gehashte Passwort +4. Ruft `eraseCredentials()` auf (löscht plainPassword aus dem Speicher) +5. Delegiert an den Standard-Persist-Processor + +## Frontend (Vue.js) + +### Komponente: UserManagement.vue + +**Features:** +- PrimeVue DataTable mit Sortierung, Pagination +- Erstellen/Bearbeiten via Dialog +- Löschen mit Bestätigungs-Dialog +- Formvalidierung +- Toast-Benachrichtigungen +- Eigenen Account kann nicht gelöscht werden +- Nur für Admins sichtbar + +**Formularfelder:** +- Vorname* / Nachname* +- E-Mail* +- Passwort* (bei Erstellung) / Neues Passwort (bei Bearbeitung, optional) +- Symfony-Rollen (ROLE_USER, ROLE_ADMIN via Checkboxen) +- Status (Aktiv/Inaktiv Toggle) + +### Integration + +```javascript +// router.js +{ path: '/users', name: 'users', component: UserManagement, meta: { requiresAdmin: true } } + +// App.vue Navigation (nur für Admins) + + Benutzerverwaltung + +``` + +## Verwendung + +### Neuen Benutzer erstellen + +1. Navigiere zu `/users` (nur als Admin) +2. Klicke auf "Neuer Benutzer" +3. Fülle alle Pflichtfelder aus: + - Vorname, Nachname, E-Mail + - Passwort (mind. 6 Zeichen empfohlen) + - Rollen auswählen (ROLE_USER ist Standard) + - Status auf "Aktiv" setzen +4. Klicke "Speichern" + +**API Request:** +```json +POST /api/users +{ + "firstName": "Max", + "lastName": "Mustermann", + "email": "max@example.com", + "plainPassword": "sicheres123", + "roles": ["ROLE_USER"], + "isActive": true +} +``` + +### Benutzer bearbeiten + +1. Klicke auf das Stift-Symbol in der Aktionsspalte +2. Ändere gewünschte Felder +3. Optional: Klicke "Passwort ändern" um ein neues Passwort zu setzen +4. Klicke "Speichern" + +**Hinweis:** Admins können alle Benutzer bearbeiten, normale Benutzer nur ihren eigenen Account. + +### Benutzer löschen + +1. Klicke auf das Papierkorb-Symbol +2. Bestätige die Löschung im Dialog + +**Einschränkungen:** +- Eigener Account kann nicht gelöscht werden (Button deaktiviert) +- Nur Admins können Benutzer löschen + +## Sicherheit + +### Authentifizierung + +Alle API-Endpunkte erfordern Authentifizierung. Die Vue.js-App sendet automatisch die Session-Cookies mit. + +### Autorisierung + +- **POST /api/users**: Nur ROLE_ADMIN +- **PUT /api/users/{id}**: ROLE_ADMIN oder eigener Account (`object == user`) +- **DELETE /api/users/{id}**: Nur ROLE_ADMIN +- **GET**: Alle authentifizierten Benutzer + +### CSRF-Schutz + +Da wir Session-basierte Authentifizierung verwenden, ist CSRF-Schutz automatisch aktiv. Für API-Requests ist dies standardmäßig deaktiviert. + +## Technische Details + +### Dependencies + +**Backend:** +- API Platform 4.x +- Symfony Security Component +- Doctrine ORM +- PasswordHasher + +**Frontend:** +- Vue.js 3 Composition API +- PrimeVue (DataTable, Dialog, InputText, Password, Checkbox, Button, Tag, Toast) +- Pinia (Auth Store) + +### Datenbankstruktur + +```sql +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(180) NOT NULL UNIQUE, + roles JSON NOT NULL, + password VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME NOT NULL, + last_login_at DATETIME DEFAULT NULL +); +``` + +## Testing + +### Backend + +```bash +# API-Endpunkte testen +curl -X GET http://localhost:8000/api/users \ + -H "Cookie: PHPSESSID=..." \ + -H "Accept: application/json" + +# Neuen User erstellen (als Admin) +curl -X POST http://localhost:8000/api/users \ + -H "Cookie: PHPSESSID=..." \ + -H "Content-Type: application/json" \ + -d '{ + "firstName": "Test", + "lastName": "User", + "email": "test@example.com", + "plainPassword": "test123", + "roles": ["ROLE_USER"], + "isActive": true + }' +``` + +### Frontend + +1. Melde dich als Admin an (admin@mycrm.local / admin123) +2. Navigiere zu `/users` +3. Teste alle CRUD-Operationen +4. Prüfe Browser-Konsole auf Fehler +5. Prüfe Toast-Benachrichtigungen + +## Troubleshooting + +### "Unauthorized" bei API-Calls + +**Problem:** 401 Unauthorized bei allen API-Requests + +**Lösung:** +- Stelle sicher, dass du eingeloggt bist +- Prüfe, ob Session-Cookie korrekt gesendet wird +- Cache leeren: `php bin/console cache:clear` + +### Passwort-Hashing funktioniert nicht + +**Problem:** Passwort wird nicht gehasht, Login nicht möglich + +**Lösung:** +- Prüfe, ob `UserPasswordHasher` Service registriert ist +- Stelle sicher, dass `decorates: 'api_platform.doctrine.orm.state.persist_processor'` korrekt ist +- Cache leeren + +### Vue-Komponente lädt nicht + +**Problem:** Leere Seite oder JavaScript-Fehler + +**Lösung:** +```bash +npm run build +php bin/console cache:clear +``` + +### "Cannot delete own account" + +**Problem:** Löschen-Button funktioniert nicht + +**Erklärung:** Das ist gewollt! Eigener Account kann nicht gelöscht werden (`:disabled="data.id === authStore.user?.id"`). + +## Erweiterungen + +### Mögliche zukünftige Features + +1. **Rollen-Zuweisung:** Integration mit Role-Entity für komplexere Berechtigungen +2. **Bulk-Operationen:** Mehrere Benutzer gleichzeitig bearbeiten/löschen +3. **Export:** Benutzerliste als CSV/Excel exportieren +4. **Avatar-Upload:** Profilbilder für Benutzer +5. **E-Mail-Benachrichtigungen:** Bei Accounterstellung/Passwortänderung +6. **Passwort-Reset:** "Passwort vergessen"-Funktion +7. **2FA:** Zwei-Faktor-Authentifizierung +8. **Audit Log:** Historie von Änderungen an Benutzeraccounts + +## Weitere Dokumentation + +- [LOGIN.md](./LOGIN.md) - Authentifizierungssystem +- [PERMISSIONS.md](./PERMISSIONS.md) - Berechtigungssystem mit Rollen und Modulen +- [API Platform Docs](https://api-platform.com/) diff --git a/src/Entity/User.php b/src/Entity/User.php index b03c6ee..0c157f4 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,39 +2,63 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Delete; use App\Repository\UserRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: 'users')] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] +#[ApiResource( + operations: [ + new GetCollection(), + new Get(), + new Post(security: "is_granted('ROLE_ADMIN')"), + new Put(security: "is_granted('ROLE_ADMIN') or object == user"), + new Delete(security: "is_granted('ROLE_ADMIN')") + ], + normalizationContext: ['groups' => ['user:read']], + denormalizationContext: ['groups' => ['user:write']] +)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] + #[Groups(['user:read'])] private ?int $id = null; #[ORM\Column(length: 180)] + #[Groups(['user:read', 'user:write'])] private ?string $email = null; #[ORM\Column(length: 100)] + #[Groups(['user:read', 'user:write'])] private ?string $firstName = null; #[ORM\Column(length: 100)] + #[Groups(['user:read', 'user:write'])] private ?string $lastName = null; #[ORM\Column] + #[Groups(['user:read', 'user:write'])] private bool $isActive = true; /** * @var list The user roles (Symfony standard roles for basic access control) */ #[ORM\Column] + #[Groups(['user:read', 'user:write'])] private array $roles = []; /** @@ -42,6 +66,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface */ #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')] #[ORM\JoinTable(name: 'user_roles')] + #[Groups(['user:read', 'user:write'])] private Collection $userRoles; /** @@ -50,10 +75,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?string $password = null; + /** + * Plain password for API write operations (not persisted) + */ + #[Groups(['user:write'])] + private ?string $plainPassword = null; + #[ORM\Column] + #[Groups(['user:read'])] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column(nullable: true)] + #[Groups(['user:read'])] private ?\DateTimeImmutable $lastLoginAt = null; public function __construct() @@ -126,10 +159,22 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + public function getPlainPassword(): ?string + { + return $this->plainPassword; + } + + public function setPlainPassword(?string $plainPassword): static + { + $this->plainPassword = $plainPassword; + return $this; + } + #[\Deprecated] public function eraseCredentials(): void { // @deprecated, to be removed when upgrading to Symfony 8 + $this->plainPassword = null; } public function getFirstName(): ?string diff --git a/src/State/UserPasswordHasher.php b/src/State/UserPasswordHasher.php new file mode 100644 index 0000000..d61c6c3 --- /dev/null +++ b/src/State/UserPasswordHasher.php @@ -0,0 +1,36 @@ +processor->process($data, $operation, $uriVariables, $context); + } + + // Hash plain password if provided + if ($data->getPlainPassword()) { + $hashedPassword = $this->passwordHasher->hashPassword( + $data, + $data->getPlainPassword() + ); + $data->setPassword($hashedPassword); + $data->eraseCredentials(); + } + + return $this->processor->process($data, $operation, $uriVariables, $context); + } +}