167 lines
6.3 KiB
Python
167 lines
6.3 KiB
Python
"""认证模块 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 配置中,请使用账密登录")
|