326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.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.core.config import settings
|
||
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,
|
||
user_has_bound_roles,
|
||
)
|
||
from app.crud.crm.quote import view_by_share_code
|
||
|
||
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:
|
||
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or settings.DEFAULT_TENANT_NO
|
||
return {
|
||
"sub": user.id,
|
||
"username": user.user_no,
|
||
"role": web_role,
|
||
"region_id": user.dept_id,
|
||
"real_name": user.nick_name,
|
||
"tenant_no": tenant_no,
|
||
}
|
||
|
||
|
||
@router.get("/web/auth/login-rsa-public-key", response_model=dict)
|
||
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)
|
||
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_for_web_login(db, user_no)
|
||
|
||
if not user:
|
||
return error_response(400, "用户名或密码错误")
|
||
|
||
pwd_hash = getattr(user, "password", None) or ""
|
||
if not verify_password(password, pwd_hash):
|
||
return error_response(400, "用户名或密码错误")
|
||
|
||
if user.status != "1":
|
||
return error_response(403, "账号已被停用,请联系管理员")
|
||
|
||
web_role = resolve_web_role_for_user(db, user.id, user.tenant_no)
|
||
|
||
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,
|
||
)
|
||
|
||
store_session(session_id=session_id, user_id=user.id)
|
||
store_refresh_token(refresh_jti=refresh_jti, user_id=user.id)
|
||
|
||
return success_response(
|
||
{
|
||
"token": access_token,
|
||
"refreshToken": refresh_token,
|
||
"expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||
"userInfo": _build_user_info_payload(user, web_role),
|
||
}
|
||
)
|
||
|
||
|
||
@router.post("/web/auth/refresh-token", response_model=dict)
|
||
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 is_session_valid(session_id=session_id):
|
||
return error_response(401, "refreshToken已失效,请重新登录")
|
||
if not 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, user.tenant_no)
|
||
|
||
revoke_refresh_token(refresh_jti=refresh_jti)
|
||
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,
|
||
)
|
||
|
||
store_refresh_token(refresh_jti=new_refresh_jti, user_id=user.id)
|
||
|
||
return success_response(
|
||
{
|
||
"token": new_access_token,
|
||
"refreshToken": new_refresh_token,
|
||
"expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||
}
|
||
)
|
||
except HTTPException:
|
||
return error_response(401, "refreshToken已过期")
|
||
|
||
|
||
@router.post("/web/auth/logout", response_model=dict)
|
||
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:
|
||
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)
|
||
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 = 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)
|
||
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 = get_current_user_by_token(token=token, db=db)
|
||
user_id = current_user.get("user_id") or ""
|
||
tenant_no = current_user.get("tenant_no")
|
||
|
||
if not user_has_bound_roles(db, user_id, tenant_no):
|
||
return success_response(
|
||
{
|
||
"isSuperAdmin": False,
|
||
"all": False,
|
||
"menuCodes": [],
|
||
}
|
||
)
|
||
|
||
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, user_id, tenant_no)
|
||
menu_codes = sorted(codes)
|
||
except OperationalError:
|
||
menu_codes = []
|
||
|
||
return success_response(
|
||
{
|
||
"isSuperAdmin": False,
|
||
"all": False,
|
||
"menuCodes": menu_codes,
|
||
}
|
||
)
|
||
except HTTPException:
|
||
return error_response(401, "Token无效或已过期")
|
||
|
||
|
||
@router.get("/public/crm/quote/share/{share_code}")
|
||
def public_quote_share(
|
||
share_code: str,
|
||
request: Request,
|
||
password: str = "",
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""公开分享查看报价单 — Java: GET /share/{shareCode}(无 JWT)"""
|
||
try:
|
||
ip_address = request.client.host if request.client else ""
|
||
user_agent = request.headers.get("user-agent", "")
|
||
detail = view_by_share_code(
|
||
db,
|
||
share_code,
|
||
password=password or "",
|
||
ip_address=ip_address,
|
||
user_agent=user_agent,
|
||
)
|
||
return success_response(detail)
|
||
except ValueError as e:
|
||
return error_response(400, str(e))
|
||
except Exception as e:
|
||
return error_response(500, str(e))
|