CI / backend (push) Canceled after 0s
用户要求:内测的固定验证码 123456 等于"知道手机号就能进任何账号",先关掉; 登录改用账号密码,并多一种"邮箱当账号名"的方式;发信邮箱要能在后台改。 短信那条路(默认关): - `SMS_LOGIN_ENABLED=false`,`/auth/sms/send` 与 `/auth/login` 统一回 2017,不是静默失败; - 生产校验改成"开着短信登录就必须配真通道",不开时 provider 是 dev 也不拦; - 新增 `GET /auth/methods`,登录页据此渲染,关着的路不出现(少一次"点进去才发现不能用")。 邮箱登录: - `app_user.email` + `uq_app_user_email`(`lower(email)` 部分索引),登录与绑定都归一成小写; - `POST /auth/login/password` 收 `phone` 或 `email`(二选一),失败统一 2013、连错 10 次锁 15 分钟; - 绑定在 `PATCH /auth/me`:格式错 2018、被占用 2019、留空串表示解绑。 系统设置(跨租户,只有平台管理员能改): - 新表 `system_setting`(不挂租户、无 RLS,写权限由 `deps.platform_admin` 把关); - `GET/PUT /settings/smtp`、`POST /settings/smtp/test`,授权码只写不读,留空表示沿用旧值; - `services/mail.py` 只用标准库 smtplib:465 走 SSL、587 走 STARTTLS,认证/拒收/网络失败都归 2020 但消息不同; - Web 新增「系统设置」页(状态卡 + 测试发信),侧栏只对平台管理员显示入口;账号页补「邮箱」。 验证:pytest 130 passed、ruff check/format 全绿、tsc 通过、vite build 通过; `web/e2e/login-email-smtp.mjs` 15 项全绿(含用 126 授权码真发一封测试邮件)。 顺带记下的两个坑:`platform_admin_phones` 是 list,pydantic-settings 会按 JSON 解析, 要用 `NoDecode` 交给 before-validator 拆逗号;迁移号写成 0009 会撞既有的 0009,一并改成 0010。
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
"""邮箱登录 + 平台设置(M1 账号功能)
|
|
|
|
Revision ID: 0010
|
|
Revises: 0009
|
|
Create Date: 2026-11-02
|
|
|
|
两件事:
|
|
|
|
1. `app_user.email`:邮箱当账号名,配合密码登录。唯一性用 `lower(email)` 的部分
|
|
唯一索引——邮箱大小写不敏感,`Steven@x.com` 和 `steven@x.com` 是同一个人;
|
|
允许为空(老用户先不绑)。
|
|
|
|
2. `system_setting`:平台级键值表,第一项就是 SMTP。**这张表刻意不挂 workspace_id、
|
|
也不开 RLS**:发信账号是整台服务共用的,属于平台而不是某个租户,开 RLS 反而要
|
|
编一个"当前租户"出来。写权限在 API 层用平台管理员白名单把关(deps.platform_admin),
|
|
应用角色只有表的读写权,没有建表/改结构的权。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
from alembic import op
|
|
|
|
from app.roles import APP_ROLE
|
|
|
|
revision: str = "0010"
|
|
down_revision: str = "0009"
|
|
branch_labels: Sequence[str] | None = None
|
|
depends_on: Sequence[str] | None = None
|
|
|
|
|
|
CREATE_SQL = """
|
|
CREATE TABLE IF NOT EXISTS system_setting (
|
|
key VARCHAR(64) PRIMARY KEY,
|
|
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
updated_by UUID REFERENCES app_user(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
version INTEGER NOT NULL DEFAULT 1
|
|
)
|
|
"""
|
|
|
|
INDEX_STATEMENTS = (
|
|
"ALTER TABLE app_user ADD COLUMN IF NOT EXISTS email VARCHAR(255)",
|
|
"CREATE UNIQUE INDEX uq_app_user_email ON app_user (lower(email)) WHERE email IS NOT NULL",
|
|
"COMMENT ON TABLE system_setting IS '平台级设置(SMTP 等),不挂租户、无 RLS'",
|
|
)
|
|
|
|
GRANT_SQL = (
|
|
f"GRANT SELECT, INSERT, UPDATE, DELETE ON system_setting TO {APP_ROLE}",
|
|
f"GRANT SELECT, UPDATE ON app_user TO {APP_ROLE}",
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute(CREATE_SQL)
|
|
for statement in INDEX_STATEMENTS:
|
|
op.execute(statement)
|
|
for statement in GRANT_SQL:
|
|
op.execute(statement)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP INDEX IF EXISTS uq_app_user_email")
|
|
op.execute("ALTER TABLE app_user DROP COLUMN IF EXISTS email")
|
|
op.execute("DROP TABLE IF EXISTS system_setting")
|