diff --git a/frontend/components/AdminUserTab.vue b/frontend/components/AdminUserTab.vue new file mode 100644 index 0000000..8c537d6 --- /dev/null +++ b/frontend/components/AdminUserTab.vue @@ -0,0 +1,106 @@ + + + + Utilisateurs + + + Ajouter un utilisateur + + + + + + + + Nom d'utilisateur + Rôles + Actions + + + + + {{ item.username }} + + + {{ role }} + + + + + + + + + + + Aucun utilisateur trouvé. + + + + + + + + + + + diff --git a/frontend/components/UserDrawer.vue b/frontend/components/UserDrawer.vue new file mode 100644 index 0000000..3561701 --- /dev/null +++ b/frontend/components/UserDrawer.vue @@ -0,0 +1,133 @@ + + + + + + + Rôles + + + + {{ role }} + + + + + + + Enregistrer + + + + + + + diff --git a/frontend/services/dto/user-data.ts b/frontend/services/dto/user-data.ts index bfe1fec..3ea999e 100644 --- a/frontend/services/dto/user-data.ts +++ b/frontend/services/dto/user-data.ts @@ -1,5 +1,12 @@ export type UserData = { - id: number - username: string - roles: string[] + id: number + '@id'?: string + username: string + roles: string[] +} + +export type UserWrite = { + username: string + password?: string + roles: string[] } diff --git a/frontend/services/users.ts b/frontend/services/users.ts new file mode 100644 index 0000000..69e02f0 --- /dev/null +++ b/frontend/services/users.ts @@ -0,0 +1,32 @@ +import type { UserData, UserWrite } from './dto/user-data' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useUserService() { + const api = useApi() + + async function getAll(): Promise { + const data = await api.get>('/users') + return extractHydraMembers(data) + } + + async function create(payload: UserWrite): Promise { + return api.post('/users', payload as Record, { + toastSuccessKey: 'users.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/users/${id}`, payload as Record, { + toastSuccessKey: 'users.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/users/${id}`, {}, { + toastSuccessKey: 'users.deleted', + }) + } + + return { getAll, create, update, remove } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index b00348f..fb40e9d 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -5,9 +5,14 @@ declare(strict_types=1); namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; use App\Repository\UserRepository; use App\State\MeProvider; +use App\State\UserPasswordHasherProcessor; use DateTimeImmutable; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -22,7 +27,17 @@ use Symfony\Component\Serializer\Attribute\Groups; provider: MeProvider::class, normalizationContext: ['groups' => ['me:read']], ), + new Get( + normalizationContext: ['groups' => ['user:list']], + ), + new GetCollection( + normalizationContext: ['groups' => ['user:list']], + ), + new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class), + new Patch(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class), + new Delete(security: "is_granted('ROLE_ADMIN')"), ], + denormalizationContext: ['groups' => ['user:write']], )] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] @@ -31,19 +46,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['me:read'])] + #[Groups(['me:read', 'task:read', 'user:list'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] - #[Groups(['me:read'])] + #[Groups(['me:read', 'task:read', 'user:list', 'user:write'])] private ?string $username = null; /** @var list */ #[ORM\Column] - #[Groups(['me:read'])] + #[Groups(['me:read', 'user:list', 'user:write'])] private array $roles = []; #[ORM\Column] + #[Groups(['user:write'])] private ?string $password = null; #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] diff --git a/src/State/UserPasswordHasherProcessor.php b/src/State/UserPasswordHasherProcessor.php new file mode 100644 index 0000000..d4a9437 --- /dev/null +++ b/src/State/UserPasswordHasherProcessor.php @@ -0,0 +1,40 @@ + + */ +final readonly class UserPasswordHasherProcessor implements ProcessorInterface +{ + /** + * @param ProcessorInterface $persistProcessor + */ + public function __construct( + #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private ProcessorInterface $persistProcessor, + private UserPasswordHasherInterface $passwordHasher, + ) {} + + /** + * @param User $data + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + if (null !== $data->getPassword() && !str_starts_with($data->getPassword(), '$')) { + $data->setPassword( + $this->passwordHasher->hashPassword($data, $data->getPassword()) + ); + } + + return $this->persistProcessor->process($data, $operation, $uriVariables, $context); + } +}