1019 lines
36 KiB
Python
1019 lines
36 KiB
Python
# 导入所需的模块
|
|
from fastapi import FastAPI, Depends, HTTPException, status, Body, Security, Request
|
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyHeader
|
|
from sqlalchemy import create_engine, Column, String, DateTime, Boolean
|
|
from sqlalchemy.orm import declarative_base
|
|
from sqlalchemy.orm import sessionmaker, Session
|
|
from sqlalchemy.sql import func
|
|
from passlib.context import CryptContext
|
|
from datetime import datetime, timedelta, timezone
|
|
import os
|
|
from typing import Optional
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
import re
|
|
from pydantic import BaseModel, EmailStr, field_validator
|
|
from dotenv import load_dotenv
|
|
import redis
|
|
import json
|
|
import uuid
|
|
import uvicorn
|
|
import secrets
|
|
import string
|
|
from typing import List
|
|
import random
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import random
|
|
import string
|
|
from fastapi import Form
|
|
from supabase import create_client, Client
|
|
import asyncio
|
|
from enum import Enum
|
|
|
|
# 加载 .env 文件中的环境变量
|
|
load_dotenv()
|
|
|
|
# 从环境变量中获取数据库连接信息
|
|
DB_USER = os.getenv("DB_USER")
|
|
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
|
DB_HOST = os.getenv("DB_HOST")
|
|
DB_NAME = os.getenv("DB_NAME")
|
|
DB_PORT = os.getenv("DB_PORT")
|
|
|
|
# 构建数据库连接URL
|
|
SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
|
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
|
|
Base = declarative_base()
|
|
|
|
# Redis配置
|
|
REDIS_HOST = os.getenv('REDIS_HOST')
|
|
REDIS_PORT = int(os.getenv('REDIS_PORT'))
|
|
REDIS_REGISTER_DB = int(os.getenv('REDIS_REGISTER_DB'))
|
|
REDIS_DB = int(os.getenv('REDIS_DB'))
|
|
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD')
|
|
REDIS_API_DB = int(os.getenv('REDIS_API_DB'))
|
|
REDIS_API_USAGE_DB = int(os.getenv('REDIS_API_USAGE_DB'))
|
|
|
|
|
|
# 邮箱配置
|
|
SMTP_SERVER = os.getenv('SMTP_SERVER')
|
|
SMTP_PORT = os.getenv('SMTP_PORT')
|
|
SMTP_USERNAME = os.getenv('SMTP_USERNAME')
|
|
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD')
|
|
|
|
# 创建Redis客户端
|
|
redis_client = redis.Redis(
|
|
host=REDIS_HOST,
|
|
port=REDIS_PORT,
|
|
db=REDIS_DB,
|
|
password=REDIS_PASSWORD
|
|
)
|
|
|
|
redis_register_client = redis.Redis(
|
|
host=REDIS_HOST,
|
|
port=REDIS_PORT,
|
|
db=REDIS_REGISTER_DB,
|
|
password=REDIS_PASSWORD
|
|
)
|
|
# 创建专门用于 API 密钥的 Redis 客户端
|
|
redis_api_client = redis.Redis(
|
|
host=REDIS_HOST,
|
|
port=REDIS_PORT,
|
|
db=REDIS_API_DB,
|
|
password=REDIS_PASSWORD
|
|
)
|
|
|
|
redis_api_usage_client = redis.Redis(
|
|
host=REDIS_HOST,
|
|
port=REDIS_PORT,
|
|
db=REDIS_API_USAGE_DB,
|
|
password=REDIS_PASSWORD
|
|
)
|
|
|
|
# 定义用户模型,继承自Base类
|
|
class User(Base):
|
|
__tablename__ = "user"
|
|
uuid = Column(String(36), primary_key=True, index=True)
|
|
username = Column(String(50), unique=True, index=True, nullable=False)
|
|
email = Column(String(100), unique=True, index=True)
|
|
hashed_password = Column(String(255))
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
last_login = Column(DateTime)
|
|
is_active = Column(Boolean, default=False)
|
|
phone_number = Column(String(20))
|
|
role = Column(String(20), default="admin")
|
|
avatar = Column(String(50))
|
|
email_verification_code = Column(String(6))
|
|
email_verified = Column(Boolean, default=False)
|
|
phone_verification_code = Column(String(6))
|
|
phone_verified = Column(Boolean, default=False)
|
|
|
|
# 创建数据库表
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
# 密码哈希配置
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
# OAuth2密码流配置
|
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
|
|
|
|
# 创建FastAPI应用
|
|
app = FastAPI()
|
|
user_app = FastAPI()
|
|
app.mount("/user", user_app)
|
|
|
|
ALLOWED_ORIGINS = 'https://user.obscura.work'
|
|
|
|
# 添加CORS中间件
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=ALLOWED_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 定义用户创建模型
|
|
class UserCreate(BaseModel):
|
|
username: str
|
|
email: EmailStr
|
|
phone_number: str
|
|
password: str
|
|
|
|
@field_validator('password')
|
|
def password_complexity(cls, v):
|
|
if len(v) < 8:
|
|
raise ValueError('密码长度至少为8个字符')
|
|
if not re.search(r'[A-Z]', v):
|
|
raise ValueError('密码必须包含至少一个大写字母')
|
|
if not re.search(r'[a-z]', v):
|
|
raise ValueError('密码必须包含至少一个小写字母')
|
|
if not re.search(r'\d', v):
|
|
raise ValueError('密码必须包含至少一个数字')
|
|
if not re.search(r'[!@#$%^&*?:]', v):
|
|
raise ValueError('密码必须包含至少一个特殊字符')
|
|
return v
|
|
|
|
@field_validator('phone_number')
|
|
def phone_number_field_validator(cls, v):
|
|
if not re.match(r'^\+?1?\d{9,15}$', v):
|
|
raise ValueError('无效的手机号码')
|
|
return v
|
|
|
|
# 数据库会话依赖
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
# 密码验证函数
|
|
def verify_password(plain_password, hashed_password):
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
|
|
# 密码哈希函数
|
|
def get_password_hash(password):
|
|
return pwd_context.hash(password)
|
|
|
|
# 根据用户名获取用户
|
|
def get_user(db: Session, username: str):
|
|
return db.query(User).filter(User.username == username).first()
|
|
|
|
# 根据邮箱获取用户
|
|
def get_user_by_email(db: Session, email: str):
|
|
return db.query(User).filter(User.email == email).first()
|
|
|
|
# 根据手机号获取用户
|
|
def get_user_by_phone(db: Session, phone_number: str):
|
|
return db.query(User).filter(User.phone_number == phone_number).first()
|
|
|
|
# 用户认证函数
|
|
def authenticate_user(db: Session, username: str, password: str):
|
|
user = get_user(db, username)
|
|
if not user:
|
|
return False
|
|
if not verify_password(password, user.hashed_password):
|
|
return False
|
|
return user
|
|
|
|
# 获取当前用户
|
|
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
try:
|
|
user_info = redis_client.get(f"session:{token}")
|
|
if not user_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="无效的token"
|
|
)
|
|
|
|
user_data = json.loads(user_info)
|
|
|
|
# 从Supabase获取最新的用户信息
|
|
response = supabase.table('users').select('*').eq('id', user_data['id']).execute()
|
|
|
|
if not response.data:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户不存在"
|
|
)
|
|
|
|
return response.data[0]
|
|
|
|
except Exception as e:
|
|
print(f"获取当前用户失败: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="认证失败"
|
|
)
|
|
|
|
# Supabase配置
|
|
supabase_url = os.getenv("SUPABASE_URL")
|
|
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
|
supabase: Client = create_client(supabase_url, supabase_key)
|
|
|
|
# 用户注册路由
|
|
@user_app.post("/register")
|
|
async def register(user: UserCreate):
|
|
try:
|
|
# 随机选择一个头像
|
|
avatars = [
|
|
'picture/cn__02504_.png', 'picture/cn__02505_.png', 'picture/cn__02506_.png',
|
|
'picture/cn__02507_.png', 'picture/cn__02510_.png', 'picture/cn__02511_.png',
|
|
'picture/cn__02512_.png', 'picture/cn__03018_.png', 'picture/cn__03040_.png',
|
|
'picture/cn__03041_.png', 'picture/cn__03042_.png', 'picture/cn__03044_.png',
|
|
'picture/cn__03059_.png'
|
|
]
|
|
random_avatar = random.choice(avatars)
|
|
|
|
# 检查用户名是否已存在
|
|
user_exists = supabase.table('users').select('*').eq('username', user.username).execute()
|
|
if user_exists.data:
|
|
raise HTTPException(status_code=400, detail="用户名已被注册")
|
|
|
|
# 检查邮箱是否已存在
|
|
email_exists = supabase.table('users').select('*').eq('email', user.email).execute()
|
|
if email_exists.data:
|
|
raise HTTPException(status_code=400, detail="邮箱已被注册")
|
|
|
|
# 检查手机号是否已存在
|
|
phone_exists = supabase.table('users').select('*').eq('phone_number', user.phone_number).execute()
|
|
if phone_exists.data:
|
|
raise HTTPException(status_code=400, detail="手机号已被注册")
|
|
|
|
# 创建用户认证
|
|
auth_response = supabase.auth.admin.create_user({
|
|
"email": user.email,
|
|
"phone": user.phone_number,
|
|
"password": user.password,
|
|
"email_confirm": False # 改为false,需要用户验证邮箱
|
|
})
|
|
|
|
if not auth_response.user:
|
|
raise HTTPException(status_code=400, detail="创建用户失败")
|
|
|
|
# 等待一小段时间确保触发器执行完成
|
|
await asyncio.sleep(1)
|
|
|
|
# 更新users表中的用户记录
|
|
user_data = {
|
|
"username": user.username,
|
|
"phone_number": user.phone_number,
|
|
"avatar": random_avatar,
|
|
"is_active": False, # 改为false,等待邮箱验证后激活
|
|
"email_verified": False,
|
|
"phone_verified": False,
|
|
"user_password": user.password,
|
|
}
|
|
|
|
response = supabase.table('users').update(user_data).eq('id', auth_response.user.id).execute()
|
|
|
|
if not response.data:
|
|
# 如果更新用户记录失败,删除认证用户
|
|
supabase.auth.admin.delete_user(auth_response.user.id)
|
|
raise HTTPException(status_code=400, detail="更新用户记录失败")
|
|
|
|
# 将用户信息保存到Redis用于缓存
|
|
redis_user_info = {
|
|
"id": auth_response.user.id,
|
|
"username": user.username,
|
|
"email": user.email,
|
|
"phone_number": user.phone_number,
|
|
"avatar": random_avatar,
|
|
"is_active": "false",
|
|
"email_verified": "false",
|
|
"phone_verified": "false",
|
|
"user_password": user.password, # 使用user_password
|
|
"created_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
redis_register_client.hmset(f"user:{auth_response.user.id}", redis_user_info)
|
|
redis_register_client.expire(f"user:{auth_response.user.id}", 2592000)
|
|
|
|
return {"message": "用户创建成功"}
|
|
|
|
except Exception as e:
|
|
print(f"注册用户时出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"注册失败: {str(e)}")
|
|
|
|
# 用户登录路由
|
|
@user_app.post("/token")
|
|
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
try:
|
|
# 首先从users表获取用户信息
|
|
user_response = supabase.table('users').select('*').eq('email', form_data.username).execute()
|
|
|
|
if not user_response.data:
|
|
print(f"用户不存在: {form_data.username}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户名或密码不正确",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
user_data = user_response.data[0]
|
|
|
|
# 验证密码是否匹配
|
|
if user_data['user_password'] != form_data.password:
|
|
print(f"密码不匹配: {form_data.username}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户名或密码不正确",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 创建会话ID
|
|
session_id = str(uuid.uuid4())
|
|
|
|
# 存储用户信息到Redis
|
|
redis_user_info = {
|
|
"id": user_data['id'],
|
|
"username": user_data['username'],
|
|
"email": user_data['email'],
|
|
"is_active": str(user_data['is_active']),
|
|
"email_verified": str(user_data['email_verified']),
|
|
"phone_verified": str(user_data['phone_verified']),
|
|
"user_password": user_data['user_password']
|
|
}
|
|
redis_client.setex(f"session:{session_id}", 604800, json.dumps(redis_user_info))
|
|
|
|
# 更新最后登录时间
|
|
supabase.table('users').update({
|
|
"last_login": datetime.now(timezone.utc).isoformat()
|
|
}).eq('id', user_data['id']).execute()
|
|
|
|
return {
|
|
"access_token": session_id,
|
|
"token_type": "bearer",
|
|
"user": {
|
|
"id": user_data['id'],
|
|
"username": user_data['username'],
|
|
"email": user_data['email'],
|
|
"avatar": user_data['avatar'],
|
|
"is_active": user_data['is_active'],
|
|
"email_verified": user_data['email_verified'],
|
|
"phone_verified": user_data['phone_verified']
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"登录失败: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户名或密码不正确",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
# 获取当前用户信息���由
|
|
@user_app.get("/me")
|
|
async def read_users_me(current_user: User = Depends(get_current_user)):
|
|
return current_user
|
|
|
|
|
|
# 添加角色枚举类
|
|
class UserRole(str, Enum):
|
|
ADMIN = "admin"
|
|
USER = "user"
|
|
|
|
# 添加用户更新模型
|
|
class UserUpdate(BaseModel):
|
|
username: str
|
|
role: UserRole
|
|
|
|
# 修改编辑用户路由
|
|
@user_app.put("/edit/{user_id}")
|
|
async def edit_user(
|
|
user_id: str,
|
|
user_update: UserUpdate,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
try:
|
|
# 检查当前用户是否有权限编辑(只有管理员可以编辑)
|
|
if current_user.get('role') != 'admin':
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="没有权限执行此操作"
|
|
)
|
|
|
|
# 检查要编辑的用户是否存在
|
|
user_response = supabase.table('users').select('*').eq('id', user_id).execute()
|
|
if not user_response.data:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="用户不存在"
|
|
)
|
|
|
|
# 更新用户信息
|
|
update_response = supabase.table('users').update({
|
|
'username': user_update.username,
|
|
'role': user_update.role
|
|
}).eq('id', user_id).execute()
|
|
|
|
if not update_response.data:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="更新用户信息失败"
|
|
)
|
|
|
|
return {"message": "用户信息更新成功"}
|
|
|
|
except Exception as e:
|
|
print(f"编辑用户时出错: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"更新用户信息失败: {str(e)}"
|
|
)
|
|
|
|
# 修改删除用户路由
|
|
@user_app.delete("/api/users/{user_id}")
|
|
async def delete_user(
|
|
user_id: str,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
try:
|
|
# 检查当前用户是否有权限删除(只有管理员可以删除)
|
|
if current_user.get('role') != 'admin':
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="没有权限执行此操作"
|
|
)
|
|
|
|
# 检查要删除的用户是否存在
|
|
user_response = supabase.table('users').select('*').eq('id', user_id).execute()
|
|
if not user_response.data:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="用户不存在"
|
|
)
|
|
|
|
# 删除用户认证
|
|
auth_response = supabase.auth.admin.delete_user(user_id)
|
|
|
|
# 删除用户记录
|
|
delete_response = supabase.table('users').delete().eq('id', user_id).execute()
|
|
|
|
if not delete_response.data:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="删除用户失败"
|
|
)
|
|
|
|
return {"message": "用户删除成功"}
|
|
|
|
except Exception as e:
|
|
print(f"删除用户时出错: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"删除用户失败: {str(e)}"
|
|
)
|
|
|
|
# 重置密码路由
|
|
@user_app.post("/reset-password")
|
|
async def reset_password(
|
|
email: str = Body(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
user = get_user_by_email(db, email)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|
# 这里应该实现发送重置密码邮件的逻辑
|
|
# 生成一个临时令牌,并将其与用户ID关联存储在Redis中
|
|
reset_token = secrets.token_urlsafe(32)
|
|
redis_client.setex(f"reset_password:{reset_token}", 3600, user.uuid) # 设置1小时过期
|
|
# 发送包含重置链接的邮件(这里省略具体实现)
|
|
return {"message": "密码重置说明已发送到邮箱"}
|
|
|
|
# 获取所有用户路由
|
|
@user_app.get("/api/users")
|
|
async def get_users():
|
|
try:
|
|
# 从 Supabase 获取所有用户,包括 role 字段
|
|
response = supabase.table('users').select('id,username,email,role').execute()
|
|
|
|
if not response.data:
|
|
return []
|
|
|
|
return [
|
|
{
|
|
"id": user['id'],
|
|
"username": user['username'],
|
|
"email": user['email'],
|
|
"role": user.get('role', 'user') # 如果 role 不存在,默认为 'user'
|
|
}
|
|
for user in response.data
|
|
]
|
|
|
|
except Exception as error:
|
|
print('获取用户时出错:', error)
|
|
raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(error)}")
|
|
|
|
# 用户登出路由
|
|
@user_app.post("/logout")
|
|
async def logout(token: str = Depends(oauth2_scheme)):
|
|
redis_client.delete(f"session:{token}")
|
|
return {"message": "成功登出"}
|
|
|
|
# 创建一个新的Pydantic模型来表示仪表盘数据
|
|
class RecentUser(BaseModel):
|
|
username: str
|
|
created_at: datetime
|
|
|
|
class DashboardData(BaseModel):
|
|
total_users: int
|
|
active_users: int
|
|
new_users_today: int
|
|
recent_users: List[RecentUser]
|
|
|
|
@user_app.get("/dashboard", response_model=DashboardData)
|
|
async def get_dashboard_data(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
if not current_user:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="没有访问权限")
|
|
|
|
# 从 Supabase 获取用户统计信息
|
|
total_users = len((supabase.table('users').select('id').execute()).data)
|
|
|
|
# 获取30天内活跃用户
|
|
thirty_days_ago = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
|
|
active_users = len((supabase.table('users')
|
|
.select('id')
|
|
.gte('last_login', thirty_days_ago)
|
|
.execute()).data)
|
|
|
|
# 获取今天新注册的用户
|
|
today = datetime.now(timezone.utc).date().isoformat()
|
|
new_users_today = len((supabase.table('users')
|
|
.select('id')
|
|
.gte('created_at', today)
|
|
.execute()).data)
|
|
|
|
# 获取最近注册的10个用户
|
|
recent_users_response = (supabase.table('users')
|
|
.select('username,created_at')
|
|
.order('created_at', desc=True)
|
|
.limit(10)
|
|
.execute())
|
|
|
|
recent_users_data = [
|
|
RecentUser(
|
|
username=user['username'],
|
|
created_at=datetime.fromisoformat(user['created_at'].replace('Z', '+00:00'))
|
|
)
|
|
for user in recent_users_response.data
|
|
]
|
|
|
|
dashboard_data = DashboardData(
|
|
total_users=total_users,
|
|
active_users=active_users,
|
|
new_users_today=new_users_today,
|
|
recent_users=recent_users_data
|
|
)
|
|
|
|
return dashboard_data
|
|
|
|
except Exception as e:
|
|
print(f"获取仪表盘数据时出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="获取仪表盘数据失败")
|
|
|
|
@user_app.get("/user-stats")
|
|
async def get_user_stats(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
if not current_user:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="没有访问权限")
|
|
|
|
# 获取过去7天的日期列表
|
|
today = datetime.now(timezone.utc).date()
|
|
dates = [(today - timedelta(days=i)).isoformat() for i in range(6, -1, -1)]
|
|
|
|
# 查询每天的新用户数
|
|
daily_new_users = []
|
|
for date in dates:
|
|
next_date = (datetime.fromisoformat(date) + timedelta(days=1)).isoformat()
|
|
count = len((supabase.table('users')
|
|
.select('id')
|
|
.gte('created_at', date)
|
|
.lt('created_at', next_date)
|
|
.execute()).data)
|
|
daily_new_users.append(count)
|
|
|
|
stats = {
|
|
"dates": dates,
|
|
"daily_new_users": daily_new_users,
|
|
"total_users": len((supabase.table('users').select('id').execute()).data)
|
|
}
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
print(f"获取用户统计数据时出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="获取用户统计数据失败")
|
|
|
|
|
|
# 定义API密钥头部
|
|
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
|
|
|
|
# 定义 base62 字符集
|
|
BASE62 = string.digits + string.ascii_letters
|
|
|
|
# UUID 转 base62 函数
|
|
def uuid_to_base62(uuid_value):
|
|
uuid_int = uuid.UUID(uuid_value).int
|
|
result = ""
|
|
while uuid_int:
|
|
uuid_int, remainder = divmod(uuid_int, 62)
|
|
result = BASE62[remainder] + result
|
|
return result.rjust(22, '0') # 补齐到22位
|
|
|
|
# 修改生成API密钥的函数
|
|
def generate_api_key():
|
|
uuid_value = str(uuid.uuid4())
|
|
base62_value = uuid_to_base62(uuid_value)
|
|
return f"obs-{base62_value}"
|
|
|
|
# 验证API密钥的函数
|
|
async def get_api_key(api_key: str = Security(api_key_header)):
|
|
if api_key and api_key.startswith("Bearer "):
|
|
key = api_key.split(" ")[1]
|
|
if key.startswith("obs-"):
|
|
return key
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="无效的API密钥",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 修改创建API密钥的路由
|
|
@user_app.post("/api-keys")
|
|
async def create_api_key(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
new_key = generate_api_key()
|
|
created_at = datetime.now(timezone.utc)
|
|
expires_at = created_at + timedelta(days=30)
|
|
|
|
# 在Redis中初始化API密钥信息
|
|
redis_api_client.hset(f"api_key:{new_key}", mapping={
|
|
"user_id": current_user['id'],
|
|
"is_active": "1",
|
|
"created_at": created_at.isoformat(),
|
|
"expires_at": expires_at.isoformat(),
|
|
"total_tokens": 10000000000000,
|
|
})
|
|
redis_api_usage_client.hset(f"api_key:{new_key}", mapping={
|
|
"user_id": current_user['id'],
|
|
"is_active": "1",
|
|
"created_at": created_at.isoformat(),
|
|
"expires_at": expires_at.isoformat(),
|
|
"total_tokens": 10000000000000,
|
|
"tokens_used": 0,
|
|
})
|
|
|
|
# 添加到过期集合
|
|
redis_api_client.zadd("expiring_keys", {new_key: expires_at.timestamp()})
|
|
|
|
return {"api_key": new_key, "expires_at": expires_at}
|
|
except Exception as e:
|
|
print(f"创建API密钥时出错: {e}")
|
|
raise HTTPException(status_code=500, detail="服务器内部错误")
|
|
|
|
|
|
# 获取 API 使用量统计的路由
|
|
@user_app.get("/api-usage")
|
|
async def get_api_usage(current_user: dict = Depends(get_current_user)):
|
|
keys = redis_api_usage_client.keys(f"api_key:*")
|
|
usage_stats = {}
|
|
for key in keys:
|
|
key_info = redis_api_usage_client.hgetall(key)
|
|
if key_info and key_info.get(b'user_id', b'').decode() == current_user['id']:
|
|
api_key = key.decode().split(':')[1] # 提取 obs-xxx 部分
|
|
usage_stats[api_key] = {
|
|
"total_tokens": int(key_info.get(b"total_tokens", 0)),
|
|
"tokens_used": int(key_info.get(b"tokens_used", 0)),
|
|
"created_at": key_info.get(b"created_at", b"").decode(),
|
|
"expires_at": key_info.get(b"expires_at", b"").decode(),
|
|
"last_used_at": key_info.get(b"last_used_at", b"").decode(),
|
|
}
|
|
return usage_stats
|
|
|
|
# 更新 API 使用量的函数
|
|
async def update_api_usage(api_key: str, tokens_used: int, endpoint: str):
|
|
now = datetime.now(timezone.utc)
|
|
today = now.date().isoformat()
|
|
redis_key = f"api_key:{api_key}"
|
|
pipe = redis_api_usage_client.pipeline() # 使用 redis_api_usage_client
|
|
pipe.hincrby(redis_key, "tokens_used", tokens_used)
|
|
pipe.hset(redis_key, "last_used_at", now.isoformat())
|
|
pipe.incrby(f"daily_usage:{api_key}:{today}", tokens_used)
|
|
pipe.execute()
|
|
|
|
@user_app.get("/model-usage")
|
|
async def get_model_usage(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
keys = redis_api_usage_client.keys(f"api_key:*")
|
|
usage_stats = {}
|
|
for key in keys:
|
|
key_info = redis_api_usage_client.hgetall(key)
|
|
if key_info and key_info.get(b'user_id', b'').decode() == current_user['id']:
|
|
for field, value in key_info.items():
|
|
field_str = field.decode()
|
|
if field_str.endswith("_tokens_used"):
|
|
model_name = field_str.replace("_tokens_used", "")
|
|
if model_name not in usage_stats:
|
|
usage_stats[model_name] = {
|
|
"calls": 0,
|
|
"last_called_at": None
|
|
}
|
|
usage_stats[model_name]["calls"] += int(value)
|
|
last_called_at = key_info.get(f"{model_name}_last_used_at".encode(), b"").decode()
|
|
if last_called_at:
|
|
if usage_stats[model_name]["last_called_at"] is None or last_called_at > usage_stats[model_name]["last_called_at"]:
|
|
usage_stats[model_name]["last_called_at"] = last_called_at
|
|
|
|
return usage_stats
|
|
except Exception as e:
|
|
print(f"获取模型使用情况时出错: {e}")
|
|
raise HTTPException(status_code=500, detail="服务器内部错误")
|
|
|
|
|
|
|
|
# 添加清除缓存的路由
|
|
@user_app.post("/clear-cache")
|
|
async def clear_cache(current_user: User = Depends(get_current_user)):
|
|
try:
|
|
# 获取当前用户的所有API密钥
|
|
user_keys = redis_api_client.keys(f"api_key:*")
|
|
updated_keys = []
|
|
removed_keys = []
|
|
|
|
for key in user_keys:
|
|
key_info = redis_api_client.hgetall(key)
|
|
if key_info and key_info.get(b'user_id', b'').decode() == current_user.uuid:
|
|
api_key = key.decode().split(':')[1]
|
|
|
|
if not key_info:
|
|
# 如果 Redis 中已经没有这个 API 密钥的信息,将其添加到需要移除的列表中
|
|
removed_keys.append(api_key)
|
|
else:
|
|
# 重置使用统计信息
|
|
redis_api_client.hset(key, "total_tokens", "0")
|
|
redis_api_client.hset(key, "tokens_used", "0")
|
|
|
|
# 清除每日使用量统计
|
|
daily_usage_keys = redis_api_usage_client.keys(f"daily_usage:{api_key}:*")
|
|
if daily_usage_keys:
|
|
redis_api_usage_client.delete(*daily_usage_keys)
|
|
|
|
updated_keys.append(api_key)
|
|
|
|
return {
|
|
"message": "API 使用统计已成功重置",
|
|
"updated_keys": updated_keys,
|
|
"removed_keys": removed_keys
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"重置缓存时发生错误: {str(e)}")
|
|
|
|
@user_app.get("/api-keys")
|
|
async def get_api_keys(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
keys = redis_api_client.keys(f"api_key:*")
|
|
valid_keys = []
|
|
for key in keys:
|
|
key_info = redis_api_client.hgetall(key)
|
|
if key_info and key_info.get(b'user_id', b'').decode() == current_user['id']: # 使用 current_user['id'] 替代 current_user.uuid
|
|
valid_keys.append({
|
|
"key": key.decode().replace("api_key:", ""),
|
|
"created_at": key_info[b"created_at"].decode(),
|
|
"expires_at": key_info[b"expires_at"].decode()
|
|
})
|
|
return valid_keys
|
|
except Exception as e:
|
|
print(f"获取API密钥时出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="获取API密钥失败")
|
|
|
|
@user_app.delete("/api-keys/{key_id}")
|
|
async def revoke_api_key(key_id: str, current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
# 确保 key_id 的格式正确
|
|
if not key_id.startswith("api_key:"):
|
|
key_id = f"api_key:{key_id}"
|
|
|
|
# 从 Redis 获取 API 密信息
|
|
key_info = redis_api_client.hgetall(key_id)
|
|
|
|
if not key_info:
|
|
raise HTTPException(status_code=404, detail="API 密钥未找到")
|
|
|
|
# 检查 API 密钥是否属于当前用户
|
|
if key_info.get(b'user_id', b'').decode() != current_user['id']: # 使用 current_user['id'] 替代 current_user.uuid
|
|
raise HTTPException(status_code=403, detail="无权删除此 API 密钥")
|
|
|
|
# 删除 API 密钥
|
|
deleted = redis_api_client.delete(key_id)
|
|
|
|
# 从 expiring_keys 中移除该密钥
|
|
redis_api_client.zrem("expiring_keys", key_id.replace("api_key:", ""))
|
|
|
|
if deleted:
|
|
return {"message": "API 密钥已成功删除", "action": "clear_api_info"}
|
|
else:
|
|
raise HTTPException(status_code=500, detail="删除 API 密钥时发生错误")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
print(f"删除API密钥时出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="删除API密钥失败")
|
|
|
|
|
|
@user_app.post("/send-verification-email")
|
|
async def send_verification_email_route(current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
# 检查用户是否已经验证过邮箱
|
|
if current_user.get('email_verified'):
|
|
raise HTTPException(status_code=400, detail="邮箱已经验证过了")
|
|
|
|
# 使用 OTP 发送验证邮件
|
|
auth_response = supabase.auth.sign_in_with_otp({
|
|
"email": current_user['email'],
|
|
"options": {
|
|
"shouldCreateUser": False, # 不要创建新用户
|
|
}
|
|
})
|
|
|
|
return {"message": "验证邮件已发送,请查收"}
|
|
|
|
except Exception as e:
|
|
print(f"发送验证邮件过程中发生错误: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="发送验证邮件失败")
|
|
|
|
@user_app.post("/verify-email")
|
|
async def verify_email(verification_code: str = Form(...), current_user: dict = Depends(get_current_user)):
|
|
try:
|
|
# 使用 OTP 验证邮箱
|
|
verify_response = supabase.auth.verify_otp({
|
|
"email": current_user['email'],
|
|
"token": verification_code,
|
|
"type": 'email' # 使用 email 类型
|
|
})
|
|
|
|
if not verify_response:
|
|
raise HTTPException(status_code=400, detail="验证码无效或已过期")
|
|
|
|
try:
|
|
# 直接更新users表中的验证状态
|
|
update_response = supabase.table('users').update({
|
|
'email_verified': True,
|
|
'is_active': True
|
|
}).eq('id', current_user['id']).execute()
|
|
|
|
if not update_response.data:
|
|
raise HTTPException(status_code=500, detail="更新用户状态失败")
|
|
|
|
# 更新Redis中的用户会话信息
|
|
session_keys = redis_client.keys("session:*")
|
|
for session_key in session_keys:
|
|
user_info = json.loads(redis_client.get(session_key))
|
|
if user_info.get("id") == current_user['id']:
|
|
user_info["is_active"] = "true"
|
|
user_info["email_verified"] = "true"
|
|
redis_client.setex(session_key, 604800, json.dumps(user_info))
|
|
|
|
# 获取更新后的用户信息
|
|
updated_user = supabase.table('users').select('*').eq('id', current_user['id']).execute()
|
|
|
|
if not updated_user.data:
|
|
raise HTTPException(status_code=404, detail="无法获取更新后的用户信息")
|
|
|
|
return {
|
|
"message": "邮箱验证成功",
|
|
"user": {
|
|
"id": updated_user.data[0]['id'],
|
|
"username": updated_user.data[0]['username'],
|
|
"email": updated_user.data[0]['email'],
|
|
"is_active": updated_user.data[0]['is_active'],
|
|
"email_verified": updated_user.data[0]['email_verified'],
|
|
"avatar": updated_user.data[0]['avatar']
|
|
}
|
|
}
|
|
|
|
except Exception as supabase_error:
|
|
print(f"更新用户状态失败: {str(supabase_error)}")
|
|
raise HTTPException(status_code=500, detail="更新验证状态失败")
|
|
|
|
except HTTPException as http_error:
|
|
raise http_error
|
|
except Exception as e:
|
|
print(f"验证邮箱过程中发生错误: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="验证邮箱失败")
|
|
|
|
# 获取单个用户信息路由
|
|
@user_app.get("/api/users/{user_id}")
|
|
async def get_user_by_id(
|
|
user_id: str,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
try:
|
|
# 检查当前用户是否有权限(只有管理员可以查看用户详情)
|
|
if current_user.get('role') != 'admin':
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="没有权限执行此操作"
|
|
)
|
|
|
|
# 从 Supabase 获取用户信息
|
|
response = supabase.table('users').select('*').eq('id', user_id).execute()
|
|
|
|
if not response.data:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="用户不存在"
|
|
)
|
|
|
|
user_data = response.data[0]
|
|
return {
|
|
"id": user_data['id'],
|
|
"username": user_data['username'],
|
|
"email": user_data['email'],
|
|
"role": user_data.get('role', 'user') # 如果 role 不存在,默认为 'user'
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as error:
|
|
print('获取用户信息时出错:', error)
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"获取用户信息失败: {str(error)}"
|
|
)
|
|
|
|
# 在现有路由之前添加新的搜索路由
|
|
@user_app.get("/api/users-search")
|
|
async def search_users(
|
|
query: str,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
try:
|
|
# 检查权限
|
|
if current_user.get('role') != 'admin':
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail="没有权限执行此操作"
|
|
)
|
|
|
|
# 构建搜索查询
|
|
search_query = supabase.table('users').select('id,username,email,role')
|
|
|
|
# 如果有搜索词,添加过滤条件
|
|
if query:
|
|
search_query = search_query.or_(
|
|
f"username.ilike.%{query}%,email.ilike.%{query}%,role.ilike.%{query}%"
|
|
)
|
|
|
|
response = search_query.execute()
|
|
|
|
if not response.data:
|
|
return []
|
|
|
|
return [
|
|
{
|
|
"id": user['id'],
|
|
"username": user['username'],
|
|
"email": user['email'],
|
|
"role": user.get('role', 'user')
|
|
}
|
|
for user in response.data
|
|
]
|
|
|
|
except Exception as error:
|
|
print('搜索用户时出错:', error)
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"搜索用户失败: {str(error)}"
|
|
)
|
|
|
|
# 主程序入口
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |