Phase 3 of Migration
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
from shared.firebase import get_db
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from staff.orm import Staff
|
||||
from auth.utils import hash_password
|
||||
from auth.models import default_permissions_for_role
|
||||
from shared.exceptions import NotFoundError, AuthorizationError
|
||||
@@ -8,198 +13,190 @@ import uuid
|
||||
VALID_ROLES = ("sysadmin", "admin", "editor", "user")
|
||||
|
||||
|
||||
def _staff_doc_to_response(doc_id: str, data: dict) -> dict:
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _to_response(staff: Staff) -> dict:
|
||||
return {
|
||||
"id": doc_id,
|
||||
"email": data.get("email", ""),
|
||||
"name": data.get("name", ""),
|
||||
"role": data.get("role", ""),
|
||||
"is_active": data.get("is_active", True),
|
||||
"permissions": data.get("permissions"),
|
||||
"id": staff.id,
|
||||
"email": staff.email,
|
||||
"name": staff.name,
|
||||
"role": staff.role,
|
||||
"is_active": staff.is_active,
|
||||
"permissions": staff.permissions,
|
||||
}
|
||||
|
||||
|
||||
async def list_staff(search: str = None, role_filter: str = None) -> dict:
|
||||
db = get_db()
|
||||
ref = db.collection("admin_users")
|
||||
docs = ref.get()
|
||||
|
||||
staff = []
|
||||
for doc in docs:
|
||||
data = doc.to_dict()
|
||||
if search:
|
||||
s = search.lower()
|
||||
if s not in (data.get("name", "").lower()) and s not in (data.get("email", "").lower()):
|
||||
continue
|
||||
if role_filter:
|
||||
if data.get("role") != role_filter:
|
||||
continue
|
||||
staff.append(_staff_doc_to_response(doc.id, data))
|
||||
|
||||
return {"staff": staff, "total": len(staff)}
|
||||
async def list_staff(db: AsyncSession, search: str = None, role_filter: str = None) -> dict:
|
||||
stmt = select(Staff)
|
||||
if role_filter:
|
||||
stmt = stmt.where(Staff.role == role_filter)
|
||||
if search:
|
||||
s = f"%{search.lower()}%"
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
func.lower(Staff.name).like(s),
|
||||
func.lower(Staff.email).like(s),
|
||||
)
|
||||
)
|
||||
stmt = stmt.order_by(Staff.name)
|
||||
result = await db.execute(stmt)
|
||||
rows = result.scalars().all()
|
||||
return {"staff": [_to_response(r) for r in rows], "total": len(rows)}
|
||||
|
||||
|
||||
async def get_staff(staff_id: str) -> dict:
|
||||
db = get_db()
|
||||
doc = db.collection("admin_users").document(staff_id).get()
|
||||
if not doc.exists:
|
||||
async def get_staff(db: AsyncSession, staff_id: str) -> dict:
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
return _staff_doc_to_response(doc.id, doc.to_dict())
|
||||
return _to_response(staff)
|
||||
|
||||
|
||||
async def get_staff_me(user_sub: str) -> dict:
|
||||
db = get_db()
|
||||
doc = db.collection("admin_users").document(user_sub).get()
|
||||
if not doc.exists:
|
||||
raise NotFoundError("Staff member not found")
|
||||
return _staff_doc_to_response(doc.id, doc.to_dict())
|
||||
async def get_staff_me(db: AsyncSession, user_sub: str) -> dict:
|
||||
return await get_staff(db, user_sub)
|
||||
|
||||
|
||||
async def create_staff(data: dict, current_user_role: str) -> dict:
|
||||
async def create_staff(db: AsyncSession, data: dict, current_user_role: str) -> dict:
|
||||
role = data.get("role", "user")
|
||||
if role not in VALID_ROLES:
|
||||
raise AuthorizationError(f"Invalid role: {role}")
|
||||
|
||||
# Admin cannot create sysadmin
|
||||
if current_user_role == "admin" and role == "sysadmin":
|
||||
raise AuthorizationError("Admin cannot create sysadmin accounts")
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Check for duplicate email
|
||||
existing = db.collection("admin_users").where("email", "==", data["email"]).limit(1).get()
|
||||
if existing:
|
||||
existing = await db.execute(
|
||||
select(Staff).where(Staff.email == data["email"]).limit(1)
|
||||
)
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise AuthorizationError("A staff member with this email already exists")
|
||||
|
||||
uid = str(uuid.uuid4())
|
||||
hashed = hash_password(data["password"])
|
||||
|
||||
# Set default permissions for editor/user if not provided
|
||||
permissions = data.get("permissions")
|
||||
if permissions is None and role in ("editor", "user"):
|
||||
permissions = default_permissions_for_role(role)
|
||||
|
||||
doc_data = {
|
||||
"uid": uid,
|
||||
"email": data["email"],
|
||||
"hashed_password": hashed,
|
||||
"name": data["name"],
|
||||
"role": role,
|
||||
"is_active": True,
|
||||
"permissions": permissions,
|
||||
}
|
||||
|
||||
doc_ref = db.collection("admin_users").document(uid)
|
||||
doc_ref.set(doc_data)
|
||||
|
||||
return _staff_doc_to_response(uid, doc_data)
|
||||
uid = str(uuid.uuid4())
|
||||
now = _now()
|
||||
staff = Staff(
|
||||
id=uid,
|
||||
firestore_id=None,
|
||||
email=data["email"],
|
||||
name=data["name"],
|
||||
role=role,
|
||||
hashed_password=hash_password(data["password"]),
|
||||
is_active=True,
|
||||
permissions=permissions,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
db.add(staff)
|
||||
await db.commit()
|
||||
await db.refresh(staff)
|
||||
return _to_response(staff)
|
||||
|
||||
|
||||
async def update_staff(staff_id: str, data: dict, current_user_role: str, current_user_id: str) -> dict:
|
||||
db = get_db()
|
||||
doc_ref = db.collection("admin_users").document(staff_id)
|
||||
doc = doc_ref.get()
|
||||
if not doc.exists:
|
||||
async def update_staff(
|
||||
db: AsyncSession,
|
||||
staff_id: str,
|
||||
data: dict,
|
||||
current_user_role: str,
|
||||
current_user_id: str,
|
||||
) -> dict:
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
|
||||
existing = doc.to_dict()
|
||||
|
||||
# Admin cannot edit sysadmin accounts
|
||||
if current_user_role == "admin" and existing.get("role") == "sysadmin":
|
||||
if current_user_role == "admin" and staff.role == "sysadmin":
|
||||
raise AuthorizationError("Admin cannot modify sysadmin accounts")
|
||||
|
||||
# Admin cannot promote to sysadmin
|
||||
if current_user_role == "admin" and data.get("role") == "sysadmin":
|
||||
raise AuthorizationError("Admin cannot promote to sysadmin")
|
||||
|
||||
update_data = {}
|
||||
if data.get("email") is not None:
|
||||
# Check for duplicate email
|
||||
others = db.collection("admin_users").where("email", "==", data["email"]).limit(1).get()
|
||||
for other in others:
|
||||
if other.id != staff_id:
|
||||
raise AuthorizationError("A staff member with this email already exists")
|
||||
update_data["email"] = data["email"]
|
||||
dup = await db.execute(
|
||||
select(Staff).where(Staff.email == data["email"], Staff.id != staff_id).limit(1)
|
||||
)
|
||||
if dup.scalar_one_or_none() is not None:
|
||||
raise AuthorizationError("A staff member with this email already exists")
|
||||
staff.email = data["email"]
|
||||
|
||||
if data.get("name") is not None:
|
||||
update_data["name"] = data["name"]
|
||||
staff.name = data["name"]
|
||||
if data.get("role") is not None:
|
||||
if data["role"] not in VALID_ROLES:
|
||||
raise AuthorizationError(f"Invalid role: {data['role']}")
|
||||
update_data["role"] = data["role"]
|
||||
staff.role = data["role"]
|
||||
if data.get("is_active") is not None:
|
||||
update_data["is_active"] = data["is_active"]
|
||||
staff.is_active = data["is_active"]
|
||||
if "permissions" in data:
|
||||
update_data["permissions"] = data["permissions"]
|
||||
staff.permissions = data["permissions"]
|
||||
|
||||
if update_data:
|
||||
doc_ref.update(update_data)
|
||||
|
||||
updated = {**existing, **update_data}
|
||||
return _staff_doc_to_response(staff_id, updated)
|
||||
staff.updated_at = _now()
|
||||
await db.commit()
|
||||
await db.refresh(staff)
|
||||
return _to_response(staff)
|
||||
|
||||
|
||||
async def update_staff_password(staff_id: str, new_password: str, current_user_role: str) -> dict:
|
||||
db = get_db()
|
||||
doc_ref = db.collection("admin_users").document(staff_id)
|
||||
doc = doc_ref.get()
|
||||
if not doc.exists:
|
||||
async def update_staff_password(
|
||||
db: AsyncSession,
|
||||
staff_id: str,
|
||||
new_password: str,
|
||||
current_user_role: str,
|
||||
) -> dict:
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
|
||||
existing = doc.to_dict()
|
||||
|
||||
# Admin cannot change sysadmin password
|
||||
if current_user_role == "admin" and existing.get("role") == "sysadmin":
|
||||
if current_user_role == "admin" and staff.role == "sysadmin":
|
||||
raise AuthorizationError("Admin cannot modify sysadmin accounts")
|
||||
|
||||
hashed = hash_password(new_password)
|
||||
doc_ref.update({"hashed_password": hashed})
|
||||
|
||||
staff.hashed_password = hash_password(new_password)
|
||||
staff.updated_at = _now()
|
||||
await db.commit()
|
||||
return {"message": "Password updated successfully"}
|
||||
|
||||
|
||||
async def get_preferences(staff_id: str) -> dict:
|
||||
"""Return the ui_prefs map for a staff member, defaulting to {} if not set."""
|
||||
db = get_db()
|
||||
doc = db.collection("admin_users").document(staff_id).get()
|
||||
if not doc.exists:
|
||||
async def get_preferences(db: AsyncSession, staff_id: str) -> dict:
|
||||
"""Return ui_prefs JSONB for a staff member."""
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
return doc.to_dict().get("ui_prefs", {})
|
||||
return staff.ui_prefs or {}
|
||||
|
||||
|
||||
async def update_preferences(staff_id: str, page_key: str, prefs: dict) -> dict:
|
||||
"""Merge a page-level preferences dict into ui_prefs.<page_key> on the staff document.
|
||||
Only the supplied keys are overwritten — other keys in the same page block survive.
|
||||
"""
|
||||
db = get_db()
|
||||
doc_ref = db.collection("admin_users").document(staff_id)
|
||||
doc = doc_ref.get()
|
||||
if not doc.exists:
|
||||
async def update_preferences(db: AsyncSession, staff_id: str, page_key: str, prefs: dict) -> dict:
|
||||
"""Merge page-level preferences into the staff member's ui_prefs column."""
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
|
||||
existing_prefs = doc.to_dict().get("ui_prefs", {})
|
||||
page_prefs = {**existing_prefs.get(page_key, {}), **prefs}
|
||||
existing_prefs[page_key] = page_prefs
|
||||
|
||||
doc_ref.update({"ui_prefs": existing_prefs})
|
||||
return existing_prefs
|
||||
current = dict(staff.ui_prefs or {})
|
||||
current[page_key] = {**current.get(page_key, {}), **prefs}
|
||||
staff.ui_prefs = current
|
||||
staff.updated_at = _now()
|
||||
await db.commit()
|
||||
return current
|
||||
|
||||
|
||||
async def delete_staff(staff_id: str, current_user_role: str, current_user_id: str) -> dict:
|
||||
db = get_db()
|
||||
doc_ref = db.collection("admin_users").document(staff_id)
|
||||
doc = doc_ref.get()
|
||||
if not doc.exists:
|
||||
raise NotFoundError("Staff member not found")
|
||||
|
||||
existing = doc.to_dict()
|
||||
|
||||
# Cannot delete self
|
||||
async def delete_staff(
|
||||
db: AsyncSession,
|
||||
staff_id: str,
|
||||
current_user_role: str,
|
||||
current_user_id: str,
|
||||
) -> dict:
|
||||
if staff_id == current_user_id:
|
||||
raise AuthorizationError("Cannot delete your own account")
|
||||
|
||||
# Admin cannot delete sysadmin
|
||||
if current_user_role == "admin" and existing.get("role") == "sysadmin":
|
||||
result = await db.execute(select(Staff).where(Staff.id == staff_id).limit(1))
|
||||
staff = result.scalar_one_or_none()
|
||||
if staff is None:
|
||||
raise NotFoundError("Staff member not found")
|
||||
|
||||
if current_user_role == "admin" and staff.role == "sysadmin":
|
||||
raise AuthorizationError("Admin cannot delete sysadmin accounts")
|
||||
|
||||
doc_ref.delete()
|
||||
await db.delete(staff)
|
||||
await db.commit()
|
||||
return {"message": "Staff member deleted"}
|
||||
|
||||
Reference in New Issue
Block a user