注册码 + 管理员系统:
User 模型新增 is_admin 字段
新增 InviteCode 模型(邀请码表)
注册接口必须提供有效邀请码,使用后自动标记
管理员接口:查看所有用户、启用/禁用用户、生成/删除邀请码
前端新增管理面板页面 /admin,导航栏对管理员显示入口
注册页面新增邀请码输入框
选择性超话签到:
新增 GET /api/v1/accounts/{id}/topics 接口获取超话列表
POST /signin 接口支持 {"topic_indices": [0,1,3]} 选择性签到
新增超话选择页面 /accounts/{id}/topics,支持全选/手动勾选
账号详情页新增"选择超话签到"按钮
This commit is contained in:
Binary file not shown.
@@ -7,12 +7,14 @@ from fastapi import FastAPI, Depends, HTTPException, status, Security
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, func as sa_func
|
||||
import uvicorn
|
||||
import os
|
||||
import logging
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
|
||||
from shared.models import get_db, User
|
||||
from shared.models import get_db, User, InviteCode
|
||||
from shared.config import shared_settings
|
||||
from auth_service.app.models.database import create_tables
|
||||
from auth_service.app.schemas.user import (
|
||||
@@ -113,11 +115,26 @@ async def health_check():
|
||||
@app.post("/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Register a new user account and return tokens
|
||||
Register a new user account and return tokens.
|
||||
Requires a valid invite code.
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
|
||||
# Validate invite code
|
||||
result = await db.execute(
|
||||
select(InviteCode).where(
|
||||
InviteCode.code == user_data.invite_code,
|
||||
InviteCode.is_used == False,
|
||||
)
|
||||
)
|
||||
invite = result.scalar_one_or_none()
|
||||
if not invite:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="邀请码无效或已被使用",
|
||||
)
|
||||
|
||||
# Check if user already exists - optimized with single query
|
||||
# Check if user already exists
|
||||
email_user, username_user = await auth_service.check_user_exists(user_data.email, user_data.username)
|
||||
|
||||
if email_user:
|
||||
@@ -135,6 +152,12 @@ async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db
|
||||
# Create new user
|
||||
try:
|
||||
user = await auth_service.create_user(user_data)
|
||||
|
||||
# Mark invite code as used
|
||||
invite.is_used = True
|
||||
invite.used_by = str(user.id)
|
||||
invite.used_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
# Create tokens for auto-login
|
||||
access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
|
||||
@@ -335,3 +358,128 @@ async def wx_login(body: WxLoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
expires_in=3600,
|
||||
user=UserResponse.from_orm(user),
|
||||
)
|
||||
|
||||
|
||||
# ===================== Admin Endpoints =====================
|
||||
|
||||
async def require_admin(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
"""Dependency: require admin user."""
|
||||
payload = decode_access_token(credentials.credentials)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
user_id = payload.get("sub")
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/admin/users")
|
||||
async def admin_list_users(
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""管理员查看所有用户"""
|
||||
result = await db.execute(select(User).order_by(User.created_at.desc()))
|
||||
users = result.scalars().all()
|
||||
return {
|
||||
"success": True,
|
||||
"data": [
|
||||
{
|
||||
"id": str(u.id),
|
||||
"username": u.username,
|
||||
"email": u.email,
|
||||
"is_active": u.is_active,
|
||||
"is_admin": u.is_admin,
|
||||
"created_at": str(u.created_at) if u.created_at else None,
|
||||
}
|
||||
for u in users
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@app.put("/admin/users/{user_id}/toggle")
|
||||
async def admin_toggle_user(
|
||||
user_id: str,
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""管理员启用/禁用用户"""
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if str(user.id) == str(admin.id):
|
||||
raise HTTPException(status_code=400, detail="不能禁用自己")
|
||||
user.is_active = not user.is_active
|
||||
await db.commit()
|
||||
return {"success": True, "is_active": user.is_active}
|
||||
|
||||
|
||||
@app.post("/admin/invite-codes")
|
||||
async def admin_create_invite_code(
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""管理员生成邀请码"""
|
||||
code = secrets.token_urlsafe(8)[:12].upper()
|
||||
invite = InviteCode(code=code, created_by=str(admin.id))
|
||||
db.add(invite)
|
||||
await db.commit()
|
||||
await db.refresh(invite)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"id": str(invite.id),
|
||||
"code": invite.code,
|
||||
"created_at": str(invite.created_at),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/admin/invite-codes")
|
||||
async def admin_list_invite_codes(
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""管理员查看所有邀请码"""
|
||||
result = await db.execute(select(InviteCode).order_by(InviteCode.created_at.desc()))
|
||||
codes = result.scalars().all()
|
||||
return {
|
||||
"success": True,
|
||||
"data": [
|
||||
{
|
||||
"id": str(c.id),
|
||||
"code": c.code,
|
||||
"is_used": c.is_used,
|
||||
"used_by": c.used_by,
|
||||
"created_at": str(c.created_at) if c.created_at else None,
|
||||
"used_at": str(c.used_at) if c.used_at else None,
|
||||
}
|
||||
for c in codes
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/admin/invite-codes/{code_id}")
|
||||
async def admin_delete_invite_code(
|
||||
code_id: str,
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""管理员删除未使用的邀请码"""
|
||||
result = await db.execute(select(InviteCode).where(InviteCode.id == code_id))
|
||||
invite = result.scalar_one_or_none()
|
||||
if not invite:
|
||||
raise HTTPException(status_code=404, detail="邀请码不存在")
|
||||
if invite.is_used:
|
||||
raise HTTPException(status_code=400, detail="已使用的邀请码不能删除")
|
||||
await db.delete(invite)
|
||||
await db.commit()
|
||||
return {"success": True}
|
||||
|
||||
Binary file not shown.
@@ -16,6 +16,7 @@ class UserBase(BaseModel):
|
||||
class UserCreate(UserBase):
|
||||
"""Schema for user registration request"""
|
||||
password: str = Field(..., min_length=8, description="Password (min 8 characters)")
|
||||
invite_code: str = Field(..., min_length=1, description="注册邀请码")
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""Schema for user login request"""
|
||||
@@ -35,6 +36,7 @@ class UserResponse(BaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
created_at: datetime
|
||||
is_active: bool
|
||||
is_admin: bool = False
|
||||
wx_openid: Optional[str] = None
|
||||
wx_nickname: Optional[str] = None
|
||||
wx_avatar: Optional[str] = None
|
||||
|
||||
Reference in New Issue
Block a user