account-kit
dedd-online 与 dental-case-pro 共用的异步账号包。PostgreSQL + SQLAlchemy + asyncpg,表在 auth schema,用户主键是 UUID。
账号能力:注册、登录、邮件验证码、审批、单设备会话、自定义角色、后台权限(is_admin,不是角色)、等级目录、两步验证。限额、限流、积分留在宿主。
from account_kit import init_db, mount_account, seed_defaults
from account_kit.config import AccountKitConfig
await init_db(engine)
await seed_defaults(session)
mount_account(app, get_db, AccountKitConfig(jwt_secret="..."))
可选:邮箱两步验证、登录图形验证码
两项默认关闭,由宿主在 AccountKitConfig 里打开:
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。captcha_verifier=fn(captcha_id, captcha_code) -> bool(可为 async):同一 IP 或用户名在captcha_fail_window_seconds内失败captcha_fail_threshold次后,POST /login必须带captcha_id、captcha_code,否则 428CAPTCHA_REQUIRED,错了 400CAPTCHA_INVALID;密码错误返回 401INVALID_CREDENTIALS(带captcha_required)。验证码图片由宿主提供(界面包从GET {api_prefix}/captcha取captcha_id、image_base64)。反向代理后用client_ip=fn(request)取真实 IP。before_two_factor=async fn(request, user):每次校验第二因素、发邮箱码前调用,宿主可在这里限流。two_factor_challenge_ttl_seconds(默认 300)、two_factor_max_attempts(默认 5):登录两步验证 challenge 的有效期和每个 challenge 允许输错的次数。每次请求时读取,宿主可在get_db里按后台设置刷新。
init_db 和直接调用 Base.metadata.create_all 都会先建 auth schema(CREATE SCHEMA IF NOT EXISTS),空库可直接启动。React 与 Vue 的 LoginForm 在服务端要求时都会显示图形验证码。
0.2.0:邮件模板覆盖、改密、头像、角色申请查询
- 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 }})。 - 宿主覆盖模板:设
email_template_dir后,Jinja 先从该目录按同名加载,找不到再回落包内默认。email_template_map可把 purpose 指到相对文件名(支持{lang});拒绝..与绝对路径。完全接管发送仍可用mailer。 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改密仍走邮箱码,两条路径语义不同。- 可选头像(默认关):
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是本地已存在文件才直接读取。 GET {api_prefix}/role-change-requests/me:当前用户 pending 申请,没有则null(需role_change_enabled)。
0.2.2:安全加固、刷新令牌、登出、自助注销、审计日志
验证码防爆破与发送限流
verify_code_max_attempts(默认 5):同一邮箱、同一用途输错达到次数后,该邮箱该用途所有未用验证码作废,返回 400验证码错误次数过多,请重新获取验证码,并记审计verification_code_locked。注册、找回密码、改密、邮箱两步验证、修改邮箱、注销账号都受此限制。- 发码限流(
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_*。 - 其它可选限流(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)。 - 超限统一返回 429,
detail={"code":"RATE_LIMITED","message":...,"retry_after":秒},带Retry-After头。 - 钩子:
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即拒绝。
状态存储:state_backend="db"(默认)把登录失败计数、限流计数、2FA 登录 challenge、内置图形验证码放进 auth schema 的表,多进程、多实例共享,重启不丢;state_backend="memory" 保持 0.2.1 的单进程内存行为。
修改邮箱需验证
POST /me/email/send-code,body{new_email, password?, language}:把验证码发到新邮箱。新邮箱已被占用时返回 400;同一用户 60 秒内重复发送返回 429EMAIL_CODE_TOO_FREQUENT。POST /me/email,body{new_email, code, password?}:验证码与当前用户绑定,别人的码不能用。change_email_require_password=True(默认)时必须带密码。成功后记审计email_changed。PATCH /me里的email由profile_email_change控制:verify(默认):要带email_code(以及current_password),否则 400EMAIL_VERIFICATION_REQUIRED;reject:400EMAIL_CHANGE_VIA_ENDPOINT;direct:0.2.1 行为,不验证,不推荐。
刷新令牌(refresh_token_enabled=False,默认关)
- 打开后,
/login和/login/2fa额外返回refresh_token、refresh_expires_in(refresh_token_expire_days默认 30)。关闭时响应字段与 0.2.1 完全一致。 POST /refresh{refresh_token}:每次轮换。库里只存 SHA-256 哈希,按 family 组织。错误码:REFRESH_INVALID、REFRESH_EXPIRED、SESSION_REPLACED、REFRESH_REUSED。重用旧 token 时吊销整个 family 并清掉 single_device 会话,所以客户端要单飞刷新(界面包已经这样做)。- 改密、重置密码、关闭或重置 2FA、停用账号、撤销审批、改角色或改
is_admin、注销账号都会吊销刷新令牌;single_device 模式下新登录会吊销其它设备的令牌。
登出:POST {api_prefix}{logout_path}(logout_enabled=True,logout_path="/logout")。
- body 可空,也可以是 JSON 或表单
{refresh_token?, all_devices?},总是返回{"status":"success"}。 - access token 过期或会话已被顶替时也返回 200,只在 token 的
sid与当前会话一致时才清会话。吊销该会话的刷新令牌;all_devices吊销全部。 - 记审计
logout,回调on_logout。
自助注销(self_delete_enabled=False,默认关)
POST /me/delete(或DELETE /me),body{password, code?, recovery_code?, email_code?}。开了 2FA 的账号必须再给一个因素,否则 400MFA_CODE_REQUIRED;用邮箱码时先调POST /me/delete/email-code。管理员不能自助注销,返回 403ADMIN_SELF_DELETE_FORBIDDEN。user_delete_mode同时作用于后台删除和自助注销:"hard"(默认):删行,与 0.2.1 相同;"soft":停用账号,用户名和邮箱改成墓碑值,清空个人资料,吊销所有凭据,清掉 2FA 和头像,保留行以满足外键。
- 两种模式都会先调用
on_deleted。
审计日志(audit_log_enabled=True)
- 写入
auth.auth_audit_logs,使用独立事务,业务回滚不影响审计。每写一条回调一次on_audit(db, event, user_id, meta)。 - 事件:
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。 - 后台查询:
GET {admin_prefix}/audit-logs?page=&page_size=(≤200)&user_id=&event=a,b&ip=&since=&until=,返回{items,total,page,page_size}。
内置图形验证码(captcha_builtin=False,默认关,需要 pip install "account-kit[captcha]")
- 打开后 kit 提供
GET {api_prefix}/captcha,返回{captcha_id, image_base64, expires_in},并自行校验。答案以 HMAC 哈希保存,只能用一次。 - 相关配置:
captcha_ttl_seconds(300)、captcha_length(4)。 - 宿主配置了
captcha_verifier时优先用宿主的。宿主自己的/captcha路由不受影响,因为未开启时 kit 不注册这个路由。
数据库变更:新增 4 张表 auth.rate_limit_counters、auth.two_factor_challenges、auth.refresh_tokens、auth.captcha_challenges,已有表的列不变。
- 用
init_db或Base.metadata.create_all的宿主会自动建表。 - 没有 Alembic 的宿主可以在启动时调用
ensure_schema(engine)/ensure_schema_sync(engine),它们会幂等地补建缺失的表。 - 用 Alembic 的宿主执行或照抄
src/account_kit/sql/upgrade_0_2_2.sql(随包安装,可用account_kit.schema_setup.upgrade_sql("0.2.2")读出)。
从 0.2.1 升级注意
PATCH /me直接改邮箱会被拒(默认verify)。前端要改用ChangeEmailForm或/me/email流程;过渡期可以设profile_email_change="direct"。dental 资料页目前在 PATCH 里带email,没有验证码,不改的话整次保存都会 400。- 默认
state_backend="db"后,login_fail_tracker.clear_all()不再清计数。测试里改用state_backend="memory",或清空auth.rate_limit_counters。 - kit 现在自己注册
/logout。宿主在 kit 之后挂同路径的路由会被 kit 的遮住;kit 的行为是 dedd 原逻辑的超集。不想要的话设logout_enabled=False,或改logout_path。 - 下面这些 API 改成了 async,并带
db参数:mfa_required(db, ...)、captcha.*,以及 2FA challenge 的create_challenge/load_challenge/save_challenge/discard_challenge。challenge_store仍可导入,调用它不会出错,但不再起作用。admin_reset(db, user_id)签名不变。 - 中英混杂的个别错误文案(如
Incorrect username or password)没有改,因为客户端和登录失败计数靠它们匹配。
安装
Python 包从 PyPI 安装:
pip install account-kit
pip install "account-kit[captcha]" # 需要内置图形验证码时(Pillow)
界面包发在 GitHub Packages(不是 npmjs.com)。安装前在项目根写 .npmrc:
@kqstone:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
NPM_TOKEN 需要带 read:packages 的 classic PAT(或等价权限)。CI 里可以用 GITHUB_TOKEN,但要先在包的 Manage Actions access 里给消费仓库(如 kqstone/dedd-online、kqstone/dental-case-pro)Read 权限。从 private 仓库首次发布的包默认为 private;可在包设置里改成 public,但无论可见性如何,安装都仍需要 token。
npm install @kqstone/account-ui-vue # Vue 3
npm install @kqstone/account-ui-react # React 18 / 19
不单独发布 client 包,请求客户端打进这两个界面包:
@kqstone/account-ui-vue/@kqstone/account-ui-react:LoginForm、RegisterForm、ResetPasswordForm、TwoFactorSettings、TwoFactorLoginDialog、AvatarUploader、UserAvatar、ProfileFields(passwordEmailCode支持改密邮箱验证码)、ChangeEmailForm、DeleteAccountForm、LogoutButton、CaptchaImage、TierBadge、createAccountClient、createTokenStorecreateAccountClient(base, { getToken, logoutPath, autoRefresh: { getRefreshToken, onTokens, onRefreshFailed } }):登录后收到 401 时单飞刷新一次再重试(需服务端refresh_token_enabled);新增refresh、logout、sendChangeEmailCode、changeEmail、deleteAccount、sendDeleteAccountEmailCode;AccountApiError.code取detail.code
界面包发布的是构建后的 dist/(ESM + .d.ts),样式在组件首次渲染时注入,不需要单独引入 CSS。2FA 设置页的二维码使用可选 peer qrcode,或由宿主传入 renderQr(otpauthUri) => dataUrl。文案内置中英,可用 labels 覆盖;Vue 不绑 vue-i18n,React 不依赖 antd。
本地开发
宿主仓库用 file: 路径引用界面包时,先在 account-kit 里构建一次(npm ci 会通过 prepare 自动构建):
cd packages/account-ui-vue && npm ci # 或 packages/account-ui-react
改了界面包源码后,重新 npm run build,再在宿主里重新安装依赖。
发布
版本号写在 pyproject.toml、src/account_kit/__init__.py 的 __version__ 和两个 packages/*/package.json 里,发布前三处保持一致。推送 v<版本号> tag(如 v0.2.0)后,GitHub Action 先校验 tag 与三处版本一致、跑测试和构建,然后:
- Python 包发布到 PyPI(Trusted Publishing,不用 token)
- 两个界面包发布到 GitHub Packages(
GITHUB_TOKEN,packages: write)
日后若改发 npmjs.com:把 publishConfig.registry 改回默认、workflow 改为 registry.npmjs.org + NPM_TOKEN(或 npm trusted publishing),并更新本节安装说明。
许可证
MIT
Metadata
Release files for account-kit 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| account_kit-0.2.2.tar.gz | 79.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| account_kit-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 150.6 kB
Release files / account_kit-0.2.2.tar.gz
| Download URL | account_kit-0.2.2.tar.gz |
|---|---|
| Size | 79.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
481baf435563c26fb6781d04f43489bdc886bbea20c14dff3f9504a2dc4409b4
|
|
BLAKE2b-256 checksum How to use checksums |
101bb28da39f46d1193be9252da2be6a5a07d4c21207c9be0da4814ca2045344
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / account_kit-0.2.2-py3-none-any.whl
| Download URL | account_kit-0.2.2-py3-none-any.whl |
|---|---|
| Size | 71.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5cdbb7c1614c6d3bb1666a7397afebbdded042e5868dac662a851f208994da65
|
|
BLAKE2b-256 checksum How to use checksums |
3fc999fb07eb7065a72c8453678f7080e786b9784ff78083a0d50b22ef79ce9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|