"""认证模块 Blueprint。4 个接口:企微登录、账密登录、刷新 Token、退出。""" from datetime import datetime, timezone import bcrypt from flask import Blueprint, request from insurance.utils.auth import decode_token, generate_token, login_required, get_current_user from insurance.utils.response import success, error auth_bp = Blueprint("insurance_auth", __name__) def _get_user_by_username(username: str): """从 wecom_user_mapping 按 username 查询用户。""" from extensions.ext_database import db from insurance.db.models import WecomUserMapping return db.session.query(WecomUserMapping).filter_by(username=username).first() def _get_user_by_wecom_userid(userid: str): """从 wecom_user_mapping 按 wecom_userid 查询用户。""" from extensions.ext_database import db from insurance.db.models import WecomUserMapping return db.session.query(WecomUserMapping).filter_by(wecom_userid=userid).first() def _sign_token(user) -> dict: """签发 JWT 并返回标准响应。""" token = generate_token( user_id=str(user.id), username=user.username or "", role=user.role or "sales", department=user.department or "", ) return success({ "token": token, "expires_in": 7200, "user": { "id": str(user.id), "username": user.username, "wecom_userid": user.wecom_userid, "role": user.role, "department": user.department, }, }) def _record_login_log(user_id: str, action: str = "login"): """记录登录/登出日志。""" try: from extensions.ext_database import db from insurance.db.models import SystemOperationLog log = SystemOperationLog( user_id=user_id, action=action, ip=request.remote_addr, user_agent=request.headers.get("User-Agent", "")[:256], ) db.session.add(log) db.session.commit() except Exception: pass # ────────────────────────────────────────────── # A1.1.2 账密登录 # ────────────────────────────────────────────── @auth_bp.route("/password-login", methods=["POST"]) def password_login(): """账密登录:POST /api/auth/password-login""" data = request.get_json(force=True, silent=True) or {} username = (data.get("username") or "").strip() password = data.get("password") or "" # 参数校验 if not username or len(username) < 3 or len(username) > 64: return error(1001, "参数错误:用户名不能为空") if not password or len(password) < 8 or len(password) > 128: return error(1001, "参数错误:密码不能为空") # 查用户 user = _get_user_by_username(username) if user is None: return error(1002, "用户名或密码错误") # 校验密码 if not user.password_hash: return error(1002, "用户名或密码错误") if not bcrypt.checkpw(password.encode("utf-8"), user.password_hash.encode("utf-8")): return error(1002, "用户名或密码错误") # 检查状态 if user.status == "disabled": return error(1004, "账号已被禁用,请联系管理员") _record_login_log(str(user.id), "login") return _sign_token(user) # ────────────────────────────────────────────── # A1.1.3 刷新 Token # ────────────────────────────────────────────── @auth_bp.route("/refresh-token", methods=["POST"]) @login_required def refresh_token(): """刷新 Token:POST /api/auth/refresh-token""" user = get_current_user() if not user: return error(1003, "登录已过期,请重新登录") # 旧 Token 加入 Redis 黑名单 try: from extensions.ext_redis import redis_client from insurance.config import REDIS_TOKEN_BLACKLIST_PREFIX old_token = request.headers.get("Authorization", "")[7:] redis_client.setex(f"{REDIS_TOKEN_BLACKLIST_PREFIX}{old_token}", 7200, "1") except Exception: pass # 签发新 Token from extensions.ext_database import db from insurance.db.models import WecomUserMapping db_user = db.session.query(WecomUserMapping).filter_by(id=user.get("user_id")).first() if not db_user: return error(1003, "登录已过期,请重新登录") return _sign_token(db_user) # ────────────────────────────────────────────── # A1.1.4 退出登录 # ────────────────────────────────────────────── @auth_bp.route("/logout", methods=["POST"]) @login_required def logout(): """退出登录:POST /api/auth/logout""" user = get_current_user() # Token 加入 Redis 黑名单 try: from extensions.ext_redis import redis_client from insurance.config import REDIS_TOKEN_BLACKLIST_PREFIX token = request.headers.get("Authorization", "")[7:] redis_client.setex(f"{REDIS_TOKEN_BLACKLIST_PREFIX}{token}", 7200, "1") except Exception: pass if user: _record_login_log(user.get("user_id", ""), "logout") return success() # ────────────────────────────────────────────── # A1.1.1 企微登录(POST,供企微 H5 调用) # ────────────────────────────────────────────── @auth_bp.route("/wework-login", methods=["POST"]) def wework_login(): """企微 OAuth 登录:POST /api/auth/wework-login""" data = request.get_json(force=True, silent=True) or {} code = (data.get("code") or "").strip() if not code or len(code) > 512: return error(1001, "参数错误:企微授权码不能为空") # 调企微 API 换用户信息(需要企微配置就绪后实现) # 暂时返回占位 return error(3001, "企微 API 配置中,请使用账密登录")