Compare commits

..

2 Commits

Author SHA1 Message Date
gitea-actions
696100a622 chore: bump version to v0.0.79
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build Release Artefact / build (push) Successful in 1m16s
2026-03-25 14:53:49 +00:00
97f21ab35c [#FER-12] Ajouter un blocage des utilisateurs (!41)
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #41
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-03-25 14:53:43 +00:00
12 changed files with 165 additions and 25 deletions

42
.idea/workspace.xml generated
View File

@@ -4,11 +4,19 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : bouton de mise en attente">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : système de blocage utilisateur">
<change afterPath="$PROJECT_DIR$/migrations/Version20260325142815.php" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/Security/UserChecker.php" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/State/ActiveUsersProvider.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/packages/security.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/packages/security.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/auth.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/auth.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/user/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/user/[[id]].vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/user/list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/user/list.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/user-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/user-data.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/stores/auth.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/stores/auth.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/User.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/User.php" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -40,7 +48,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="develop" />
<entry key="$PROJECT_DIR$" value="fix/FER-11-corriger-le-probleme-de-bearer-token" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -231,7 +239,7 @@
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "fix/FER-11-corriger-le-probleme-de-bearer-token",
"git-widget-placeholder": "feature/FER-12-ajouter-un-blocage-des-utilisateurs",
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/Ferme",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
@@ -325,15 +333,7 @@
<workItem from="1773766075191" duration="6202000" />
<workItem from="1773824491213" duration="24805000" />
<workItem from="1774275549972" duration="51000" />
<workItem from="1774276665015" duration="13844000" />
</task>
<task id="LOCAL-00031" summary="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil">
<option name="closed" value="true" />
<created>1769098861988</created>
<option name="number" value="00031" />
<option name="presentableId" value="LOCAL-00031" />
<option name="project" value="LOCAL" />
<updated>1769098861988</updated>
<workItem from="1774276665015" duration="16178000" />
</task>
<task id="LOCAL-00032" summary="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster">
<option name="closed" value="true" />
@@ -719,7 +719,15 @@
<option name="project" value="LOCAL" />
<updated>1774337609427</updated>
</task>
<option name="localTasksCounter" value="80" />
<task id="LOCAL-00080" summary="fix : problème de bearer token">
<option name="closed" value="true" />
<created>1774448105945</created>
<option name="number" value="00080" />
<option name="presentableId" value="LOCAL-00080" />
<option name="project" value="LOCAL" />
<updated>1774448105945</updated>
</task>
<option name="localTasksCounter" value="81" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -769,8 +777,6 @@
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="feat : changelog" />
<MESSAGE value="feat : lister les expeditions terminees" />
<MESSAGE value="fix: corrections diverses" />
<MESSAGE value="fix : corrections diverses" />
<MESSAGE value="fix : corrections frontend" />
@@ -794,7 +800,9 @@
<MESSAGE value="fix : order récéption/expédition + correction style bouton récéption" />
<MESSAGE value="fix : style bon de récéption" />
<MESSAGE value="fix : bouton de mise en attente" />
<option name="LAST_COMMIT_MESSAGE" value="fix : bouton de mise en attente" />
<MESSAGE value="fix : problème de bearer token" />
<MESSAGE value="feat : système de blocage utilisateur" />
<option name="LAST_COMMIT_MESSAGE" value="feat : système de blocage utilisateur" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>

View File

@@ -60,6 +60,7 @@ Ajouter dans le fichier .env du frontend
* [#353] modification front admin client
* [#353] modification front admin utilisateur
* [#FER-11] Corriger le problème de bearer token
* [#FER-12] Ajouter un blocage des utilisateurs
### Changed

View File

@@ -20,6 +20,7 @@ security:
pattern: ^/login_check
stateless: true
provider: app_user_provider
user_checker: App\Security\UserChecker
json_login:
check_path: /login_check
username_path: username
@@ -30,6 +31,7 @@ security:
pattern: ^/
stateless: true
provider: app_user_provider
user_checker: App\Security\UserChecker
jwt: ~
logout:
path: /api/logout

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.0.78'
app.version: '0.0.79'

View File

@@ -41,10 +41,24 @@
type="password"
:disabled="!auth.isAdmin"
wrapper-class="w-[280px]"
required
:required="!userId"
/>
</div>
<div class="flex items-center mb-11">
<label class="flex items-center gap-2 cursor-pointer">
<input
id="user-locked"
v-model="form.isLocked"
type="checkbox"
:disabled="!auth.isAdmin"
class="w-5 h-5 accent-primary-500"
/>
<span class="text-sm text-primary-700">Verrouiller le compte</span>
</label>
<p class="ml-4 text-xs text-slate-400">Un compte verrouillé ne peut plus se connecter.</p>
</div>
<div class="flex items-center justify-center">
<UiButton
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
@@ -86,7 +100,8 @@ const resolveUserId = (param: unknown) => {
const form = reactive<UserFormData>({
username: '',
password: '',
role: ''
role: '',
isLocked: false
})
const hydrateFromUser = (user: UserData | null) => {
@@ -99,6 +114,7 @@ const hydrateFromUser = (user: UserData | null) => {
const hasAdmin = roles.includes('ROLE_ADMIN')
form.role = hasAdmin ? 'ROLE_ADMIN' : 'ROLE_USER'
form.password = ''
form.isLocked = user.isLocked ?? false
isHydrating.value = false
}
@@ -129,6 +145,7 @@ async function validate() {
const basePayload: UserPayload = {
username: normalizedUsername,
roles: normalizedRole ? [normalizedRole] : undefined,
isLocked: form.isLocked,
}
if (normalizedPassword) {
basePayload.password = normalizedPassword

View File

@@ -4,9 +4,10 @@
</div>
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
<div class="grid grid-cols-2 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div class="grid grid-cols-3 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Utilisateur</div>
<div>Role</div>
<div>Statut</div>
</div>
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
Aucun utilisateur.
@@ -15,7 +16,7 @@
<div
v-for="user in userList"
:key="user.id"
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
class="grid grid-cols-3 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
role="button"
tabindex="0"
@click="goToUser(user.id)"
@@ -23,6 +24,16 @@
>
<div>{{ user.username }}</div>
<div>{{ getRoleLabels(user.roles) }}</div>
<div>
<span
v-if="user.isLocked"
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-red-100 text-red-700"
>Verrouillé</span>
<span
v-else
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-green-100 text-green-700"
>Actif</span>
</div>
</div>
</template>
</div>

View File

@@ -2,16 +2,19 @@ export interface UserData {
id: number
username: string
roles: string[]
isLocked: boolean
}
export type UserPayload = {
username?: string
password?: string
roles?: string[]
isLocked?: boolean
}
export type UserFormData = {
username: string
password: string
role: string
isLocked: boolean
}

View File

@@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import type {UserData} from '~/services/dto/user-data'
import {getCurrentUser, createUser, login, logout} from '~/services/auth'
import {getCurrentUser, createUser, updateUser, login, logout} from '~/services/auth'
import type {UserPayload} from "~/services/dto/user-data";
import {ROLE} from '~/utils/constants'
@@ -58,7 +58,7 @@ export const useAuthStore = defineStore('auth', {
},
async updateUser(id: number, payload: UserPayload) {
this.isLoading = true
const result = await createUser(payload).finally(() => {
const result = await updateUser(id, payload).finally(() => {
this.isLoading = false
})
return result

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260325142815 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD is_locked BOOLEAN DEFAULT false NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE public."user" DROP is_locked');
}
}

View File

@@ -9,12 +9,14 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\State\ActiveUsersProvider;
use App\State\MeProvider;
use App\State\UserPasswordProcessor;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName;
#[ORM\Entity]
#[ORM\Table(name: 'user', schema: 'public')]
@@ -45,7 +47,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
),
new GetCollection(
normalizationContext: ['groups' => ['user-login:read']],
security: "is_granted('PUBLIC_ACCESS')"
security: "is_granted('PUBLIC_ACCESS')",
provider: ActiveUsersProvider::class
),
new GetCollection(
uriTemplate: '/admin/users',
@@ -76,6 +79,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[Groups(['user:write'])]
private string $password = '';
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['user:read', 'user:write'])]
#[SerializedName('isLocked')]
private bool $isLocked = false;
public function getId(): ?int
{
return $this->id;
@@ -125,6 +133,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
public function getIsLocked(): bool
{
return $this->isLocked;
}
public function setIsLocked(bool $isLocked): self
{
$this->isLocked = $isLocked;
return $this;
}
public function eraseCredentials(): void
{
// No-op: we don't store temporary sensitive data on the entity.

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user): void
{
if (!$user instanceof User) {
return;
}
if ($user->getIsLocked()) {
throw new CustomUserMessageAccountStatusException('Ce compte est verrouillé.');
}
}
public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void {}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
final readonly class ActiveUsersProvider implements ProviderInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
return $this->em->getRepository(User::class)->findBy(['isLocked' => false]);
}
}