9eec21a5b2
Backend API (38 routes): - providers: full CRUD + test connection + list collections + API key masking - trackers: full CRUD + trigger + history + test-periodic/memory - tracking-configs: full CRUD with Pydantic models, provider_type filter - template-configs: full CRUD + preview + preview-raw with two-pass validation - targets: full CRUD + test notification + config masking - telegram-bots: full CRUD + chat discovery + token endpoint - users: full admin CRUD + password reset + self-delete protection - status: dashboard endpoint with providers/trackers/targets/events counts Frontend pages updated: - Dashboard with animated stat cards and event timeline - Providers with proper components, delete confirm, snackbar - Trackers/targets/tracking-configs/template-configs/telegram-bots/users all use PageHeader, Card, Loading, MdiIcon with correct i18n keys Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
3.1 KiB
Python
102 lines
3.1 KiB
Python
"""User management API routes (admin only)."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlmodel import select
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
import bcrypt
|
|
|
|
from ..auth.dependencies import require_admin
|
|
from ..database.engine import get_session
|
|
from ..database.models import User
|
|
|
|
router = APIRouter(prefix="/api/users", tags=["users"])
|
|
|
|
|
|
class UserCreate(BaseModel):
|
|
username: str
|
|
password: str
|
|
role: str = "user"
|
|
|
|
|
|
class UserUpdate(BaseModel):
|
|
username: str | None = None
|
|
password: str | None = None
|
|
role: str | None = None
|
|
|
|
|
|
@router.get("")
|
|
async def list_users(
|
|
admin: User = Depends(require_admin),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""List all users (admin only)."""
|
|
result = await session.exec(select(User))
|
|
return [
|
|
{"id": u.id, "username": u.username, "role": u.role, "created_at": u.created_at.isoformat()}
|
|
for u in result.all()
|
|
]
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
async def create_user(
|
|
body: UserCreate,
|
|
admin: User = Depends(require_admin),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Create a new user (admin only)."""
|
|
# Check for duplicate username
|
|
result = await session.exec(select(User).where(User.username == body.username))
|
|
if result.first():
|
|
raise HTTPException(status_code=409, detail="Username already exists")
|
|
|
|
user = User(
|
|
username=body.username,
|
|
hashed_password=bcrypt.hashpw(body.password.encode(), bcrypt.gensalt()).decode(),
|
|
role=body.role if body.role in ("admin", "user") else "user",
|
|
)
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
return {"id": user.id, "username": user.username, "role": user.role}
|
|
|
|
|
|
class ResetPasswordRequest(BaseModel):
|
|
new_password: str
|
|
|
|
|
|
@router.put("/{user_id}/password")
|
|
async def reset_user_password(
|
|
user_id: int,
|
|
body: ResetPasswordRequest,
|
|
admin: User = Depends(require_admin),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Reset a user's password (admin only)."""
|
|
user = await session.get(User, user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
if len(body.new_password) < 6:
|
|
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
|
|
user.hashed_password = bcrypt.hashpw(body.new_password.encode(), bcrypt.gensalt()).decode()
|
|
session.add(user)
|
|
await session.commit()
|
|
return {"success": True}
|
|
|
|
|
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_user(
|
|
user_id: int,
|
|
admin: User = Depends(require_admin),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Delete a user (admin only, cannot delete self)."""
|
|
if user_id == admin.id:
|
|
raise HTTPException(status_code=400, detail="Cannot delete yourself")
|
|
user = await session.get(User, user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
await session.delete(user)
|
|
await session.commit()
|