v1.0.0提交

This commit is contained in:
2026-03-31 16:03:55 +08:00
parent 5f3945ae03
commit dce843fd9d
25 changed files with 1702 additions and 0 deletions

167
backend/routers/v1/auth.py Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PerToolBox Server - 认证路由
Copyright (C) 2024 Sea Network Technology Studio
Author: Canglan <admin@sea-studio.top>
License: AGPL v3
"""
import re
from fastapi import APIRouter, HTTPException, status
from sqlalchemy.orm import Session
from ...dependencies import DbDependency
from ...models import User
from ...schemas import (
SendCodeRequest, RegisterRequest, LoginRequest, UserResponse
)
from ...utils.redis_client import redis_client
from ...utils.security import generate_verify_code, hash_password, verify_password, create_access_token
from ...utils.sms import send_sms
from ...utils.email import send_email
from ...utils.logger import logger
from ...middleware.rate_limit import rate_limit
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
def is_phone(account: str) -> bool:
return re.match(r'^1[3-9]\d{9}$', account) is not None
def is_email(account: str) -> bool:
return re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', account) is not None
@router.post("/send-code")
@rate_limit(requests=1, period=60) # 同一账号每分钟1次
async def send_verify_code(request: SendCodeRequest, db: DbDependency):
"""发送验证码"""
account = request.account
account_type = request.type
# 验证账号格式
if account_type == "phone" and not is_phone(account):
raise HTTPException(status_code=400, detail="手机号格式错误")
if account_type == "email" and not is_email(account):
raise HTTPException(status_code=400, detail="邮箱格式错误")
# 检查是否已存在(注册时使用)
# 这里只负责发送,不检查是否存在
# 生成验证码
code = generate_verify_code()
# 存储到 Redis5分钟有效
redis_key = f"verify:code:{account}"
redis_client.set(redis_key, code, expire=300)
# 发送验证码
success = False
if account_type == "phone":
success = send_sms(account, code)
else:
success = await send_email(account, code)
if not success:
raise HTTPException(status_code=500, detail="验证码发送失败")
logger.info(f"验证码已发送: {account} -> {code}")
return {"success": True, "message": "验证码已发送"}
@router.post("/register")
@rate_limit(requests=5, period=60)
async def register(request: RegisterRequest, db: DbDependency):
"""注册(手机号/邮箱 + 验证码)"""
account = request.account
code = request.code
password = request.password
# 验证验证码
redis_key = f"verify:code:{account}"
saved_code = redis_client.get(redis_key)
if not saved_code or saved_code != code:
raise HTTPException(status_code=400, detail="验证码错误或已过期")
# 检查用户是否已存在
if is_phone(account):
existing = db.query(User).filter(User.phone == account).first()
if existing:
raise HTTPException(status_code=400, detail="手机号已注册")
user = User(phone=account, password_hash=hash_password(password))
elif is_email(account):
existing = db.query(User).filter(User.email == account).first()
if existing:
raise HTTPException(status_code=400, detail="邮箱已注册")
user = User(email=account, password_hash=hash_password(password))
else:
raise HTTPException(status_code=400, detail="账号格式错误")
db.add(user)
db.commit()
db.refresh(user)
# 删除已使用的验证码
redis_client.delete(redis_key)
# 生成 token
token = create_access_token({"sub": str(user.id)})
logger.info(f"新用户注册: {account}")
return {
"success": True,
"token": token,
"user": UserResponse.model_validate(user)
}
@router.post("/login")
@rate_limit(requests=10, period=60)
async def login(request: LoginRequest, db: DbDependency):
"""登录(支持验证码登录或密码登录)"""
account = request.account
password = request.password
code = request.code
# 查找用户
user = None
if is_phone(account):
user = db.query(User).filter(User.phone == account).first()
elif is_email(account):
user = db.query(User).filter(User.email == account).first()
else:
raise HTTPException(status_code=400, detail="账号格式错误")
if not user:
raise HTTPException(status_code=400, detail="用户不存在")
# 验证码登录
if code:
redis_key = f"verify:code:{account}"
saved_code = redis_client.get(redis_key)
if not saved_code or saved_code != code:
raise HTTPException(status_code=400, detail="验证码错误或已过期")
redis_client.delete(redis_key)
# 密码登录
elif password:
if not verify_password(password, user.password_hash):
raise HTTPException(status_code=400, detail="密码错误")
else:
raise HTTPException(status_code=400, detail="请提供验证码或密码")
# 更新最后登录时间
from datetime import datetime
user.last_login = datetime.now()
db.commit()
token = create_access_token({"sub": str(user.id)})
logger.info(f"用户登录: {account}")
return {
"success": True,
"token": token,
"user": UserResponse.model_validate(user)
}
@router.post("/wechat/login")
async def wechat_login():
"""微信公众号登录(预留)"""
# TODO: 实现公众号 OAuth2 登录
raise HTTPException(status_code=501, detail="功能开发中")