"""Auth 路由:platform_key 登录 / 管理员发用户 / 邮箱密码登录 / 改密码。 业务逻辑(bcrypt / users upsert)在 web/auth.py,这里只是 HTTP 壳。 """ from __future__ import annotations from datetime import datetime as _dt from uuid import UUID from fastapi import Depends, HTTPException from ..auth import ( AuthConfig, UserCreateError, change_password, create_user, ensure_user_row, get_user_profile, mint_token, resolve_user_by_email, ) from ..schemas import ( AdminCreateUserRequest, ChangePasswordRequest, LoginRequest, PasswordLoginRequest, ) def register_auth_routes(app, *, require_user, auth_cfg: AuthConfig) -> None: @app.post("/v1/auth/login", tags=["auth"]) def login(body: LoginRequest): """platform_key 校验通过 → 签 JWT(user_id 作为 sub)。 platform_key 错 → 403;user_id 非 UUID → 400。 user_id 未存在则幂等创建 users 行(避免下游 FK 失败);body 带 name / user_name 时一并 upsert 落库(平台侧改名每次登录自动同步,见 ensure_user_row)。 platform 服务端用此入口注入指定 user_id;dev SPA 走 /login_password。 """ if body.platform_key != auth_cfg.platform_key: raise HTTPException(403, "invalid platform_key") try: uid = UUID(body.user_id) except (ValueError, TypeError): raise HTTPException(400, f"invalid user_id (must be UUID): {body.user_id!r}") ensure_user_row(uid, name=body.name, user_name=body.user_name) token, exp = mint_token(auth_cfg, uid) prof = get_user_profile(uid) or {} return { "token": token, "expires_at": _dt.fromtimestamp(exp).isoformat(), "user_id": str(uid), "name": prof.get("name"), "user_name": prof.get("user_name"), "role": prof.get("role", "user"), "ttl_seconds": auth_cfg.ttl_seconds, } @app.post("/v1/auth/admin/create_user", tags=["auth"]) def admin_create_user(body: AdminCreateUserRequest): """管理员发用户(dev SPA 登录页右下角入口)。 - `ZCBOT_ADMIN_TOKEN` env 未设 → 503,功能关闭 - `admin_token` 不匹配 → 403(不细分 "未设" / "错了",防探测) - email 不合法 / password 太短 → 400 - email 已存在 → 409 - 成功 → `{"user_id": ..., "email": ...}`,前端提示 "已创建,请登录" 不签 token、不自动登录 —— 管理员发完用户用户自己登,逻辑清晰。 """ if auth_cfg.admin_token is None: raise HTTPException(503, "admin create_user disabled (ZCBOT_ADMIN_TOKEN not set)") if body.admin_token != auth_cfg.admin_token: raise HTTPException(403, "invalid admin_token") try: uid, email = create_user( email=body.email, password=body.password, role=body.role ) except UserCreateError as ex: if ex.code in ("invalid_email", "weak_password", "invalid_role"): raise HTTPException(400, ex.message) if ex.code == "email_taken": raise HTTPException(409, "email already exists") raise HTTPException(500, f"create_user failed: {ex.message}") return {"user_id": str(uid), "email": email, "role": body.role} @app.post("/v1/auth/login_password", tags=["auth"]) def login_password(body: PasswordLoginRequest): """邮箱密码登录(dev SPA 给同事 / 自己试用)。 - users.email 未命中 / password_hash 为空 / bcrypt 校验失败 → 一律 403 (不细分错因,防探测用户存在性) - 命中 → 直接用 DB 里现成 user_id 签 JWT(不 ensure_user_row,行已在 `user add` 时建) - 发用户:`.venv/Scripts/python.exe main.py user add --email X --password Y`; 撤用户:`DELETE FROM users WHERE email=...`(先 DELETE 该 user 的 tasks) """ hit = resolve_user_by_email(body.email, body.password) if hit is None: raise HTTPException(403, "账号或密码错误") uid, email = hit token, exp = mint_token(auth_cfg, uid) prof = get_user_profile(uid) or {} return { "token": token, "expires_at": _dt.fromtimestamp(exp).isoformat(), "user_id": str(uid), "email": email, "name": prof.get("name"), "user_name": prof.get("user_name"), "role": prof.get("role", "user"), "ttl_seconds": auth_cfg.ttl_seconds, } @app.post("/v1/auth/change_password", tags=["auth"]) def change_password_route( body: ChangePasswordRequest, user_id: UUID = Depends(require_user) ): """改密码(dev SPA 顶栏入口)。user_id 取自 JWT,不信任前端传值。 - 新密码 < 6 → 400 - 旧密码错 / 该账号无密码(platform_key 建的)→ 403(不细分,防探测) - 用户不存在(JWT 有效但行没了)→ 401 成功 → `{"ok": true}`,前端提示并清空表单。 """ try: change_password(user_id, body.old_password, body.new_password) except UserCreateError as ex: if ex.code == "weak_password": raise HTTPException(400, ex.message) if ex.code in ("wrong_password", "no_password"): raise HTTPException(403, ex.message) if ex.code == "user_not_found": raise HTTPException(401, ex.message) raise HTTPException(500, f"change_password failed: {ex.message}") return {"ok": True}