Table `app_user`, son dépôt, et la commande `create-admin`. Le nom évite `user`, mot réservé de PostgreSQL, et rappelle qu'il s'agit d'un compte applicatif, par opposition au rôle PostgreSQL qui portera le cantonnement des accès ETL et ML. `credentials_changed_at` couvre à elle seule le changement de mot de passe, le changement de rôle et la désactivation : tout jeton émis avant cet instant sera refusé, sans attendre son expiration. La configuration refuse désormais de démarrer sur cinq erreurs silencieuses : secret trop court ou laissé à sa valeur d'exemple, `debug` en production, joker CORS, origines vides hors local, et cookie `SameSite=None` sans `Secure`. Les fixtures de test et les deux `.env.example` suivent, sans quoi rien ne démarrerait. Le mot de passe de l'admin ne transite jamais par `argv`, visible de tout `ps` : il est saisi par `getpass` ou tiré au sort. Une révision Alembic qui insérerait ce compte graverait son empreinte dans Git pour toujours.
73 lines
2.4 KiB
Python
73 lines
2.4 KiB
Python
"""comptes applicatifs
|
|
|
|
Revision ID: b1a7c3d9e240
|
|
Revises: 5353c0e4f094
|
|
Create Date: 2026-09-15 14:40:00.000000
|
|
|
|
Cree `app_user`, la table des comptes humains et de service. Le nom evite `user`,
|
|
mot reserve de PostgreSQL. `gen_random_uuid()` est au coeur de PG17, aucune
|
|
extension n'est necessaire.
|
|
"""
|
|
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "b1a7c3d9e240"
|
|
down_revision: str | Sequence[str] | None = "5353c0e4f094"
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"app_user",
|
|
sa.Column(
|
|
"id",
|
|
postgresql.UUID(as_uuid=True),
|
|
server_default=sa.text("gen_random_uuid()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("email", sa.String(length=320), nullable=False),
|
|
sa.Column("password_hash", sa.Text(), nullable=False),
|
|
sa.Column("role", sa.Text(), nullable=False),
|
|
sa.Column("kind", sa.Text(), server_default=sa.text("'human'"), nullable=False),
|
|
sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), nullable=False),
|
|
sa.Column(
|
|
"must_change_password", sa.Boolean(), server_default=sa.text("false"), nullable=False
|
|
),
|
|
sa.Column(
|
|
"credentials_changed_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("full_name", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.CheckConstraint("email = lower(email)", name="ck_app_user_email_minuscule"),
|
|
sa.CheckConstraint(
|
|
"role in ('lecteur', 'operateur', 'admin')", name="ck_app_user_role"
|
|
),
|
|
sa.CheckConstraint("kind in ('human', 'service')", name="ck_app_user_kind"),
|
|
sa.PrimaryKeyConstraint("id", name="pk_app_user"),
|
|
sa.UniqueConstraint("email", name="uq_app_user_email"),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("app_user")
|