Files
bellsystems-cp/backend/staff/service.py
2026-04-17 15:44:17 +03:00

269 lines
7.9 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.audit import log_action, diff
from shared.exceptions import NotFoundError, AuthorizationError
import uuid
VALID_ROLES = ("sysadmin", "admin", "editor", "user")
def _now() -> datetime:
return datetime.now(timezone.utc)
def _to_dict(staff: Staff) -> dict:
return {
"id": staff.id,
"email": staff.email,
"name": staff.name,
"role": staff.role,
"is_active": staff.is_active,
"permissions": staff.permissions,
}
def _to_response(staff: Staff) -> dict:
return _to_dict(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(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,
actor_id: str,
actor_name: 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,
ui_prefs={},
created_at=now,
updated_at=now,
)
db.add(staff)
await db.flush()
await log_action(
db,
actor_id=actor_id,
actor_name=actor_name,
action="CREATE",
entity_type="staff",
entity_id=uid,
entity_label=data["email"],
meta={"role": role},
)
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,
actor_id: str,
actor_name: 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")
old = _to_dict(staff)
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.flush()
changes = diff(old, _to_dict(staff))
action = "PERMISSION_CHANGE" if "permissions" in data and len(changes) == 1 else "UPDATE"
await log_action(
db,
actor_id=actor_id,
actor_name=actor_name,
action=action,
entity_type="staff",
entity_id=staff_id,
entity_label=staff.email,
changes=changes or None,
)
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,
actor_id: str,
actor_name: 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.flush()
await log_action(
db,
actor_id=actor_id,
actor_name=actor_name,
action="UPDATE",
entity_type="staff",
entity_id=staff_id,
entity_label=staff.email,
meta={"detail": "password changed"},
)
await db.commit()
return {"message": "Password updated successfully"}
async def get_preferences(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.ui_prefs or {}
async def update_preferences(db: AsyncSession, staff_id: str, page_key: str, prefs: dict) -> 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")
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,
actor_id: str,
actor_name: 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")
label = staff.email
await log_action(
db,
actor_id=actor_id,
actor_name=actor_name,
action="DELETE",
entity_type="staff",
entity_id=staff_id,
entity_label=label,
)
await db.delete(staff)
await db.commit()
return {"message": "Staff member deleted"}