mirror of
https://github.com/myronblair/kino-app
synced 2026-07-27 21:18:43 -05:00
Add user management (CRUD + admin toggle), make Settings available to all users
- New /admin/users page: create/delete users, toggle admin role, guarded against self-demotion/self-deletion - Backend: GET/POST/PATCH/DELETE /api/admin/users, admin-only - Settings moved from /admin/settings to /settings (all logged-in users); the password-change section shows for everyone, integration/API-key sections only render (and only fetch) for admins, since those endpoints stay admin-gated - Navbar: Settings link now visible to all users; added Users nav link for admins
This commit is contained in:
@@ -34,6 +34,19 @@ class UserPublic(BaseModel):
|
||||
created_at: str
|
||||
|
||||
|
||||
class AdminUserCreate(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
name: str
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
class AdminUserUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
is_admin: Optional[bool] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
# ---------- Profiles ("Who's Watching") ----------
|
||||
RATING_ORDER = ["G", "PG", "PG-13", "R", "NR"]
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
from models import (
|
||||
UserCreate, UserLogin, UserPublic, TokenResponse, PasswordChange,
|
||||
AdminUserCreate, AdminUserUpdate,
|
||||
Profile, ProfileCreate, ProfileUpdate, RATING_ORDER,
|
||||
Movie, MovieCreate, MovieUpdate,
|
||||
Subtitle,
|
||||
@@ -249,6 +250,64 @@ async def change_password(payload: PasswordChange, user: dict = Depends(get_curr
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ============ USERS (admin) ============
|
||||
@api.get("/admin/users", response_model=List[UserPublic])
|
||||
async def admin_list_users(user: dict = Depends(require_admin)):
|
||||
docs = await db.users.find({}, {"_id": 0, "password_hash": 0}).sort("created_at", 1).to_list(500)
|
||||
return docs
|
||||
|
||||
|
||||
@api.post("/admin/users", response_model=UserPublic)
|
||||
async def admin_create_user(payload: AdminUserCreate, user: dict = Depends(require_admin)):
|
||||
if await db.users.find_one({"email": payload.email.lower()}):
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
new_user = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"email": payload.email.lower(),
|
||||
"name": payload.name,
|
||||
"password_hash": hash_password(payload.password),
|
||||
"is_admin": payload.is_admin,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
await db.users.insert_one(new_user)
|
||||
await db.profiles.insert_one(Profile(user_id=new_user["id"], name=new_user["name"]).model_dump())
|
||||
return _user_public(new_user)
|
||||
|
||||
|
||||
@api.patch("/admin/users/{user_id}", response_model=UserPublic)
|
||||
async def admin_update_user(user_id: str, payload: AdminUserUpdate, user: dict = Depends(require_admin)):
|
||||
target = await db.users.find_one({"id": user_id})
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
updates = {}
|
||||
if payload.name is not None:
|
||||
updates["name"] = payload.name
|
||||
if payload.is_admin is not None:
|
||||
if user_id == user["id"] and not payload.is_admin:
|
||||
raise HTTPException(status_code=400, detail="Cannot remove your own admin access")
|
||||
updates["is_admin"] = payload.is_admin
|
||||
if payload.password:
|
||||
if len(payload.password) < 8:
|
||||
raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
|
||||
updates["password_hash"] = hash_password(payload.password)
|
||||
if updates:
|
||||
await db.users.update_one({"id": user_id}, {"$set": updates})
|
||||
target = await db.users.find_one({"id": user_id})
|
||||
return _user_public(target)
|
||||
|
||||
|
||||
@api.delete("/admin/users/{user_id}")
|
||||
async def admin_delete_user(user_id: str, user: dict = Depends(require_admin)):
|
||||
if user_id == user["id"]:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete your own account")
|
||||
target = await db.users.find_one({"id": user_id})
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
await db.users.delete_one({"id": user_id})
|
||||
await db.profiles.delete_many({"user_id": user_id})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ============ PROFILES ============
|
||||
@api.get("/profiles", response_model=List[Profile])
|
||||
async def list_profiles(user: dict = Depends(get_current_user)):
|
||||
|
||||
Reference in New Issue
Block a user