"""Jeton d'accès au formulaire public (lien non devinable) Revision ID: 0019_form_access_token Revises: 0018_pending_submissions_mecenat Create Date: 2026-09-30 `form_template.access_token` : secret partagé désormais nécessaire pour atteindre le formulaire public (`/inscription/`) — voir CLAUDE.md § 2, `FormTemplate`, et `app/api/forms.py:_require_access_token`. `/inscription` seul, sans jeton dans l'URL, ne mène plus nulle part. Rétrocompatibilité : chaque `form_template` déjà en base reçoit un jeton généré aléatoirement — les liens déjà partagés cessent de fonctionner, la coordination doit en repartager un nouveau (« Copier le lien » sur l'écran Inscriptions) — c'est précisément le but du changement. """ import secrets from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "0019_form_access_token" down_revision: str | None = "0018_pending_submissions_mecenat" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.add_column( "form_template", sa.Column("access_token", sa.String(), nullable=True) ) conn = op.get_bind() for (row_id,) in conn.execute(sa.text("SELECT id FROM form_template")): conn.execute( sa.text("UPDATE form_template SET access_token = :token WHERE id = :id"), {"token": secrets.token_urlsafe(16), "id": row_id}, ) op.alter_column("form_template", "access_token", nullable=False) def downgrade() -> None: op.drop_column("form_template", "access_token")