新增用户注册功能
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_roles
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import User, UserRole, UserStatus
|
||||
from app.schemas.user import AdminUserListResponse, UserAdminReviewRequest, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/users")
|
||||
|
||||
|
||||
@router.get("/", response_model=AdminUserListResponse)
|
||||
async def list_users_for_review(
|
||||
status_filter: UserStatus = Query(default=UserStatus.PENDING, alias="status"),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
) -> AdminUserListResponse:
|
||||
users = await user_crud.list_users_by_status(db, status=status_filter)
|
||||
return AdminUserListResponse(items=list(users), total=len(users))
|
||||
|
||||
|
||||
async def _get_review_user(db: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
user = await user_crud.get_by_id(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
if user.role == UserRole.ADMIN:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="管理员账号不允许审核")
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/{user_id}/approve", response_model=UserResponse)
|
||||
async def approve_user(
|
||||
user_id: uuid.UUID,
|
||||
review: UserAdminReviewRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
) -> UserResponse:
|
||||
if review.action != "approve":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作")
|
||||
user = await _get_review_user(db, user_id)
|
||||
if user.status != UserStatus.PENDING:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许审核待审核用户")
|
||||
user = await user_crud.approve_user(db, user, admin_id=current_user.id)
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/{user_id}/reject", response_model=UserResponse)
|
||||
async def reject_user(
|
||||
user_id: uuid.UUID,
|
||||
review: UserAdminReviewRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
) -> UserResponse:
|
||||
if review.action not in ("reject", "approve"):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作")
|
||||
user = await _get_review_user(db, user_id)
|
||||
if user.status != UserStatus.PENDING:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许审核待审核用户")
|
||||
user = await user_crud.reject_user(db, user, admin_id=current_user.id)
|
||||
return user
|
||||
@@ -1,40 +1,66 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from fastapi import File, UploadFile
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
from app.core.security import create_access_token, verify_password
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import user as user_crud
|
||||
from app.schemas.user import Token, UserRead
|
||||
from app.models.user import UserRole, UserStatus
|
||||
from app.schemas.user import Token, UserRead, UserRegisterRequest, UserSelfUpdate, UserUpdate
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str = Field(min_length=1)
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=1)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
AVATAR_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads" / "avatars"
|
||||
AVATAR_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
payload: UserRegisterRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if payload.role == UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许注册管理员账号")
|
||||
existing = await user_crud.get_by_email(db, payload.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="邮箱已注册")
|
||||
await user_crud.create_pending_user(db, payload)
|
||||
return {"message": "注册成功,等待管理员审核"}
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
payload: LoginRequest, db: AsyncSession = Depends(get_db_session)
|
||||
) -> Token:
|
||||
db_user = await user_crud.get_by_username(db, payload.username)
|
||||
if not db_user or not verify_password(payload.password, db_user.hashed_password):
|
||||
db_user = await user_crud.get_by_email(db, payload.email)
|
||||
if not db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
detail="账号不存在",
|
||||
)
|
||||
if not db_user.is_active:
|
||||
if not verify_password(payload.password, db_user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Inactive user",
|
||||
detail="密码错误",
|
||||
)
|
||||
if db_user.status != UserStatus.ACTIVE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="账号未审核或不可用",
|
||||
)
|
||||
|
||||
access_token = create_access_token(
|
||||
user_id=str(db_user.id),
|
||||
role=db_user.role,
|
||||
role=db_user.role.value if hasattr(db_user.role, "value") else db_user.role,
|
||||
expires_minutes=None,
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
@@ -43,3 +69,60 @@ async def login_for_access_token(
|
||||
@router.get("/me", response_model=UserRead)
|
||||
async def read_me(current_user=Depends(get_current_user)) -> UserRead:
|
||||
return current_user
|
||||
|
||||
|
||||
@router.patch("/me", response_model=UserRead)
|
||||
async def update_me(
|
||||
payload: UserSelfUpdate,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> UserRead:
|
||||
if payload.password:
|
||||
if not payload.current_password or not verify_password(payload.current_password, current_user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前密码不正确")
|
||||
update_data = {
|
||||
"full_name": payload.full_name if payload.full_name is not None else current_user.full_name,
|
||||
"department": payload.department if payload.department is not None else current_user.department,
|
||||
"password": payload.password if payload.password else None,
|
||||
"avatar_url": payload.avatar_url if payload.avatar_url is not None else current_user.avatar_url,
|
||||
}
|
||||
updated = await user_crud.update_user(db, current_user, UserUpdate(**update_data))
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/me/avatar", response_model=UserRead)
|
||||
async def upload_avatar(
|
||||
file: UploadFile = File(...),
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> UserRead:
|
||||
AVATAR_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
user_dir = AVATAR_ROOT / str(current_user.id)
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = Path(file.filename).suffix or ".png"
|
||||
filename = f"{uuid.uuid4()}{ext}"
|
||||
dest = user_dir / filename
|
||||
content = await file.read()
|
||||
dest.write_bytes(content)
|
||||
# Optionally clean old files
|
||||
for old in user_dir.iterdir():
|
||||
if old.name != filename:
|
||||
try:
|
||||
old.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
rel_url = f"/api/v1/auth/avatar/{current_user.id}/{filename}"
|
||||
updated = await user_crud.update_user(
|
||||
db,
|
||||
current_user,
|
||||
UserUpdate(avatar_url=rel_url),
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
@router.get("/avatar/{user_id}/{filename}")
|
||||
async def get_avatar(user_id: str, filename: str):
|
||||
file_path = AVATAR_ROOT / user_id / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Avatar not found")
|
||||
return FileResponse(file_path)
|
||||
|
||||
@@ -48,7 +48,7 @@ async def create_comment(
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
action="CREATE_COMMENT",
|
||||
detail=f"Comment created by {current_user.username}",
|
||||
detail=f"Comment created by {current_user.full_name}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs, constants
|
||||
from app.api.v1 import auth, users, admin_users, studies, sites, members, comments, attachments, audit_logs, milestones, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs, constants
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
|
||||
api_router.include_router(sites.router, prefix="/studies/{study_id}/sites", tags=["sites"])
|
||||
|
||||
@@ -32,11 +32,11 @@ async def create_user(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> UserRead:
|
||||
existing = await user_crud.get_by_username(db, user_in.username)
|
||||
existing = await user_crud.get_by_email(db, user_in.email)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Username already exists",
|
||||
detail="Email already exists",
|
||||
)
|
||||
user = await user_crud.create_user(db, user_in)
|
||||
return user
|
||||
@@ -64,7 +64,7 @@ async def delete_user(
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
):
|
||||
admin_password = payload.get("admin_password")
|
||||
if not admin_password or not verify_password(admin_password, current_user.hashed_password):
|
||||
if not admin_password or not verify_password(admin_password, current_user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员密码错误")
|
||||
db_user = await user_crud.get_by_id(db, user_id)
|
||||
if not db_user:
|
||||
|
||||
Reference in New Issue
Block a user