Files

343 lines
12 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_
from pydantic import BaseModel, Field
from typing import Optional
import uuid
import random
import re
from datetime import datetime, timedelta
from ..database import get_db
from ..models import (
User, LoginLog, RegisterSetting, SmsConfig, SmsVerificationCode,
gen_uuid
)
from ..auth import get_current_user, hash_password, verify_password, create_access_token
from ..middleware.log_service import LogService
from ..sms_client import send_sms_code
router = APIRouter()
class LoginRequest(BaseModel):
username: str
password: str
class RegisterRequest(BaseModel):
username: str = Field(min_length=3, max_length=50)
password: str = Field(min_length=6, max_length=100)
email: Optional[str] = None
phone: Optional[str] = None
sms_code: Optional[str] = None
class SendCodeRequest(BaseModel):
phone: str
purpose: str = "register" # register / reset_password
class ResetPasswordRequest(BaseModel):
phone: str
code: str
new_password: str = Field(min_length=6, max_length=100)
@router.post("/login")
def login(req: LoginRequest, db: Session = Depends(get_db)):
import logging
logger = logging.getLogger(__name__)
user = db.query(User).filter(User.username == req.username).first()
if not user:
logger.warning(f"登录失败: 用户 '{req.username}' 不存在")
raise HTTPException(status_code=401, detail="用户名不存在")
stored_hash = user.password_hash
logger.info(f"登录调试: username={req.username} hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}")
if not verify_password(req.password, user.password_hash):
logger.warning(f"登录失败: 用户 '{req.username}' 密码验证失败 hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}")
raise HTTPException(status_code=401, detail="密码错误")
if user.status == "banned":
raise HTTPException(status_code=403, detail="账号已被封禁")
token_data = {"sub": user.id, "username": user.username, "role": user.role}
access_token = create_access_token(token_data)
# 记录登录日志
login_log = LoginLog(
id=gen_uuid(),
user_id=user.id,
ip="",
login_at=datetime.now(),
)
db.add(login_log)
# 记录操作日志
LogService.log_operation(
user_id=user.id,
username=user.username,
action="login",
target="user",
detail=f"用户 {user.username} 登录成功",
)
db.commit()
return {
"code": 0,
"data": {
"access_token": access_token,
"token_type": "bearer",
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
"phone": user.phone,
"avatar": user.avatar,
"role": user.role,
"status": user.status,
"membership_level": user.membership_level,
"membership_expire_at": str(user.membership_expire_at) if user.membership_expire_at else None,
"created_at": str(user.created_at) if user.created_at else None,
},
},
"message": "登录成功",
}
@router.post("/register")
def register(req: RegisterRequest, db: Session = Depends(get_db)):
# 检查注册设置
reg_setting = db.query(RegisterSetting).first()
if reg_setting and reg_setting.phone_required and not req.phone:
raise HTTPException(status_code=400, detail="手机号为必填项")
if reg_setting and reg_setting.email_required and not req.email:
raise HTTPException(status_code=400, detail="邮箱为必填项")
# 检查用户名是否已存在
existing = db.query(User).filter(User.username == req.username).first()
if existing:
raise HTTPException(status_code=400, detail="用户名已存在")
if req.email:
existing_email = db.query(User).filter(User.email == req.email).first()
if existing_email:
raise HTTPException(status_code=400, detail="邮箱已被注册")
if req.phone:
existing_phone = db.query(User).filter(User.phone == req.phone).first()
if existing_phone:
raise HTTPException(status_code=400, detail="手机号已被注册")
# 短信验证码校验(手机号注册且开启短信验证)
if req.phone and reg_setting and reg_setting.phone_sms_required:
if not req.sms_code:
raise HTTPException(status_code=400, detail="请先获取并填写短信验证码")
vrec = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == "register",
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if not vrec or vrec.code != req.sms_code:
raise HTTPException(status_code=400, detail="短信验证码错误或已过期")
vrec.used = True
user = User(
id=gen_uuid(),
username=req.username,
password_hash=hash_password(req.password),
email=req.email,
phone=req.phone,
avatar=None,
role="user",
status="active",
real_name_status="unverified",
membership_level=0,
created_at=datetime.now(),
updated_at=datetime.now(),
)
db.add(user)
db.flush()
# 创建Token余额记录
from ..models import TokenBalance
balance = TokenBalance(
id=gen_uuid(),
user_id=user.id,
total_consumed=0,
balance=0,
daily_free_used=0,
)
db.add(balance)
token_data = {"sub": user.id, "username": user.username, "role": user.role}
access_token = create_access_token(token_data)
LogService.log_operation(
user_id=user.id,
username=user.username,
action="register",
target="user",
detail=f"新用户 {user.username} 注册成功",
)
db.commit()
return {
"code": 0,
"data": {
"access_token": access_token,
"token_type": "bearer",
"user": {
"id": user.id,
"username": user.username,
"email": user.email or "",
"phone": user.phone or "",
"avatar": user.avatar or "",
"role": user.role,
"status": user.status,
"membership_level": user.membership_level,
"created_at": str(user.created_at) if user.created_at else None,
},
},
"message": "注册成功",
}
@router.post("/send-code")
def send_code(req: SendCodeRequest, db: Session = Depends(get_db)):
"""发送短信验证码(注册 / 重置密码)。公开接口。"""
if not re.match(r'^1[3-9]\d{9}$', req.phone or ''):
raise HTTPException(status_code=400, detail="手机号格式不正确")
purpose = req.purpose or "register"
if purpose not in ("register", "reset_password"):
raise HTTPException(status_code=400, detail="不支持的验证码类型")
config = db.query(SmsConfig).first()
if not config or not config.provider:
raise HTTPException(status_code=400, detail="短信服务未配置,请联系管理员")
# 注册场景:手机号已注册则直接提示,避免浪费短信
if purpose == "register":
existing = db.query(User).filter(User.phone == req.phone).first()
if existing:
raise HTTPException(status_code=400, detail="该手机号已注册,请直接登录")
# 频率限制:同一手机号+用途 60 秒内不重复下发
recent = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == purpose,
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if recent:
age = (datetime.now() - recent.created_at).total_seconds()
if age < 60:
raise HTTPException(status_code=429, detail=f"验证码发送过于频繁,请{int(60 - age)}秒后再试")
code = f"{random.randint(100000, 999999)}"
rec = SmsVerificationCode(
id=gen_uuid(),
phone=req.phone,
code=code,
purpose=purpose,
expires_at=datetime.now() + timedelta(minutes=5),
used=False,
)
db.add(rec)
db.commit()
ok, msg, _ = send_sms_code(config, req.phone, code, purpose)
if not ok:
raise HTTPException(status_code=400, detail=f"短信发送失败:{msg}")
return {"code": 0, "data": {"sent": True}, "message": "验证码已发送"}
@router.post("/reset-password")
def reset_password(req: ResetPasswordRequest, db: Session = Depends(get_db)):
"""通过短信验证码重置密码(公开接口)。"""
user = db.query(User).filter(User.phone == req.phone).first()
if not user:
raise HTTPException(status_code=400, detail="该手机号未注册")
vrec = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == "reset_password",
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if not vrec or vrec.code != req.code:
raise HTTPException(status_code=400, detail="验证码错误或已过期")
user.password_hash = hash_password(req.new_password)
user.updated_at = datetime.now()
vrec.used = True
LogService.log_operation(
user_id=user.id,
username=user.username,
action="reset_password",
target="user",
detail=f"用户 {user.username} 通过短信验证码重置了密码",
)
db.commit()
return {"code": 0, "message": "密码重置成功"}
@router.get("/me")
def get_me(current_user: User = Depends(get_current_user)):
return {
"code": 0,
"data": {
"id": current_user.id,
"username": current_user.username,
"email": current_user.email or "",
"phone": current_user.phone or "",
"avatar": current_user.avatar or "",
"role": current_user.role,
"status": current_user.status,
"real_name_status": current_user.real_name_status,
"membership_level": current_user.membership_level,
"membership_expire_at": str(current_user.membership_expire_at) if current_user.membership_expire_at else None,
"auto_renew": current_user.auto_renew,
"created_at": str(current_user.created_at) if current_user.created_at else None,
},
"message": "ok",
}
@router.post("/logout")
def logout(current_user: User = Depends(get_current_user)):
LogService.log_operation(
user_id=current_user.id,
username=current_user.username,
action="logout",
target="user",
detail=f"用户 {current_user.username} 登出",
)
return {"code": 0, "message": "登出成功"}
@router.get("/settings")
def get_settings(db: Session = Depends(get_db)):
settings = db.query(RegisterSetting).first()
if not settings:
return {"code": 0, "data": {}, "message": "ok"}
return {
"code": 0,
"data": {
"phone_required": settings.phone_required,
"phone_sms_required": settings.phone_sms_required,
"email_required": settings.email_required,
"email_verify_required": settings.email_verify_required,
"real_name_required": settings.real_name_required,
},
"message": "ok",
}