fix(auth) : use dedicated plainPassword field for password hashing
- Add non-persisted plainPassword field to User entity (write-only via API) - Remove direct write access to password field - Update UserPasswordHasherProcessor to hash from plainPassword - Update frontend DTO and UserDrawer component Ticket: T-009 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'">
|
<AppDrawer v-model="isOpen" :title="isEditing ? $t('users.editUser') : $t('users.addUser')">
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-2" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
@@ -90,6 +90,8 @@ import { useProjectService } from '~/services/projects'
|
|||||||
import type { Client } from '~/services/dto/client'
|
import type { Client } from '~/services/dto/client'
|
||||||
import type { Project } from '~/services/dto/project'
|
import type { Project } from '~/services/dto/project'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
item: UserData | null
|
item: UserData | null
|
||||||
@@ -114,7 +116,7 @@ const clients = ref<Client[]>([])
|
|||||||
const allProjects = ref<Project[]>([])
|
const allProjects = ref<Project[]>([])
|
||||||
|
|
||||||
const clientOptions = computed(() => [
|
const clientOptions = computed(() => [
|
||||||
{ label: 'Aucun client', value: null as number | null },
|
{ label: t('common.noClient'), value: null as number | null },
|
||||||
...clients.value.map((c) => ({ label: c.name, value: c.id as number | null })),
|
...clients.value.map((c) => ({ label: c.name, value: c.id as number | null })),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -190,7 +192,7 @@ async function handleSubmit() {
|
|||||||
allowedProjects: form.allowedProjectIds.map((id) => `/api/projects/${id}`),
|
allowedProjects: form.allowedProjectIds.map((id) => `/api/projects/${id}`),
|
||||||
}
|
}
|
||||||
if (form.password) {
|
if (form.password) {
|
||||||
payload.password = form.password
|
payload.plainPassword = form.password
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing.value && props.item) {
|
if (isEditing.value && props.item) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type UserData = {
|
|||||||
|
|
||||||
export type UserWrite = {
|
export type UserWrite = {
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
plainPassword?: string
|
||||||
roles: string[]
|
roles: string[]
|
||||||
client?: string | null
|
client?: string | null
|
||||||
allowedProjects?: string[]
|
allowedProjects?: string[]
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
private array $roles = [];
|
private array $roles = [];
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['user:write'])]
|
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[Groups(['user:write'])]
|
||||||
|
private ?string $plainPassword = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
private ?DateTimeImmutable $createdAt = null;
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
@@ -224,5 +226,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
return '/api/users/'.$this->id.'/avatar';
|
return '/api/users/'.$this->id.'/avatar';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function eraseCredentials(): void {}
|
public function getPlainPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlainPassword(?string $plainPassword): static
|
||||||
|
{
|
||||||
|
$this->plainPassword = $plainPassword;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eraseCredentials(): void
|
||||||
|
{
|
||||||
|
$this->plainPassword = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ final readonly class UserPasswordHasherProcessor implements ProcessorInterface
|
|||||||
*/
|
*/
|
||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||||
{
|
{
|
||||||
$plainPassword = $data->getPassword();
|
$plainPassword = $data->getPlainPassword();
|
||||||
|
|
||||||
if (null !== $plainPassword && !str_starts_with($plainPassword, '$')) {
|
if (null !== $plainPassword && '' !== $plainPassword) {
|
||||||
$data->setPassword($this->passwordHasher->hashPassword($data, $plainPassword));
|
$data->setPassword($this->passwordHasher->hashPassword($data, $plainPassword));
|
||||||
|
$data->setPlainPassword(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||||
|
|||||||
Reference in New Issue
Block a user