"""公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线)""" from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.exc import OperationalError from sqlalchemy.orm import Session from app.core.database import get_db from app.core.security import verify_password from app.core.auth import ( create_access_token, create_refresh_token, decode_token, get_current_user_by_token, extend_session, is_refresh_token_valid, is_session_valid, revoke_refresh_token, revoke_session, store_refresh_token, store_session, ) from app.core.response import success_response, error_response from app.models.models import SysUser from app.crud.user import user_crud from app.core.utils import generate_uuid from app.core import login_crypto from app.services.web_auth_roles import iter_user_menu_perm_codes, resolve_web_role_for_user router = APIRouter(prefix="", tags=["公共接口"]) def _avatar_url_field(user: SysUser) -> Optional[str]: raw = getattr(user, "avatar", None) if not raw: return None s = str(raw).strip() return s if s else None def _build_user_info_payload(user: SysUser, web_role: str) -> dict: """与 web-admin `userInfo` 字段约定一致。""" phone = (getattr(user, "phone_no", None) or "").strip() or None return { "id": user.id, "username": user.user_no, "realName": user.nick_name, "phone": phone, "email": (getattr(user, "email", None) or "").strip() or None, "role": web_role, "regionId": user.dept_id, "regionName": None, "avatarUrl": _avatar_url_field(user), } def _jwt_user_payload(user: SysUser, web_role: str) -> dict: return { "sub": user.id, "username": user.user_no, "role": web_role, "region_id": user.dept_id, "real_name": user.nick_name, } @router.get("/web/auth/login-rsa-public-key", response_model=dict) async def web_login_rsa_public_key(): """Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致)""" login_crypto.ensure_initialized() return success_response({"publicKeyPem": login_crypto.get_public_pem()}) @router.post("/web/auth/login", response_model=dict) async def web_login( request: Request, body: dict, db: Session = Depends(get_db), ): """Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。""" login_crypto.ensure_initialized() user_no, password, cred_err = login_crypto.extract_login_credentials(body) if cred_err: return error_response(400, cred_err) user = user_crud.get_by_user_no(db, user_no) if not user: return error_response(401, "用户名或密码错误") pwd_hash = getattr(user, "password", None) or "" if not verify_password(password, pwd_hash): return error_response(401, "用户名或密码错误") if user.status != "1": return error_response(403, "账号已被停用,请联系管理员") web_role = resolve_web_role_for_user(db, user.id) session_id = generate_uuid() access_jti = generate_uuid() refresh_jti = generate_uuid() access_token = create_access_token( data=_jwt_user_payload(user, web_role), session_id=session_id, token_jti=access_jti, ) refresh_token = create_refresh_token( data={ "sub": user.id, "username": user.user_no, "role": web_role, }, session_id=session_id, refresh_jti=refresh_jti, ) await store_session(session_id=session_id, user_id=user.id) await store_refresh_token(refresh_jti=refresh_jti, user_id=user.id) return success_response( { "token": access_token, "refreshToken": refresh_token, "expiresIn": 7200, "userInfo": _build_user_info_payload(user, web_role), } ) @router.post("/web/auth/refresh-token", response_model=dict) async def refresh_token(body: dict, db: Session = Depends(get_db)): """刷新 Token""" refresh_token_val = body.get("refreshToken") if not refresh_token_val: return error_response(400, "refreshToken不能为空") try: payload = decode_token(refresh_token_val) if payload.get("type") != "refresh": return error_response(401, "无效的refreshToken") session_id = payload.get("sid") refresh_jti = payload.get("jti") if not await is_session_valid(session_id=session_id): return error_response(401, "refreshToken已失效,请重新登录") if not await is_refresh_token_valid(refresh_jti=refresh_jti): return error_response(401, "无效的refreshToken") user_id = payload.get("sub") user = user_crud.get(db, user_id) if not user or user.del_flag != 0 or user.status != "1": return error_response(401, "用户不存在或已禁用") web_role = resolve_web_role_for_user(db, user.id) await revoke_refresh_token(refresh_jti=refresh_jti) await extend_session(session_id=session_id) new_access_jti = generate_uuid() new_refresh_jti = generate_uuid() new_access_token = create_access_token( data=_jwt_user_payload(user, web_role), session_id=session_id, token_jti=new_access_jti, ) new_refresh_token = create_refresh_token( data={ "sub": user.id, "username": user.user_no, "role": web_role, }, session_id=session_id, refresh_jti=new_refresh_jti, ) await store_refresh_token(refresh_jti=new_refresh_jti, user_id=user.id) return success_response( { "token": new_access_token, "refreshToken": new_refresh_token, "expiresIn": 7200, } ) except HTTPException: return error_response(401, "refreshToken已过期") @router.post("/web/auth/logout", response_model=dict) async def logout(request: Request): """登出:吊销 Redis 会话(若已配置 REDIS_HOST)。""" authorization = request.headers.get("Authorization", "") if not authorization.startswith("Bearer "): return error_response(401, "未授权") token = authorization.replace("Bearer ", "") try: payload = decode_token(token) session_id = payload.get("sid") if session_id: await revoke_session(session_id=session_id) except HTTPException: return error_response(401, "Token无效或已过期") return success_response({"message": "登出成功"}) @router.get("/web/auth/current-user", response_model=dict) async def get_current_user_profile(request: Request, db: Session = Depends(get_db)): """获取当前用户信息(与 sys_user 字段对应)。""" authorization = request.headers.get("Authorization", "") if not authorization.startswith("Bearer "): return error_response(401, "未授权") token = authorization.replace("Bearer ", "") try: current_user = await get_current_user_by_token(token=token, db=db) au = current_user.get("avatar_url") avatar_url = str(au).strip() if au else None return success_response( { "id": current_user.get("user_id"), "username": current_user.get("username"), "realName": current_user.get("real_name"), "phone": current_user.get("phone"), "email": current_user.get("email"), "role": current_user.get("role"), "isSuperAdmin": current_user.get("role") == "super_admin", "regionId": current_user.get("region_id"), "regionName": None, "regionIds": [], "isActive": current_user.get("is_active"), "avatarUrl": avatar_url if avatar_url else None, } ) except HTTPException: return error_response(401, "Token无效或已过期") @router.get("/web/auth/menu-codes", response_model=dict) async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)): """获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。""" authorization = request.headers.get("Authorization", "") if not authorization.startswith("Bearer "): return error_response(401, "未授权") token = authorization.replace("Bearer ", "") try: current_user = await get_current_user_by_token(token=token, db=db) role = current_user.get("role") if role == "super_admin": return success_response( { "isSuperAdmin": True, "all": True, "menuCodes": [], } ) try: codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "") menu_codes = sorted(codes) except OperationalError: menu_codes = [] if role == "admin" and not menu_codes: return success_response( { "isSuperAdmin": False, "all": True, "menuCodes": [], } ) return success_response( { "isSuperAdmin": False, "all": False, "menuCodes": menu_codes, } ) except HTTPException: return error_response(401, "Token无效或已过期")