account-kit 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. account_kit-0.2.2/PKG-INFO +198 -0
  2. account_kit-0.2.2/README.md +153 -0
  3. {account_kit-0.2.1 → account_kit-0.2.2}/pyproject.toml +4 -3
  4. account_kit-0.2.2/src/account_kit/__init__.py +9 -0
  5. account_kit-0.2.2/src/account_kit/admin_router.py +358 -0
  6. account_kit-0.2.2/src/account_kit/audit.py +95 -0
  7. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/captcha.py +50 -18
  8. account_kit-0.2.2/src/account_kit/captcha_image.py +172 -0
  9. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/config.py +71 -0
  10. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/emailer.py +4 -0
  11. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/models.py +73 -0
  12. account_kit-0.2.2/src/account_kit/otp.py +212 -0
  13. account_kit-0.2.2/src/account_kit/router.py +648 -0
  14. account_kit-0.2.2/src/account_kit/schema_setup.py +35 -0
  15. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/schemas.py +37 -0
  16. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/service.py +196 -19
  17. account_kit-0.2.2/src/account_kit/sql/upgrade_0_2_2.sql +68 -0
  18. account_kit-0.2.2/src/account_kit/state.py +174 -0
  19. account_kit-0.2.2/src/account_kit/tokens.py +117 -0
  20. account_kit-0.2.2/src/account_kit/two_factor/challenges.py +210 -0
  21. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/two_factor/router.py +61 -13
  22. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/two_factor/service.py +35 -11
  23. account_kit-0.2.2/src/account_kit.egg-info/PKG-INFO +198 -0
  24. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit.egg-info/SOURCES.txt +10 -0
  25. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit.egg-info/requires.txt +4 -0
  26. {account_kit-0.2.1 → account_kit-0.2.2}/tests/conftest.py +6 -1
  27. account_kit-0.2.2/tests/helpers.py +36 -0
  28. account_kit-0.2.2/tests/test_change_email.py +96 -0
  29. account_kit-0.2.2/tests/test_delete_audit_captcha.py +269 -0
  30. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_email_2fa_captcha.py +20 -3
  31. account_kit-0.2.2/tests/test_otp_limits.py +168 -0
  32. account_kit-0.2.2/tests/test_refresh_logout.py +165 -0
  33. account_kit-0.2.2/tests/test_schema.py +68 -0
  34. account_kit-0.2.1/PKG-INFO +0 -126
  35. account_kit-0.2.1/README.md +0 -84
  36. account_kit-0.2.1/src/account_kit/__init__.py +0 -9
  37. account_kit-0.2.1/src/account_kit/admin_router.py +0 -221
  38. account_kit-0.2.1/src/account_kit/otp.py +0 -99
  39. account_kit-0.2.1/src/account_kit/router.py +0 -256
  40. account_kit-0.2.1/src/account_kit/schema_setup.py +0 -11
  41. account_kit-0.2.1/src/account_kit/two_factor/challenges.py +0 -87
  42. account_kit-0.2.1/src/account_kit.egg-info/PKG-INFO +0 -126
  43. account_kit-0.2.1/tests/test_schema.py +0 -25
  44. {account_kit-0.2.1 → account_kit-0.2.2}/LICENSE +0 -0
  45. {account_kit-0.2.1 → account_kit-0.2.2}/MANIFEST.in +0 -0
  46. {account_kit-0.2.1 → account_kit-0.2.2}/setup.cfg +0 -0
  47. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/admin_schemas.py +0 -0
  48. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/avatar.py +0 -0
  49. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/catalog.py +0 -0
  50. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/crypto.py +0 -0
  51. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/db.py +0 -0
  52. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/deps.py +0 -0
  53. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/profile.py +0 -0
  54. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/security.py +0 -0
  55. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/seed.py +0 -0
  56. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/two_factor_disable_en.html +0 -0
  57. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/two_factor_disable_zh.html +0 -0
  58. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/two_factor_login_en.html +0 -0
  59. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/two_factor_login_zh.html +0 -0
  60. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/verification_en.html +0 -0
  61. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/templates/emails/verification_zh.html +0 -0
  62. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/two_factor/__init__.py +0 -0
  63. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit/two_factor/totp.py +0 -0
  64. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit.egg-info/dependency_links.txt +0 -0
  65. {account_kit-0.2.1 → account_kit-0.2.2}/src/account_kit.egg-info/top_level.txt +0 -0
  66. {account_kit-0.2.1 → account_kit-0.2.2}/tests/__init__.py +0 -0
  67. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_auth.py +0 -0
  68. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_avatar.py +0 -0
  69. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_change_password.py +0 -0
  70. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_role_change_me.py +0 -0
  71. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_templates.py +0 -0
  72. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_two_factor.py +0 -0
  73. {account_kit-0.2.1 → account_kit-0.2.2}/tests/test_user_response_birth.py +0 -0
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: account-kit
3
+ Version: 0.2.2
4
+ Summary: Shared async account service for FastAPI apps
5
+ Author: kqstone
6
+ License-Expression: MIT
7
+ Keywords: fastapi,authentication,account,jwt,totp,2fa,sqlalchemy,asyncpg,postgresql
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: Chinese (Simplified)
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: fastapi>=0.110
28
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
29
+ Requires-Dist: asyncpg>=0.29
30
+ Requires-Dist: pydantic[email]>=2.0
31
+ Requires-Dist: python-jose[cryptography]>=3.3
32
+ Requires-Dist: bcrypt>=4.0
33
+ Requires-Dist: python-multipart
34
+ Requires-Dist: jinja2>=3.1
35
+ Requires-Dist: pyotp>=2.9
36
+ Requires-Dist: cryptography
37
+ Provides-Extra: captcha
38
+ Requires-Dist: Pillow>=9.2; extra == "captcha"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=8; extra == "dev"
41
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
42
+ Requires-Dist: httpx>=0.27; extra == "dev"
43
+ Requires-Dist: Pillow>=9.2; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # account-kit
47
+
48
+ dedd-online 与 dental-case-pro 共用的异步账号包。PostgreSQL + SQLAlchemy + asyncpg,表在 `auth` schema,用户主键是 UUID。
49
+
50
+ 账号能力:注册、登录、邮件验证码、审批、单设备会话、自定义角色、后台权限(`is_admin`,不是角色)、等级目录、两步验证。限额、限流、积分留在宿主。
51
+
52
+ ```python
53
+ from account_kit import init_db, mount_account, seed_defaults
54
+ from account_kit.config import AccountKitConfig
55
+
56
+ await init_db(engine)
57
+ await seed_defaults(session)
58
+ mount_account(app, get_db, AccountKitConfig(jwt_secret="..."))
59
+ ```
60
+
61
+ ### 可选:邮箱两步验证、登录图形验证码
62
+
63
+ 两项默认关闭,由宿主在 `AccountKitConfig` 里打开:
64
+
65
+ - `two_factor_email_enabled=True`:两步验证可用邮箱验证码代替验证器("跳过 2FA")。登录时 `POST /login/2fa/email/send`(`challenge_token`、`language`)发码,再把 `email_code` 提交到 `POST /login/2fa`;设置里 `POST /2fa/disable/email-code` 发码,`POST /2fa/disable` 带 `email_code` 关闭。`MFA_REQUIRED` 的 `detail` 里 `email_available` 为真,`methods` 含 `email`。
66
+ - `captcha_verifier=fn(captcha_id, captcha_code) -> bool`(可为 async):同一 IP 或用户名在 `captcha_fail_window_seconds` 内失败 `captcha_fail_threshold` 次后,`POST /login` 必须带 `captcha_id`、`captcha_code`,否则 428 `CAPTCHA_REQUIRED`,错了 400 `CAPTCHA_INVALID`;密码错误返回 401 `INVALID_CREDENTIALS`(带 `captcha_required`)。验证码图片由宿主提供(界面包从 `GET {api_prefix}/captcha` 取 `captcha_id`、`image_base64`)。反向代理后用 `client_ip=fn(request)` 取真实 IP。
67
+ - `before_two_factor=async fn(request, user)`:每次校验第二因素、发邮箱码前调用,宿主可在这里限流。
68
+ - `two_factor_challenge_ttl_seconds`(默认 300)、`two_factor_max_attempts`(默认 5):登录两步验证 challenge 的有效期和每个 challenge 允许输错的次数。每次请求时读取,宿主可在 `get_db` 里按后台设置刷新。
69
+
70
+ `init_db` 和直接调用 `Base.metadata.create_all` 都会先建 `auth` schema(`CREATE SCHEMA IF NOT EXISTS`),空库可直接启动。React 与 Vue 的 `LoginForm` 在服务端要求时都会显示图形验证码。
71
+
72
+ ### 0.2.0:邮件模板覆盖、改密、头像、角色申请查询
73
+
74
+ - **2FA 邮件模板**:`login_2fa` / `disable_2fa` 默认使用包内 `two_factor_login_{zh,en}.html`、`two_factor_disable_{zh,en}.html`(警示文案,`{{ brand }}` / `{{ code }}`)。其它 purpose 仍用 `verification_{lang}.html`(`{{ action }}` `{{ code }}` `{{ brand }}`)。
75
+ - **宿主覆盖模板**:设 `email_template_dir` 后,Jinja 先从该目录按同名加载,找不到再回落包内默认。`email_template_map` 可把 purpose 指到相对文件名(支持 `{lang}`);拒绝 `..` 与绝对路径。完全接管发送仍可用 `mailer`。
76
+ - **`POST {api_prefix}/change-password`**:body `{ "old_password", "new_password" }`,默认**不要求**邮箱验证码(兼容 dedd)。成功后吊销受信设备、丢掉未完成的 2FA challenge;`session_mode==single_device` 时清会话。响应 `{ "status": "success", "detail": "密码修改成功" }`。dental 若要更严,设 `change_password_require_email_code=True`(可再带 `code`)。`PATCH /me` 改密仍走邮箱码,两条路径语义不同。
77
+ - **可选头像**(默认关):`avatar_enabled=True` 并注入 `avatar_save` / `avatar_delete` / `avatar_open`。端点:`POST/DELETE /me/avatar`、`GET /users/{id}/avatar`。存储与图片处理由宿主 callback 完成(Pillow **不是** kit 依赖)。漏配 `avatar_save` 或未启用时返回 501。未提供 `avatar_open` 时,仅当 `avatar_path` 是本地已存在文件才直接读取。
78
+ - **`GET {api_prefix}/role-change-requests/me`**:当前用户 pending 申请,没有则 `null`(需 `role_change_enabled`)。
79
+
80
+ ### 0.2.2:安全加固、刷新令牌、登出、自助注销、审计日志
81
+
82
+ **验证码防爆破与发送限流**
83
+
84
+ - `verify_code_max_attempts`(默认 5):同一邮箱、同一用途输错达到次数后,该邮箱该用途所有未用验证码作废,返回 400 `验证码错误次数过多,请重新获取验证码`,并记审计 `verification_code_locked`。注册、找回密码、改密、邮箱两步验证、修改邮箱、注销账号都受此限制。
85
+ - 发码限流(`send-code`、2FA 邮箱码、修改邮箱、注销账号共用):`send_code_rate_limit_ip`(10)、`send_code_rate_limit_email`(5),窗口 `send_code_rate_window_seconds`(3600)。名称和默认值沿用 dedd 原来的 `auth_send_code_*`。
86
+ - 其它可选限流(0 = 关):`reset_password_rate_limit_ip`(10)/ `reset_password_rate_window_seconds`(3600)、`register_rate_limit_ip`(0)/ `register_rate_window_seconds`(3600)、`login_rate_limit_ip`(0)、`login_rate_limit_user`(0)、`login_rate_window_seconds`(60)。
87
+ - 超限统一返回 429,`detail={"code":"RATE_LIMITED","message":...,"retry_after":秒}`,带 `Retry-After` 头。
88
+ - 钩子:`before_send_code=async fn(request, email, purpose)`,每次发验证码邮件前调用;`before_action=async fn(request, action, info)`,在 `send_code`、`verify_code`、`register`、`reset_password`、`change_password`、`change_email`、`refresh`、`logout`、`delete_account`、`captcha` 前调用。抛 `HTTPException` 即拒绝。
89
+
90
+ **状态存储**:`state_backend="db"`(默认)把登录失败计数、限流计数、2FA 登录 challenge、内置图形验证码放进 `auth` schema 的表,多进程、多实例共享,重启不丢;`state_backend="memory"` 保持 0.2.1 的单进程内存行为。
91
+
92
+ **修改邮箱需验证**
93
+
94
+ - `POST /me/email/send-code`,body `{new_email, password?, language}`:把验证码发到新邮箱。新邮箱已被占用时返回 400;同一用户 60 秒内重复发送返回 429 `EMAIL_CODE_TOO_FREQUENT`。
95
+ - `POST /me/email`,body `{new_email, code, password?}`:验证码与当前用户绑定,别人的码不能用。`change_email_require_password=True`(默认)时必须带密码。成功后记审计 `email_changed`。
96
+ - `PATCH /me` 里的 `email` 由 `profile_email_change` 控制:
97
+ - `verify`(默认):要带 `email_code`(以及 `current_password`),否则 400 `EMAIL_VERIFICATION_REQUIRED`;
98
+ - `reject`:400 `EMAIL_CHANGE_VIA_ENDPOINT`;
99
+ - `direct`:0.2.1 行为,不验证,不推荐。
100
+
101
+ **刷新令牌**(`refresh_token_enabled=False`,默认关)
102
+
103
+ - 打开后,`/login` 和 `/login/2fa` 额外返回 `refresh_token`、`refresh_expires_in`(`refresh_token_expire_days` 默认 30)。关闭时响应字段与 0.2.1 完全一致。
104
+ - `POST /refresh` `{refresh_token}`:每次轮换。库里只存 SHA-256 哈希,按 family 组织。错误码:`REFRESH_INVALID`、`REFRESH_EXPIRED`、`SESSION_REPLACED`、`REFRESH_REUSED`。重用旧 token 时吊销整个 family 并清掉 single_device 会话,所以客户端要单飞刷新(界面包已经这样做)。
105
+ - 改密、重置密码、关闭或重置 2FA、停用账号、撤销审批、改角色或改 `is_admin`、注销账号都会吊销刷新令牌;single_device 模式下新登录会吊销其它设备的令牌。
106
+
107
+ **登出**:`POST {api_prefix}{logout_path}`(`logout_enabled=True`,`logout_path="/logout"`)。
108
+
109
+ - body 可空,也可以是 JSON 或表单 `{refresh_token?, all_devices?}`,总是返回 `{"status":"success"}`。
110
+ - access token 过期或会话已被顶替时也返回 200,只在 token 的 `sid` 与当前会话一致时才清会话。吊销该会话的刷新令牌;`all_devices` 吊销全部。
111
+ - 记审计 `logout`,回调 `on_logout`。
112
+
113
+ **自助注销**(`self_delete_enabled=False`,默认关)
114
+
115
+ - `POST /me/delete`(或 `DELETE /me`),body `{password, code?, recovery_code?, email_code?}`。开了 2FA 的账号必须再给一个因素,否则 400 `MFA_CODE_REQUIRED`;用邮箱码时先调 `POST /me/delete/email-code`。管理员不能自助注销,返回 403 `ADMIN_SELF_DELETE_FORBIDDEN`。
116
+ - `user_delete_mode` 同时作用于后台删除和自助注销:
117
+ - `"hard"`(默认):删行,与 0.2.1 相同;
118
+ - `"soft"`:停用账号,用户名和邮箱改成墓碑值,清空个人资料,吊销所有凭据,清掉 2FA 和头像,保留行以满足外键。
119
+ - 两种模式都会先调用 `on_deleted`。
120
+
121
+ **审计日志**(`audit_log_enabled=True`)
122
+
123
+ - 写入 `auth.auth_audit_logs`,使用独立事务,业务回滚不影响审计。每写一条回调一次 `on_audit(db, event, user_id, meta)`。
124
+ - 事件:`login_success`、`login_failed`、`login_2fa_failed`、`2fa_enabled`、`2fa_disabled`、`2fa_reset`、`2fa_recovery_regenerated`、`password_changed`、`password_reset`、`email_changed`、`logout`、`refresh_reuse_detected`、`account_deleted`、`verification_code_locked`、`admin_user_updated`(含修改前后)、`admin_user_deleted`、`admin_role_changed`、`admin_tier_changed`、`role_change_reviewed`。
125
+ - 后台查询:`GET {admin_prefix}/audit-logs?page=&page_size=(≤200)&user_id=&event=a,b&ip=&since=&until=`,返回 `{items,total,page,page_size}`。
126
+
127
+ **内置图形验证码**(`captcha_builtin=False`,默认关,需要 `pip install "account-kit[captcha]"`)
128
+
129
+ - 打开后 kit 提供 `GET {api_prefix}/captcha`,返回 `{captcha_id, image_base64, expires_in}`,并自行校验。答案以 HMAC 哈希保存,只能用一次。
130
+ - 相关配置:`captcha_ttl_seconds`(300)、`captcha_length`(4)。
131
+ - 宿主配置了 `captcha_verifier` 时优先用宿主的。宿主自己的 `/captcha` 路由不受影响,因为未开启时 kit 不注册这个路由。
132
+
133
+ **数据库变更**:新增 4 张表 `auth.rate_limit_counters`、`auth.two_factor_challenges`、`auth.refresh_tokens`、`auth.captcha_challenges`,已有表的列不变。
134
+
135
+ - 用 `init_db` 或 `Base.metadata.create_all` 的宿主会自动建表。
136
+ - 没有 Alembic 的宿主可以在启动时调用 `ensure_schema(engine)` / `ensure_schema_sync(engine)`,它们会幂等地补建缺失的表。
137
+ - 用 Alembic 的宿主执行或照抄 `src/account_kit/sql/upgrade_0_2_2.sql`(随包安装,可用 `account_kit.schema_setup.upgrade_sql("0.2.2")` 读出)。
138
+
139
+ **从 0.2.1 升级注意**
140
+
141
+ - `PATCH /me` 直接改邮箱会被拒(默认 `verify`)。前端要改用 `ChangeEmailForm` 或 `/me/email` 流程;过渡期可以设 `profile_email_change="direct"`。dental 资料页目前在 PATCH 里带 `email`,没有验证码,不改的话整次保存都会 400。
142
+ - 默认 `state_backend="db"` 后,`login_fail_tracker.clear_all()` 不再清计数。测试里改用 `state_backend="memory"`,或清空 `auth.rate_limit_counters`。
143
+ - kit 现在自己注册 `/logout`。宿主在 kit 之后挂同路径的路由会被 kit 的遮住;kit 的行为是 dedd 原逻辑的超集。不想要的话设 `logout_enabled=False`,或改 `logout_path`。
144
+ - 下面这些 API 改成了 async,并带 `db` 参数:`mfa_required(db, ...)`、`captcha.*`,以及 2FA challenge 的 `create_challenge` / `load_challenge` / `save_challenge` / `discard_challenge`。`challenge_store` 仍可导入,调用它不会出错,但不再起作用。`admin_reset(db, user_id)` 签名不变。
145
+ - 中英混杂的个别错误文案(如 `Incorrect username or password`)没有改,因为客户端和登录失败计数靠它们匹配。
146
+
147
+ ## 安装
148
+
149
+ Python 包从 [PyPI](https://pypi.org/project/account-kit/) 安装:
150
+
151
+ ```bash
152
+ pip install account-kit
153
+ pip install "account-kit[captcha]" # 需要内置图形验证码时(Pillow)
154
+ ```
155
+
156
+ 界面包发在 GitHub Packages(不是 npmjs.com)。安装前在项目根写 `.npmrc`:
157
+
158
+ ```
159
+ @kqstone:registry=https://npm.pkg.github.com
160
+ //npm.pkg.github.com/:_authToken=${NPM_TOKEN}
161
+ ```
162
+
163
+ `NPM_TOKEN` 需要带 `read:packages` 的 classic PAT(或等价权限)。CI 里可以用 `GITHUB_TOKEN`,但要先在包的 **Manage Actions access** 里给消费仓库(如 `kqstone/dedd-online`、`kqstone/dental-case-pro`)Read 权限。从 private 仓库首次发布的包默认为 private;可在包设置里改成 public,但无论可见性如何,安装都仍需要 token。
164
+
165
+ ```bash
166
+ npm install @kqstone/account-ui-vue # Vue 3
167
+ npm install @kqstone/account-ui-react # React 18 / 19
168
+ ```
169
+
170
+ 不单独发布 client 包,请求客户端打进这两个界面包:
171
+
172
+ - `@kqstone/account-ui-vue` / `@kqstone/account-ui-react`:`LoginForm`、`RegisterForm`、`ResetPasswordForm`、`TwoFactorSettings`、`TwoFactorLoginDialog`、`AvatarUploader`、`UserAvatar`、`ProfileFields`(`passwordEmailCode` 支持改密邮箱验证码)、`ChangeEmailForm`、`DeleteAccountForm`、`LogoutButton`、`CaptchaImage`、`TierBadge`、`createAccountClient`、`createTokenStore`
173
+ - `createAccountClient(base, { getToken, logoutPath, autoRefresh: { getRefreshToken, onTokens, onRefreshFailed } })`:登录后收到 401 时单飞刷新一次再重试(需服务端 `refresh_token_enabled`);新增 `refresh`、`logout`、`sendChangeEmailCode`、`changeEmail`、`deleteAccount`、`sendDeleteAccountEmailCode`;`AccountApiError.code` 取 `detail.code`
174
+
175
+ 界面包发布的是构建后的 `dist/`(ESM + `.d.ts`),样式在组件首次渲染时注入,不需要单独引入 CSS。2FA 设置页的二维码使用可选 peer `qrcode`,或由宿主传入 `renderQr(otpauthUri) => dataUrl`。文案内置中英,可用 `labels` 覆盖;Vue 不绑 vue-i18n,React 不依赖 antd。
176
+
177
+ ## 本地开发
178
+
179
+ 宿主仓库用 `file:` 路径引用界面包时,先在 account-kit 里构建一次(`npm ci` 会通过 `prepare` 自动构建):
180
+
181
+ ```bash
182
+ cd packages/account-ui-vue && npm ci # 或 packages/account-ui-react
183
+ ```
184
+
185
+ 改了界面包源码后,重新 `npm run build`,再在宿主里重新安装依赖。
186
+
187
+ ## 发布
188
+
189
+ 版本号写在 `pyproject.toml`、`src/account_kit/__init__.py` 的 `__version__` 和两个 `packages/*/package.json` 里,发布前三处保持一致。推送 `v<版本号>` tag(如 `v0.2.0`)后,GitHub Action 先校验 tag 与三处版本一致、跑测试和构建,然后:
190
+
191
+ - Python 包发布到 [PyPI](https://pypi.org/project/account-kit/)(Trusted Publishing,不用 token)
192
+ - 两个界面包发布到 [GitHub Packages](https://github.com/kqstone?tab=packages)(`GITHUB_TOKEN`,`packages: write`)
193
+
194
+ 日后若改发 npmjs.com:把 `publishConfig.registry` 改回默认、workflow 改为 `registry.npmjs.org` + `NPM_TOKEN`(或 npm trusted publishing),并更新本节安装说明。
195
+
196
+ ## 许可证
197
+
198
+ MIT
@@ -0,0 +1,153 @@
1
+ # account-kit
2
+
3
+ dedd-online 与 dental-case-pro 共用的异步账号包。PostgreSQL + SQLAlchemy + asyncpg,表在 `auth` schema,用户主键是 UUID。
4
+
5
+ 账号能力:注册、登录、邮件验证码、审批、单设备会话、自定义角色、后台权限(`is_admin`,不是角色)、等级目录、两步验证。限额、限流、积分留在宿主。
6
+
7
+ ```python
8
+ from account_kit import init_db, mount_account, seed_defaults
9
+ from account_kit.config import AccountKitConfig
10
+
11
+ await init_db(engine)
12
+ await seed_defaults(session)
13
+ mount_account(app, get_db, AccountKitConfig(jwt_secret="..."))
14
+ ```
15
+
16
+ ### 可选:邮箱两步验证、登录图形验证码
17
+
18
+ 两项默认关闭,由宿主在 `AccountKitConfig` 里打开:
19
+
20
+ - `two_factor_email_enabled=True`:两步验证可用邮箱验证码代替验证器("跳过 2FA")。登录时 `POST /login/2fa/email/send`(`challenge_token`、`language`)发码,再把 `email_code` 提交到 `POST /login/2fa`;设置里 `POST /2fa/disable/email-code` 发码,`POST /2fa/disable` 带 `email_code` 关闭。`MFA_REQUIRED` 的 `detail` 里 `email_available` 为真,`methods` 含 `email`。
21
+ - `captcha_verifier=fn(captcha_id, captcha_code) -> bool`(可为 async):同一 IP 或用户名在 `captcha_fail_window_seconds` 内失败 `captcha_fail_threshold` 次后,`POST /login` 必须带 `captcha_id`、`captcha_code`,否则 428 `CAPTCHA_REQUIRED`,错了 400 `CAPTCHA_INVALID`;密码错误返回 401 `INVALID_CREDENTIALS`(带 `captcha_required`)。验证码图片由宿主提供(界面包从 `GET {api_prefix}/captcha` 取 `captcha_id`、`image_base64`)。反向代理后用 `client_ip=fn(request)` 取真实 IP。
22
+ - `before_two_factor=async fn(request, user)`:每次校验第二因素、发邮箱码前调用,宿主可在这里限流。
23
+ - `two_factor_challenge_ttl_seconds`(默认 300)、`two_factor_max_attempts`(默认 5):登录两步验证 challenge 的有效期和每个 challenge 允许输错的次数。每次请求时读取,宿主可在 `get_db` 里按后台设置刷新。
24
+
25
+ `init_db` 和直接调用 `Base.metadata.create_all` 都会先建 `auth` schema(`CREATE SCHEMA IF NOT EXISTS`),空库可直接启动。React 与 Vue 的 `LoginForm` 在服务端要求时都会显示图形验证码。
26
+
27
+ ### 0.2.0:邮件模板覆盖、改密、头像、角色申请查询
28
+
29
+ - **2FA 邮件模板**:`login_2fa` / `disable_2fa` 默认使用包内 `two_factor_login_{zh,en}.html`、`two_factor_disable_{zh,en}.html`(警示文案,`{{ brand }}` / `{{ code }}`)。其它 purpose 仍用 `verification_{lang}.html`(`{{ action }}` `{{ code }}` `{{ brand }}`)。
30
+ - **宿主覆盖模板**:设 `email_template_dir` 后,Jinja 先从该目录按同名加载,找不到再回落包内默认。`email_template_map` 可把 purpose 指到相对文件名(支持 `{lang}`);拒绝 `..` 与绝对路径。完全接管发送仍可用 `mailer`。
31
+ - **`POST {api_prefix}/change-password`**:body `{ "old_password", "new_password" }`,默认**不要求**邮箱验证码(兼容 dedd)。成功后吊销受信设备、丢掉未完成的 2FA challenge;`session_mode==single_device` 时清会话。响应 `{ "status": "success", "detail": "密码修改成功" }`。dental 若要更严,设 `change_password_require_email_code=True`(可再带 `code`)。`PATCH /me` 改密仍走邮箱码,两条路径语义不同。
32
+ - **可选头像**(默认关):`avatar_enabled=True` 并注入 `avatar_save` / `avatar_delete` / `avatar_open`。端点:`POST/DELETE /me/avatar`、`GET /users/{id}/avatar`。存储与图片处理由宿主 callback 完成(Pillow **不是** kit 依赖)。漏配 `avatar_save` 或未启用时返回 501。未提供 `avatar_open` 时,仅当 `avatar_path` 是本地已存在文件才直接读取。
33
+ - **`GET {api_prefix}/role-change-requests/me`**:当前用户 pending 申请,没有则 `null`(需 `role_change_enabled`)。
34
+
35
+ ### 0.2.2:安全加固、刷新令牌、登出、自助注销、审计日志
36
+
37
+ **验证码防爆破与发送限流**
38
+
39
+ - `verify_code_max_attempts`(默认 5):同一邮箱、同一用途输错达到次数后,该邮箱该用途所有未用验证码作废,返回 400 `验证码错误次数过多,请重新获取验证码`,并记审计 `verification_code_locked`。注册、找回密码、改密、邮箱两步验证、修改邮箱、注销账号都受此限制。
40
+ - 发码限流(`send-code`、2FA 邮箱码、修改邮箱、注销账号共用):`send_code_rate_limit_ip`(10)、`send_code_rate_limit_email`(5),窗口 `send_code_rate_window_seconds`(3600)。名称和默认值沿用 dedd 原来的 `auth_send_code_*`。
41
+ - 其它可选限流(0 = 关):`reset_password_rate_limit_ip`(10)/ `reset_password_rate_window_seconds`(3600)、`register_rate_limit_ip`(0)/ `register_rate_window_seconds`(3600)、`login_rate_limit_ip`(0)、`login_rate_limit_user`(0)、`login_rate_window_seconds`(60)。
42
+ - 超限统一返回 429,`detail={"code":"RATE_LIMITED","message":...,"retry_after":秒}`,带 `Retry-After` 头。
43
+ - 钩子:`before_send_code=async fn(request, email, purpose)`,每次发验证码邮件前调用;`before_action=async fn(request, action, info)`,在 `send_code`、`verify_code`、`register`、`reset_password`、`change_password`、`change_email`、`refresh`、`logout`、`delete_account`、`captcha` 前调用。抛 `HTTPException` 即拒绝。
44
+
45
+ **状态存储**:`state_backend="db"`(默认)把登录失败计数、限流计数、2FA 登录 challenge、内置图形验证码放进 `auth` schema 的表,多进程、多实例共享,重启不丢;`state_backend="memory"` 保持 0.2.1 的单进程内存行为。
46
+
47
+ **修改邮箱需验证**
48
+
49
+ - `POST /me/email/send-code`,body `{new_email, password?, language}`:把验证码发到新邮箱。新邮箱已被占用时返回 400;同一用户 60 秒内重复发送返回 429 `EMAIL_CODE_TOO_FREQUENT`。
50
+ - `POST /me/email`,body `{new_email, code, password?}`:验证码与当前用户绑定,别人的码不能用。`change_email_require_password=True`(默认)时必须带密码。成功后记审计 `email_changed`。
51
+ - `PATCH /me` 里的 `email` 由 `profile_email_change` 控制:
52
+ - `verify`(默认):要带 `email_code`(以及 `current_password`),否则 400 `EMAIL_VERIFICATION_REQUIRED`;
53
+ - `reject`:400 `EMAIL_CHANGE_VIA_ENDPOINT`;
54
+ - `direct`:0.2.1 行为,不验证,不推荐。
55
+
56
+ **刷新令牌**(`refresh_token_enabled=False`,默认关)
57
+
58
+ - 打开后,`/login` 和 `/login/2fa` 额外返回 `refresh_token`、`refresh_expires_in`(`refresh_token_expire_days` 默认 30)。关闭时响应字段与 0.2.1 完全一致。
59
+ - `POST /refresh` `{refresh_token}`:每次轮换。库里只存 SHA-256 哈希,按 family 组织。错误码:`REFRESH_INVALID`、`REFRESH_EXPIRED`、`SESSION_REPLACED`、`REFRESH_REUSED`。重用旧 token 时吊销整个 family 并清掉 single_device 会话,所以客户端要单飞刷新(界面包已经这样做)。
60
+ - 改密、重置密码、关闭或重置 2FA、停用账号、撤销审批、改角色或改 `is_admin`、注销账号都会吊销刷新令牌;single_device 模式下新登录会吊销其它设备的令牌。
61
+
62
+ **登出**:`POST {api_prefix}{logout_path}`(`logout_enabled=True`,`logout_path="/logout"`)。
63
+
64
+ - body 可空,也可以是 JSON 或表单 `{refresh_token?, all_devices?}`,总是返回 `{"status":"success"}`。
65
+ - access token 过期或会话已被顶替时也返回 200,只在 token 的 `sid` 与当前会话一致时才清会话。吊销该会话的刷新令牌;`all_devices` 吊销全部。
66
+ - 记审计 `logout`,回调 `on_logout`。
67
+
68
+ **自助注销**(`self_delete_enabled=False`,默认关)
69
+
70
+ - `POST /me/delete`(或 `DELETE /me`),body `{password, code?, recovery_code?, email_code?}`。开了 2FA 的账号必须再给一个因素,否则 400 `MFA_CODE_REQUIRED`;用邮箱码时先调 `POST /me/delete/email-code`。管理员不能自助注销,返回 403 `ADMIN_SELF_DELETE_FORBIDDEN`。
71
+ - `user_delete_mode` 同时作用于后台删除和自助注销:
72
+ - `"hard"`(默认):删行,与 0.2.1 相同;
73
+ - `"soft"`:停用账号,用户名和邮箱改成墓碑值,清空个人资料,吊销所有凭据,清掉 2FA 和头像,保留行以满足外键。
74
+ - 两种模式都会先调用 `on_deleted`。
75
+
76
+ **审计日志**(`audit_log_enabled=True`)
77
+
78
+ - 写入 `auth.auth_audit_logs`,使用独立事务,业务回滚不影响审计。每写一条回调一次 `on_audit(db, event, user_id, meta)`。
79
+ - 事件:`login_success`、`login_failed`、`login_2fa_failed`、`2fa_enabled`、`2fa_disabled`、`2fa_reset`、`2fa_recovery_regenerated`、`password_changed`、`password_reset`、`email_changed`、`logout`、`refresh_reuse_detected`、`account_deleted`、`verification_code_locked`、`admin_user_updated`(含修改前后)、`admin_user_deleted`、`admin_role_changed`、`admin_tier_changed`、`role_change_reviewed`。
80
+ - 后台查询:`GET {admin_prefix}/audit-logs?page=&page_size=(≤200)&user_id=&event=a,b&ip=&since=&until=`,返回 `{items,total,page,page_size}`。
81
+
82
+ **内置图形验证码**(`captcha_builtin=False`,默认关,需要 `pip install "account-kit[captcha]"`)
83
+
84
+ - 打开后 kit 提供 `GET {api_prefix}/captcha`,返回 `{captcha_id, image_base64, expires_in}`,并自行校验。答案以 HMAC 哈希保存,只能用一次。
85
+ - 相关配置:`captcha_ttl_seconds`(300)、`captcha_length`(4)。
86
+ - 宿主配置了 `captcha_verifier` 时优先用宿主的。宿主自己的 `/captcha` 路由不受影响,因为未开启时 kit 不注册这个路由。
87
+
88
+ **数据库变更**:新增 4 张表 `auth.rate_limit_counters`、`auth.two_factor_challenges`、`auth.refresh_tokens`、`auth.captcha_challenges`,已有表的列不变。
89
+
90
+ - 用 `init_db` 或 `Base.metadata.create_all` 的宿主会自动建表。
91
+ - 没有 Alembic 的宿主可以在启动时调用 `ensure_schema(engine)` / `ensure_schema_sync(engine)`,它们会幂等地补建缺失的表。
92
+ - 用 Alembic 的宿主执行或照抄 `src/account_kit/sql/upgrade_0_2_2.sql`(随包安装,可用 `account_kit.schema_setup.upgrade_sql("0.2.2")` 读出)。
93
+
94
+ **从 0.2.1 升级注意**
95
+
96
+ - `PATCH /me` 直接改邮箱会被拒(默认 `verify`)。前端要改用 `ChangeEmailForm` 或 `/me/email` 流程;过渡期可以设 `profile_email_change="direct"`。dental 资料页目前在 PATCH 里带 `email`,没有验证码,不改的话整次保存都会 400。
97
+ - 默认 `state_backend="db"` 后,`login_fail_tracker.clear_all()` 不再清计数。测试里改用 `state_backend="memory"`,或清空 `auth.rate_limit_counters`。
98
+ - kit 现在自己注册 `/logout`。宿主在 kit 之后挂同路径的路由会被 kit 的遮住;kit 的行为是 dedd 原逻辑的超集。不想要的话设 `logout_enabled=False`,或改 `logout_path`。
99
+ - 下面这些 API 改成了 async,并带 `db` 参数:`mfa_required(db, ...)`、`captcha.*`,以及 2FA challenge 的 `create_challenge` / `load_challenge` / `save_challenge` / `discard_challenge`。`challenge_store` 仍可导入,调用它不会出错,但不再起作用。`admin_reset(db, user_id)` 签名不变。
100
+ - 中英混杂的个别错误文案(如 `Incorrect username or password`)没有改,因为客户端和登录失败计数靠它们匹配。
101
+
102
+ ## 安装
103
+
104
+ Python 包从 [PyPI](https://pypi.org/project/account-kit/) 安装:
105
+
106
+ ```bash
107
+ pip install account-kit
108
+ pip install "account-kit[captcha]" # 需要内置图形验证码时(Pillow)
109
+ ```
110
+
111
+ 界面包发在 GitHub Packages(不是 npmjs.com)。安装前在项目根写 `.npmrc`:
112
+
113
+ ```
114
+ @kqstone:registry=https://npm.pkg.github.com
115
+ //npm.pkg.github.com/:_authToken=${NPM_TOKEN}
116
+ ```
117
+
118
+ `NPM_TOKEN` 需要带 `read:packages` 的 classic PAT(或等价权限)。CI 里可以用 `GITHUB_TOKEN`,但要先在包的 **Manage Actions access** 里给消费仓库(如 `kqstone/dedd-online`、`kqstone/dental-case-pro`)Read 权限。从 private 仓库首次发布的包默认为 private;可在包设置里改成 public,但无论可见性如何,安装都仍需要 token。
119
+
120
+ ```bash
121
+ npm install @kqstone/account-ui-vue # Vue 3
122
+ npm install @kqstone/account-ui-react # React 18 / 19
123
+ ```
124
+
125
+ 不单独发布 client 包,请求客户端打进这两个界面包:
126
+
127
+ - `@kqstone/account-ui-vue` / `@kqstone/account-ui-react`:`LoginForm`、`RegisterForm`、`ResetPasswordForm`、`TwoFactorSettings`、`TwoFactorLoginDialog`、`AvatarUploader`、`UserAvatar`、`ProfileFields`(`passwordEmailCode` 支持改密邮箱验证码)、`ChangeEmailForm`、`DeleteAccountForm`、`LogoutButton`、`CaptchaImage`、`TierBadge`、`createAccountClient`、`createTokenStore`
128
+ - `createAccountClient(base, { getToken, logoutPath, autoRefresh: { getRefreshToken, onTokens, onRefreshFailed } })`:登录后收到 401 时单飞刷新一次再重试(需服务端 `refresh_token_enabled`);新增 `refresh`、`logout`、`sendChangeEmailCode`、`changeEmail`、`deleteAccount`、`sendDeleteAccountEmailCode`;`AccountApiError.code` 取 `detail.code`
129
+
130
+ 界面包发布的是构建后的 `dist/`(ESM + `.d.ts`),样式在组件首次渲染时注入,不需要单独引入 CSS。2FA 设置页的二维码使用可选 peer `qrcode`,或由宿主传入 `renderQr(otpauthUri) => dataUrl`。文案内置中英,可用 `labels` 覆盖;Vue 不绑 vue-i18n,React 不依赖 antd。
131
+
132
+ ## 本地开发
133
+
134
+ 宿主仓库用 `file:` 路径引用界面包时,先在 account-kit 里构建一次(`npm ci` 会通过 `prepare` 自动构建):
135
+
136
+ ```bash
137
+ cd packages/account-ui-vue && npm ci # 或 packages/account-ui-react
138
+ ```
139
+
140
+ 改了界面包源码后,重新 `npm run build`,再在宿主里重新安装依赖。
141
+
142
+ ## 发布
143
+
144
+ 版本号写在 `pyproject.toml`、`src/account_kit/__init__.py` 的 `__version__` 和两个 `packages/*/package.json` 里,发布前三处保持一致。推送 `v<版本号>` tag(如 `v0.2.0`)后,GitHub Action 先校验 tag 与三处版本一致、跑测试和构建,然后:
145
+
146
+ - Python 包发布到 [PyPI](https://pypi.org/project/account-kit/)(Trusted Publishing,不用 token)
147
+ - 两个界面包发布到 [GitHub Packages](https://github.com/kqstone?tab=packages)(`GITHUB_TOKEN`,`packages: write`)
148
+
149
+ 日后若改发 npmjs.com:把 `publishConfig.registry` 改回默认、workflow 改为 `registry.npmjs.org` + `NPM_TOKEN`(或 npm trusted publishing),并更新本节安装说明。
150
+
151
+ ## 许可证
152
+
153
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "account-kit"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Shared async account service for FastAPI apps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -40,7 +40,8 @@ dependencies = [
40
40
  ]
41
41
 
42
42
  [project.optional-dependencies]
43
- dev = ["pytest>=8", "pytest-asyncio>=0.23", "httpx>=0.27"]
43
+ captcha = ["Pillow>=9.2"]
44
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "httpx>=0.27", "Pillow>=9.2"]
44
45
 
45
46
  [build-system]
46
47
  requires = ["setuptools>=77"]
@@ -50,7 +51,7 @@ build-backend = "setuptools.build_meta"
50
51
  where = ["src"]
51
52
 
52
53
  [tool.setuptools.package-data]
53
- account_kit = ["templates/emails/*.html"]
54
+ account_kit = ["templates/emails/*.html", "sql/*.sql"]
54
55
 
55
56
  [tool.pytest.ini_options]
56
57
  asyncio_mode = "auto"
@@ -0,0 +1,9 @@
1
+ """Shared account kit: one async user model for every app."""
2
+
3
+ from account_kit.router import mount_account
4
+ from account_kit.schema_setup import ensure_schema, ensure_schema_sync, init_db
5
+ from account_kit.seed import seed_defaults
6
+
7
+ __version__ = "0.2.2"
8
+
9
+ __all__ = ["ensure_schema", "ensure_schema_sync", "init_db", "mount_account", "seed_defaults", "__version__"]