Phase 4 Complete by Claude Code

This commit is contained in:
2026-02-17 21:06:00 +02:00
parent f38057361d
commit fc2d04b8bb
9 changed files with 1446 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ from auth.router import router as auth_router
from melodies.router import router as melodies_router
from devices.router import router as devices_router
from settings.router import router as settings_router
from users.router import router as users_router
app = FastAPI(
title="BellSystems Admin Panel",
@@ -26,6 +27,7 @@ app.include_router(auth_router)
app.include_router(melodies_router)
app.include_router(devices_router)
app.include_router(settings_router)
app.include_router(users_router)
@app.on_event("startup")

View File

@@ -1 +1,43 @@
# TODO: User Pydantic schemas
from pydantic import BaseModel
from typing import List, Optional
# --- Request / Response schemas ---
class UserCreate(BaseModel):
email: str = ""
display_name: str = ""
photo_url: str = ""
uid: str = ""
phone_number: str = ""
status: str = ""
bio: str = ""
userTitle: str = ""
settingsPIN: str = ""
quickSettingsPIN: str = ""
class UserUpdate(BaseModel):
email: Optional[str] = None
display_name: Optional[str] = None
photo_url: Optional[str] = None
phone_number: Optional[str] = None
status: Optional[str] = None
bio: Optional[str] = None
userTitle: Optional[str] = None
settingsPIN: Optional[str] = None
quickSettingsPIN: Optional[str] = None
class UserInDB(UserCreate):
id: str
created_time: str = ""
lastActive: str = ""
createdAt: str = ""
friendsList: List[str] = []
friendsInvited: List[str] = []
class UserListResponse(BaseModel):
users: List[UserInDB]
total: int

View File

@@ -1 +1,95 @@
# TODO: CRUD endpoints for users
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from auth.models import TokenPayload
from auth.dependencies import require_user_access, require_viewer
from users.models import (
UserCreate, UserUpdate, UserInDB, UserListResponse,
)
from users import service
router = APIRouter(prefix="/api/users", tags=["users"])
@router.get("", response_model=UserListResponse)
async def list_users(
search: Optional[str] = Query(None),
status: Optional[str] = Query(None),
_user: TokenPayload = Depends(require_viewer),
):
users = service.list_users(search=search, status=status)
return UserListResponse(users=users, total=len(users))
@router.get("/{user_id}", response_model=UserInDB)
async def get_user(
user_id: str,
_user: TokenPayload = Depends(require_viewer),
):
return service.get_user(user_id)
@router.post("", response_model=UserInDB, status_code=201)
async def create_user(
body: UserCreate,
_user: TokenPayload = Depends(require_user_access),
):
return service.create_user(body)
@router.put("/{user_id}", response_model=UserInDB)
async def update_user(
user_id: str,
body: UserUpdate,
_user: TokenPayload = Depends(require_user_access),
):
return service.update_user(user_id, body)
@router.delete("/{user_id}", status_code=204)
async def delete_user(
user_id: str,
_user: TokenPayload = Depends(require_user_access),
):
service.delete_user(user_id)
@router.post("/{user_id}/block", response_model=UserInDB)
async def block_user(
user_id: str,
_user: TokenPayload = Depends(require_user_access),
):
return service.block_user(user_id)
@router.post("/{user_id}/unblock", response_model=UserInDB)
async def unblock_user(
user_id: str,
_user: TokenPayload = Depends(require_user_access),
):
return service.unblock_user(user_id)
@router.get("/{user_id}/devices", response_model=List[dict])
async def get_user_devices(
user_id: str,
_user: TokenPayload = Depends(require_viewer),
):
return service.get_user_devices(user_id)
@router.post("/{user_id}/devices/{device_id}", response_model=UserInDB)
async def assign_device(
user_id: str,
device_id: str,
_user: TokenPayload = Depends(require_user_access),
):
return service.assign_device(user_id, device_id)
@router.delete("/{user_id}/devices/{device_id}", response_model=UserInDB)
async def unassign_device(
user_id: str,
device_id: str,
_user: TokenPayload = Depends(require_user_access),
):
return service.unassign_device(user_id, device_id)

View File

@@ -1 +1,252 @@
# TODO: User Firestore operations
from datetime import datetime
from google.cloud.firestore_v1 import DocumentReference
from shared.firebase import get_db
from shared.exceptions import NotFoundError
from users.models import UserCreate, UserUpdate, UserInDB
COLLECTION = "users"
def _convert_firestore_value(val):
"""Convert Firestore-specific types (Timestamp, DocumentReference) to strings."""
if isinstance(val, datetime):
return val.strftime("%d %B %Y at %H:%M:%S UTC%z")
if isinstance(val, DocumentReference):
return val.path
return val
def _sanitize_dict(d: dict) -> dict:
"""Recursively convert Firestore-native types in a dict to plain strings."""
result = {}
for k, v in d.items():
if isinstance(v, dict):
result[k] = _sanitize_dict(v)
elif isinstance(v, list):
result[k] = [
_sanitize_dict(item) if isinstance(item, dict)
else _convert_firestore_value(item)
for item in v
]
else:
result[k] = _convert_firestore_value(v)
return result
def _doc_to_user(doc) -> UserInDB:
"""Convert a Firestore document snapshot to a UserInDB model."""
data = _sanitize_dict(doc.to_dict())
return UserInDB(id=doc.id, **data)
def list_users(
search: str | None = None,
status: str | None = None,
) -> list[UserInDB]:
"""List users with optional filters."""
db = get_db()
ref = db.collection(COLLECTION)
query = ref
if status:
query = query.where("status", "==", status)
docs = query.stream()
results = []
for doc in docs:
user = _doc_to_user(doc)
if search:
search_lower = search.lower()
name_match = search_lower in (user.display_name or "").lower()
email_match = search_lower in (user.email or "").lower()
phone_match = search_lower in (user.phone_number or "").lower()
uid_match = search_lower in (user.uid or "").lower()
if not (name_match or email_match or phone_match or uid_match):
continue
results.append(user)
return results
def get_user(user_doc_id: str) -> UserInDB:
"""Get a single user by Firestore document ID."""
db = get_db()
doc = db.collection(COLLECTION).document(user_doc_id).get()
if not doc.exists:
raise NotFoundError("User")
return _doc_to_user(doc)
def create_user(data: UserCreate) -> UserInDB:
"""Create a new user document in Firestore."""
db = get_db()
doc_data = data.model_dump()
doc_data["friendsList"] = []
doc_data["friendsInvited"] = []
_, doc_ref = db.collection(COLLECTION).add(doc_data)
return UserInDB(id=doc_ref.id, **doc_data)
def update_user(user_doc_id: str, data: UserUpdate) -> UserInDB:
"""Update an existing user document. Only provided fields are updated."""
db = get_db()
doc_ref = db.collection(COLLECTION).document(user_doc_id)
doc = doc_ref.get()
if not doc.exists:
raise NotFoundError("User")
update_data = data.model_dump(exclude_none=True)
doc_ref.update(update_data)
updated_doc = doc_ref.get()
return _doc_to_user(updated_doc)
def delete_user(user_doc_id: str) -> None:
"""Delete a user document from Firestore."""
db = get_db()
doc_ref = db.collection(COLLECTION).document(user_doc_id)
doc = doc_ref.get()
if not doc.exists:
raise NotFoundError("User")
doc_ref.delete()
def block_user(user_doc_id: str) -> UserInDB:
"""Block a user by setting their status to 'blocked'."""
db = get_db()
doc_ref = db.collection(COLLECTION).document(user_doc_id)
doc = doc_ref.get()
if not doc.exists:
raise NotFoundError("User")
doc_ref.update({"status": "blocked"})
updated_doc = doc_ref.get()
return _doc_to_user(updated_doc)
def unblock_user(user_doc_id: str) -> UserInDB:
"""Unblock a user by setting their status to 'active'."""
db = get_db()
doc_ref = db.collection(COLLECTION).document(user_doc_id)
doc = doc_ref.get()
if not doc.exists:
raise NotFoundError("User")
doc_ref.update({"status": "active"})
updated_doc = doc_ref.get()
return _doc_to_user(updated_doc)
def assign_device(user_doc_id: str, device_doc_id: str) -> UserInDB:
"""Assign a device to a user by adding user ref to device's user_list."""
db = get_db()
# Verify user exists
user_ref = db.collection(COLLECTION).document(user_doc_id)
user_doc = user_ref.get()
if not user_doc.exists:
raise NotFoundError("User")
# Verify device exists
device_ref = db.collection("devices").document(device_doc_id)
device_doc = device_ref.get()
if not device_doc.exists:
raise NotFoundError("Device")
# Add user path to device's user_list if not already there
device_data = device_doc.to_dict()
user_list = device_data.get("user_list", [])
user_path = f"users/{user_doc_id}"
# Check if already assigned (handle both string paths and DocumentReferences)
already_assigned = False
for entry in user_list:
if isinstance(entry, DocumentReference):
if entry.path == user_path:
already_assigned = True
break
elif entry == user_path:
already_assigned = True
break
if not already_assigned:
user_list.append(user_path)
device_ref.update({"user_list": user_list})
return _doc_to_user(user_ref.get())
def unassign_device(user_doc_id: str, device_doc_id: str) -> UserInDB:
"""Remove a user from a device's user_list."""
db = get_db()
# Verify user exists
user_ref = db.collection(COLLECTION).document(user_doc_id)
user_doc = user_ref.get()
if not user_doc.exists:
raise NotFoundError("User")
# Verify device exists
device_ref = db.collection("devices").document(device_doc_id)
device_doc = device_ref.get()
if not device_doc.exists:
raise NotFoundError("Device")
# Remove user from device's user_list
device_data = device_doc.to_dict()
user_list = device_data.get("user_list", [])
user_path = f"users/{user_doc_id}"
new_list = []
for entry in user_list:
if isinstance(entry, DocumentReference):
if entry.path != user_path:
new_list.append(entry)
elif entry != user_path:
new_list.append(entry)
device_ref.update({"user_list": new_list})
return _doc_to_user(user_ref.get())
def get_user_devices(user_doc_id: str) -> list[dict]:
"""Get all devices assigned to a user."""
db = get_db()
# Verify user exists
user_ref = db.collection(COLLECTION).document(user_doc_id)
user_doc = user_ref.get()
if not user_doc.exists:
raise NotFoundError("User")
user_path = f"users/{user_doc_id}"
# Search all devices for this user in their user_list
devices = []
for doc in db.collection("devices").stream():
data = doc.to_dict()
user_list = data.get("user_list", [])
for entry in user_list:
entry_path = entry.path if isinstance(entry, DocumentReference) else entry
if entry_path == user_path:
devices.append({
"id": doc.id,
"device_name": data.get("device_name", ""),
"device_id": data.get("device_id", ""),
"device_location": data.get("device_location", ""),
"is_Online": data.get("is_Online", False),
})
break
return devices