feat(backend): ouvre l'administration des comptes et le changement de mot de passe
Liste, création, changement de rôle, activation, réinitialisation, plus `/auth/password` pour son propre mot de passe. Les schémas de lecture et d'écriture sont séparés : un modèle unique laisserait passer `role` ou `is_active` depuis un corps de requête et renverrait `password_hash` en réponse, soit l'attribution de masse, API3 du top 10 API. Un test envoie ces deux champs et vérifie qu'ils sont ignorés. Le service refuse de rétrograder ou de désactiver le dernier administrateur actif. Sans cette garde, un administrateur peut se verrouiller lui-même dehors et il ne reste que `psql` pour rentrer. Tout changement de rôle ou désactivation révoque les sessions de la cible, et `credentials_changed_at` rend le jeton d'accès encore valide inutilisable dès la requête suivante. La promesse de révocation immédiate ne tient que si les deux sont faits. Le changement de son propre mot de passe révoque toutes les familles puis en rouvre une : l'appareil courant reste connecté, tous les autres sont déconnectés. Il faut le coder explicitement pour l'obtenir. Les mots de passe provisoires sont tirés au sort et affichés une seule fois, sous `Cache-Control: no-store`.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Contrainte : les schémas de lecture et d'écriture sont séparés. Un modèle unique laisserait
|
||||
# passer `role` ou `is_active` depuis un corps de requête, et renverrait `password_hash` en
|
||||
# réponse. C'est l'attribution de masse, API3 du top 10 API.
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
from app.core.roles import AccountKind, Role
|
||||
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
email: EmailStr
|
||||
role: Role
|
||||
full_name: str | None = Field(default=None, max_length=200)
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
role: Role | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: UUID
|
||||
email: str
|
||||
role: Role
|
||||
kind: AccountKind
|
||||
is_active: bool
|
||||
must_change_password: bool
|
||||
full_name: str | None
|
||||
last_login_at: datetime | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class TemporaryPasswordResponse(BaseModel):
|
||||
# Affiché une seule fois : l'empreinte seule est conservée côté serveur.
|
||||
user: UserResponse
|
||||
temporary_password: str
|
||||
Reference in New Issue
Block a user