0022_login_lockout.py 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. """登录失败锁定:sys_user 增加连续失败计数与锁定截止时间列。
  2. Revision ID: 0022_login_lockout
  3. Revises: 0021_remove_assigned_scope
  4. Create Date: 2026-09-01
  5. 变更要点:
  6. 1. sys_user 增加 failed_login_count INT NOT NULL DEFAULT 0(连续密码错误计数,
  7. 仅 /auth/login 密码错误计入,登录成功清零);
  8. 2. sys_user 增加 locked_until DATETIME NULL(锁定截止时间,UTC,NULL=未锁定;
  9. 连续5次密码错误锁定15分钟,由服务层 login_lockout 维护);
  10. 3. 升级幂等:列已存在时跳过;降级直接删列(锁定状态为易失运行态,可随时丢弃)。
  11. """
  12. from __future__ import annotations
  13. from collections.abc import Sequence
  14. import sqlalchemy as sa
  15. from alembic import op
  16. revision: str = "0022_login_lockout"
  17. down_revision: str | None = "0021_remove_assigned_scope"
  18. branch_labels: str | Sequence[str] | None = None
  19. depends_on: str | Sequence[str] | None = None
  20. _TABLE = "sys_user"
  21. _COLUMNS = ("failed_login_count", "locked_until")
  22. def _existing_columns() -> set[str]:
  23. inspector = sa.inspect(op.get_bind())
  24. return {column["name"] for column in inspector.get_columns(_TABLE)}
  25. def upgrade() -> None:
  26. existing = _existing_columns()
  27. if "failed_login_count" not in existing:
  28. op.add_column(
  29. _TABLE,
  30. sa.Column(
  31. "failed_login_count",
  32. sa.Integer(),
  33. nullable=False,
  34. server_default=sa.text("0"),
  35. ),
  36. )
  37. if "locked_until" not in existing:
  38. op.add_column(
  39. _TABLE,
  40. sa.Column("locked_until", sa.DateTime(), nullable=True),
  41. )
  42. def downgrade() -> None:
  43. existing = _existing_columns()
  44. if "locked_until" in existing:
  45. op.drop_column(_TABLE, "locked_until")
  46. if "failed_login_count" in existing:
  47. op.drop_column(_TABLE, "failed_login_count")