feat(api): add mini wechat-login and share JWT issuance with web login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7ccd1f553e
commit
713aa700ae
5 files changed
+168
-81
No files matched your search
@@ -8,6 +8,7 @@ from app.api.crm import router as crm_router
|
||||
from app.api.monitor import router as monitor_router
|
||||
from app.api.bx import bx_router
|
||||
from app.api.db import db_router
|
||||
from app.api.mini_auth import router as mini_auth_router
|
||||
|
||||
__all__ = [
|
||||
"public_router",
|
||||
@@ -19,4 +20,5 @@ __all__ = [
|
||||
"monitor_router",
|
||||
"bx_router",
|
||||
"db_router",
|
||||
"mini_auth_router",
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""小程序鉴权:微信手机号快捷登录。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.response import error_response, success_response
|
||||
from app.services.web_login_tokens import issue_web_login_tokens
|
||||
from app.services.wechat_login_service import resolve_wechat_login_user
|
||||
from app.services.wechat_mini_auth import (
|
||||
WechatMiniAuthError,
|
||||
code2session,
|
||||
get_phone_number,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/mini/auth", tags=["小程序鉴权"])
|
||||
|
||||
|
||||
@router.post("/wechat-login", response_model=dict)
|
||||
def wechat_login(body: dict, db: Session = Depends(get_db)):
|
||||
"""手机号快捷登录:须 loginCode + phoneCode;匹配 phone_no 并绑定 wx_openid。"""
|
||||
login_code = (body.get("loginCode") or "").strip()
|
||||
phone_code = (body.get("phoneCode") or "").strip()
|
||||
if not phone_code:
|
||||
return error_response(400, "请授权手机号登录,或使用账号密码登录")
|
||||
|
||||
try:
|
||||
sess = code2session(login_code)
|
||||
except WechatMiniAuthError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
try:
|
||||
phone = get_phone_number(phone_code)
|
||||
except WechatMiniAuthError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
user, err_msg, err_code = resolve_wechat_login_user(
|
||||
db, openid=sess["openid"], phone=phone
|
||||
)
|
||||
if err_msg:
|
||||
return error_response(err_code or 400, err_msg)
|
||||
|
||||
unionid = (sess.get("unionid") or "").strip()
|
||||
if unionid and not (getattr(user, "wx_unionid", None) or "").strip():
|
||||
user.wx_unionid = unionid
|
||||
user.update_time = datetime.now()
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
try:
|
||||
data = issue_web_login_tokens(db, user)
|
||||
except ValueError as exc:
|
||||
return error_response(403, str(exc))
|
||||
return success_response(data)
|
||||
+12
-81
@@ -1,6 +1,4 @@
|
||||
"""公共接口(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
|
||||
@@ -18,11 +16,9 @@ from app.core.auth import (
|
||||
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, change_password
|
||||
from app.core.utils import generate_uuid
|
||||
from app.core import login_crypto
|
||||
@@ -31,56 +27,18 @@ from app.services.web_auth_roles import (
|
||||
resolve_web_role_for_user,
|
||||
user_has_bound_roles,
|
||||
)
|
||||
from app.services.web_login_tokens import (
|
||||
_jwt_user_payload,
|
||||
_web_role_list,
|
||||
build_user_info_payload,
|
||||
issue_web_login_tokens,
|
||||
)
|
||||
from app.services.mp_webview_ticket import TICKET_TTL_SEC, consume_ticket, issue_ticket
|
||||
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 _web_role_list(web_role: str) -> list[str]:
|
||||
"""与 web-admin 静态路由 meta.roles(admin/super_admin)对齐的单元素数组。"""
|
||||
role = (web_role or "").strip() or settings.WEB_AUTH_DEFAULT_ROLE
|
||||
return [role]
|
||||
|
||||
|
||||
def _build_user_info_payload(user: SysUser, web_role: str) -> dict:
|
||||
"""与 web-admin `userInfo` 字段约定一致。"""
|
||||
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
||||
role = (web_role or "").strip() or settings.WEB_AUTH_DEFAULT_ROLE
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.user_no,
|
||||
"realName": user.nick_name,
|
||||
"phone": phone,
|
||||
"email": (getattr(user, "email", None) or "").strip() or None,
|
||||
"role": role,
|
||||
"roles": _web_role_list(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 一致)"""
|
||||
@@ -112,38 +70,11 @@ def web_login(
|
||||
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),
|
||||
}
|
||||
)
|
||||
try:
|
||||
data = issue_web_login_tokens(db, user)
|
||||
except ValueError as exc:
|
||||
return error_response(403, str(exc))
|
||||
return success_response(data)
|
||||
|
||||
|
||||
@router.post("/web/auth/refresh-token", response_model=dict)
|
||||
@@ -276,7 +207,7 @@ def mp_webview_exchange(body: dict, db: Session = Depends(get_db)):
|
||||
"token": access_token,
|
||||
"refreshToken": refresh_token_val,
|
||||
"expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
"userInfo": _build_user_info_payload(user, web_role),
|
||||
"userInfo": build_user_info_payload(user, web_role),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from app.api import (
|
||||
monitor_router,
|
||||
bx_router,
|
||||
db_router,
|
||||
mini_auth_router,
|
||||
)
|
||||
from app.api.pl import pl_router
|
||||
from app.api.pl.public_transport import router as public_pl_transport_router
|
||||
@@ -159,6 +160,7 @@ async def unhandled_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
|
||||
app.include_router(public_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(public_pl_transport_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(public_pl_transport_site_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(public_pl_site_router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Web / 小程序共用:为 sys_user 签发 access + refresh JWT。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
store_refresh_token,
|
||||
store_session,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.models import SysUser
|
||||
from app.services.web_auth_roles import resolve_web_role_for_user
|
||||
|
||||
|
||||
def _avatar_url_field(user: SysUser):
|
||||
raw = getattr(user, "avatar", None)
|
||||
if not raw:
|
||||
return None
|
||||
s = str(raw).strip()
|
||||
return s if s else None
|
||||
|
||||
|
||||
def _web_role_list(web_role: str) -> list:
|
||||
role = (web_role or "").strip() or settings.WEB_AUTH_DEFAULT_ROLE
|
||||
return [role]
|
||||
|
||||
|
||||
def build_user_info_payload(user: SysUser, web_role: str) -> dict:
|
||||
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
||||
role = (web_role or "").strip() or settings.WEB_AUTH_DEFAULT_ROLE
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.user_no,
|
||||
"realName": user.nick_name,
|
||||
"phone": phone,
|
||||
"email": (getattr(user, "email", None) or "").strip() or None,
|
||||
"role": role,
|
||||
"roles": _web_role_list(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,
|
||||
}
|
||||
|
||||
|
||||
def issue_web_login_tokens(db: Session, user: SysUser) -> Dict[str, Any]:
|
||||
if getattr(user, "status", "1") != "1":
|
||||
raise ValueError("账号已被停用,请联系管理员")
|
||||
|
||||
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 {
|
||||
"token": access_token,
|
||||
"refreshToken": refresh_token,
|
||||
"expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
"userInfo": build_user_info_payload(user, web_role),
|
||||
}
|
||||
Reference in new issue
Block a user