203 lines
6.5 KiB
Python
203 lines
6.5 KiB
Python
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
|
|
import uuid
|
|
|
|
|
|
VALID_ROLES = ("sysadmin", "admin", "editor", "user")
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def _to_response(staff: Staff) -> dict:
|
|
return {
|
|
"id": staff.id,
|
|
"email": staff.email,
|
|
"name": staff.name,
|
|
"role": staff.role,
|
|
"is_active": staff.is_active,
|
|
"permissions": staff.permissions,
|
|
}
|
|
|
|
|
|
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(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 _to_response(staff)
|
|
|
|
|
|
async def get_staff_me(db: AsyncSession, user_sub: str) -> dict:
|
|
return await get_staff(db, user_sub)
|
|
|
|
|
|
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}")
|
|
if current_user_role == "admin" and role == "sysadmin":
|
|
raise AuthorizationError("Admin cannot create sysadmin accounts")
|
|
|
|
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")
|
|
|
|
permissions = data.get("permissions")
|
|
if permissions is None and role in ("editor", "user"):
|
|
permissions = default_permissions_for_role(role)
|
|
|
|
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(
|
|
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")
|
|
|
|
if current_user_role == "admin" and staff.role == "sysadmin":
|
|
raise AuthorizationError("Admin cannot modify sysadmin accounts")
|
|
if current_user_role == "admin" and data.get("role") == "sysadmin":
|
|
raise AuthorizationError("Admin cannot promote to sysadmin")
|
|
|
|
if data.get("email") is not None:
|
|
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:
|
|
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']}")
|
|
staff.role = data["role"]
|
|
if data.get("is_active") is not None:
|
|
staff.is_active = data["is_active"]
|
|
if "permissions" in data:
|
|
staff.permissions = data["permissions"]
|
|
|
|
staff.updated_at = _now()
|
|
await db.commit()
|
|
await db.refresh(staff)
|
|
return _to_response(staff)
|
|
|
|
|
|
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")
|
|
if current_user_role == "admin" and staff.role == "sysadmin":
|
|
raise AuthorizationError("Admin cannot modify sysadmin accounts")
|
|
|
|
staff.hashed_password = hash_password(new_password)
|
|
staff.updated_at = _now()
|
|
await db.commit()
|
|
return {"message": "Password updated successfully"}
|
|
|
|
|
|
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 staff.ui_prefs or {}
|
|
|
|
|
|
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")
|
|
|
|
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(
|
|
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")
|
|
|
|
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")
|
|
|
|
await db.delete(staff)
|
|
await db.commit()
|
|
return {"message": "Staff member deleted"}
|