b50495bebe
短信(SMS_MOCK 切 mock/real): - integrations/sms.py 重写: real 模式走极光短信 REST /v1/messages 自定义验证码(本服务 secrets 生成 6 位码 + 进程内存 + 本地校验一次性/防爆破), 鉴权复用极光一键登录 JG_APP_KEY/MASTER_SECRET (同一极光应用, 上线只需 SMS_MOCK=false); mock 仍"任意6位通过"不动其余测试 - 防刷四层: 单号冷却 + 单号每日上限 + 单IP rate_limit(/sms/send 10/min、/sms/login 20/min) + 单码失败次数作废; SmsError 带 status_code 映射 429/503/400 - config 增 SMS_SEND_ENDPOINT/SIGN_ID/TEMPLATE_ID/CODE_LENGTH/DAILY_LIMIT/MAX_VERIFY_ATTEMPTS; test_auth 加 real 模式单测; sms.md/后端技术实现/待办账本同步 admin 后台(app/admin/ 独立子应用, uvicorn app.admin.main:admin_app :8771): - 复用主仓 models/repositories/integrations + 同库, 鉴权完全隔离(ADMIN_JWT_SECRET≠JWT_SECRET_KEY + payload typ=admin + bcrypt 密码 + 可选 IP 白名单); 主 app 不 import 本包, admin 崩不影响主进程 - 路由: 登录 / 账号管理(RBAC: super_admin·finance·operator) / 用户列表+360详情+封禁+手动调币 / 钱包流水 / 提现重试对账 / 反馈工单 / 数据大盘; 全写操作落 admin_audit_log(涉钱与业务写同事务) - 涉钱逻辑(调微信/退款/对账)复用 app.repositories.wallet 不重写 - 新增 models/admin.py(AdminUser/AdminAuditLog) + admin_tables 迁移 + create_admin.py + deploy/shaguabijia-admin.service; 依赖加 bcrypt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""Admin API 共享依赖:DB session、当前 admin、角色守卫、客户端 IP。
|
|
|
|
仿 app/api/deps.py 的 get_current_user,但验的是独立的 admin JWT、查的是 admin_user 表。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, HTTPException, Request, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.admin.repositories import admin_user as admin_repo
|
|
from app.admin.security import AdminTokenError, decode_admin_token
|
|
from app.db.session import get_db
|
|
from app.models.admin import AdminUser
|
|
|
|
_bearer = HTTPBearer(auto_error=False, scheme_name="AdminBearer")
|
|
|
|
|
|
def get_current_admin(
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> AdminUser:
|
|
"""从 Authorization: Bearer <admin_token> 解出当前管理员。失败统一 401。"""
|
|
if credentials is None or credentials.scheme.lower() != "bearer":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="missing bearer token",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
try:
|
|
payload = decode_admin_token(credentials.credentials)
|
|
except AdminTokenError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=str(e),
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
) from e
|
|
|
|
admin = admin_repo.get_by_id(db, int(payload["sub"]))
|
|
if admin is None or admin.status != "active":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="admin not found or disabled",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
return admin
|
|
|
|
|
|
CurrentAdmin = Annotated[AdminUser, Depends(get_current_admin)]
|
|
AdminDb = Annotated[Session, Depends(get_db)]
|
|
|
|
|
|
def require_role(*roles: str):
|
|
"""角色守卫依赖工厂。super_admin 恒通过(全权)。
|
|
|
|
用法:在路由签名加 `_: Annotated[AdminUser, Depends(require_role("finance"))]`,
|
|
或 `dependencies=[Depends(require_role("finance"))]`(不需要拿 admin 时)。
|
|
"""
|
|
allowed = set(roles)
|
|
|
|
def _checker(admin: CurrentAdmin) -> AdminUser:
|
|
if admin.role != "super_admin" and admin.role not in allowed:
|
|
need = sorted(allowed | {"super_admin"})
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"role '{admin.role}' not allowed (need one of {need})",
|
|
)
|
|
return admin
|
|
|
|
return _checker
|
|
|
|
|
|
def get_client_ip(request: Request) -> str:
|
|
"""取客户端 IP(审计日志用)。生产经 nginx 反代,优先 X-Forwarded-For 第一段;否则直连 IP。
|
|
|
|
⚠️ XFF 可被客户端伪造。nginx 必须用 `proxy_set_header X-Forwarded-For $remote_addr`
|
|
覆盖客户端传入值(见 M4 部署),否则审计里的 IP 可被伪造。审计 IP 仅作记录、不参与鉴权。
|
|
"""
|
|
xff = request.headers.get("x-forwarded-for")
|
|
if xff:
|
|
return xff.split(",")[0].strip()
|
|
return request.client.host if request.client else ""
|