343 lines
12 KiB
Python
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",
|
|
}
|