Remplace la regle de longueur seule (12 caracteres) par une exigence de composition (8 caracteres minimum, majuscule, minuscule, chiffre, caractere special), non documentee dans les exigences officielles du projet, par une regle explicite partagee entre le backend (validateur Pydantic) et le frontend. Ajoute un flux "mot de passe oublie" en libre-service, absent jusqu'ici : jeton a usage unique hache en base (meme principe que les refresh tokens), expirant a 15 minutes, envoye par email via un service SMTP (aiosmtplib, Mailpit en dev), avec limitation de debit dediee et reponse generique pour eviter l'enumeration des comptes. Closes #87
41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
# Pourquoi : même schéma que `refresh_token` (chaîne opaque, jamais un JWT) pour la même
|
|
# raison : un jeton de réinitialisation doit être révocable d'un coup, et un JWT ne figure
|
|
# dans aucune ligne à invalider.
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Index, LargeBinary, Text, func
|
|
from sqlalchemy.dialects.postgresql import INET
|
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.db.base import Base
|
|
|
|
|
|
class PasswordResetToken(Base):
|
|
__tablename__ = "password_reset_token"
|
|
__table_args__ = (
|
|
Index("ix_password_reset_token_user", "user_id"),
|
|
Index(
|
|
"ix_password_reset_token_vivants",
|
|
"user_id",
|
|
postgresql_where="consumed_at is null",
|
|
),
|
|
)
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
PG_UUID(as_uuid=True), primary_key=True, server_default=func.gen_random_uuid()
|
|
)
|
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
PG_UUID(as_uuid=True), ForeignKey("app_user.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
token_hash: Mapped[bytes] = mapped_column(LargeBinary, nullable=False, unique=True)
|
|
issued_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
client_ip: Mapped[str | None] = mapped_column(INET, nullable=True)
|
|
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
|