Gate STO workplace by role
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-16 10:33:33 +09:00
parent 83ad880b9d
commit ac5845d5a0
10 changed files with 1612 additions and 593 deletions

View File

@@ -40,6 +40,7 @@ from app.schemas.service_center import (
ServiceCenterVerificationRead,
ServiceEmployeeInvite,
ServiceEmployeeRead,
ServiceEmployeeUpdate,
ServiceInboxCreate,
ServiceInboxRead,
ServiceVisitCreate,
@@ -55,6 +56,41 @@ from app.services.vehicle_identity import mask_license_plate, mask_vin
router = APIRouter(prefix="/service-centers", tags=["service-centers"])
APPROVED_SERVICE_STATUSES = {"approved", "verified"}
SERVICE_EMPLOYEE_ROLES = {"owner", "manager", "receptionist", "mechanic"}
SERVICE_EMPLOYEE_STATUSES = {"active", "invited", "inactive", "revoked", "expired"}
def validate_employee_role(role: str) -> str:
role = role.strip().lower()
if role not in SERVICE_EMPLOYEE_ROLES:
raise HTTPException(status_code=400, detail="Unsupported service employee role")
return role
def validate_employee_status(status_value: str) -> str:
status_value = status_value.strip().lower()
if status_value not in SERVICE_EMPLOYEE_STATUSES:
raise HTTPException(status_code=400, detail="Unsupported service employee status")
return status_value
async def attach_employee_user_fields(session: AsyncSession, employees: list[ServiceEmployee]) -> list[ServiceEmployee]:
if not employees:
return employees
user_ids = [employee.user_id for employee in employees]
users = {
user.id: user
for user in (await session.execute(select(User).where(User.id.in_(user_ids)))).scalars()
}
for employee in employees:
user = users.get(employee.user_id)
if user is None:
continue
employee.telegram_id = user.telegram_id
employee.username = user.username
employee.first_name = user.first_name
employee.last_name = user.last_name
return employees
@router.post("", response_model=ServiceCenterRead, status_code=status.HTTP_201_CREATED)
async def create_service_center(
@@ -265,7 +301,9 @@ async def invite_employee(
current_user: User = Depends(get_current_telegram_user),
) -> ServiceEmployee:
await check_rate_limit(scope="employee_invite", limit=10, window_seconds=3600, request=request, user=current_user, session=session)
await ensure_service_center_approved(session, service_center_id)
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
role = validate_employee_role(payload.role)
user = await get_or_create_telegram_user(session, telegram_id=payload.telegram_id)
result = await session.execute(
select(ServiceEmployee).where(
@@ -278,7 +316,7 @@ async def invite_employee(
employee = ServiceEmployee(
service_center_id=service_center_id,
user_id=user.id,
role=payload.role,
role=role,
permissions=payload.permissions,
status="invited",
invite_token=secrets.token_urlsafe(32),
@@ -286,7 +324,9 @@ async def invite_employee(
)
session.add(employee)
else:
employee.role = payload.role
if employee.role == "owner":
raise HTTPException(status_code=409, detail="Owner role cannot be replaced by invite")
employee.role = role
employee.permissions = payload.permissions
employee.status = "invited"
employee.invite_token = secrets.token_urlsafe(32)
@@ -296,9 +336,27 @@ async def invite_employee(
await log_audit(session, actor=current_user, action="service_employee.invite", target_type="service_center", target_id=service_center_id, metadata={"telegram_id": payload.telegram_id})
await session.commit()
await session.refresh(employee)
await attach_employee_user_fields(session, [employee])
return employee
@router.get("/{service_center_id}/employees", response_model=list[ServiceEmployeeRead])
async def list_service_employees(
service_center_id: int,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[ServiceEmployee]:
await ensure_service_center_approved(session, service_center_id)
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
result = await session.execute(
select(ServiceEmployee)
.where(ServiceEmployee.service_center_id == service_center_id)
.order_by(ServiceEmployee.role.asc(), ServiceEmployee.created_at.asc())
)
employees = list(result.scalars())
return await attach_employee_user_fields(session, employees)
@router.post("/employees/invites/{invite_token}/accept", response_model=ServiceEmployeeRead)
async def accept_employee_invite(
invite_token: str,
@@ -340,6 +398,47 @@ async def accept_employee_invite(
)
await session.commit()
await session.refresh(employee)
await attach_employee_user_fields(session, [employee])
return employee
@router.patch("/employees/{employee_id}", response_model=ServiceEmployeeRead)
async def update_service_employee(
employee_id: int,
payload: ServiceEmployeeUpdate,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceEmployee:
employee = await session.get(ServiceEmployee, employee_id)
if employee is None:
raise HTTPException(status_code=404, detail="Employee not found")
await ensure_service_center_approved(session, employee.service_center_id)
await ensure_service_employee(session, employee.service_center_id, current_user, {"owner", "manager"})
if employee.role == "owner" and payload.role and payload.role != "owner":
raise HTTPException(status_code=409, detail="Owner role cannot be changed")
if employee.role == "owner" and payload.status and payload.status != "active":
raise HTTPException(status_code=409, detail="Owner cannot be deactivated")
data = payload.model_dump(exclude_unset=True)
if "role" in data and data["role"] is not None:
employee.role = validate_employee_role(data["role"])
if "status" in data and data["status"] is not None:
employee.status = validate_employee_status(data["status"])
if employee.status != "invited":
employee.invite_token = None
employee.invite_revoked_at = datetime.now(UTC) if employee.status == "revoked" else employee.invite_revoked_at
if "permissions" in data:
employee.permissions = data["permissions"]
await log_audit(
session,
actor=current_user,
action="service_employee.update",
target_type="service_employee",
target_id=employee.id,
metadata=data,
)
await session.commit()
await session.refresh(employee)
await attach_employee_user_fields(session, [employee])
return employee
@@ -367,6 +466,7 @@ async def revoke_employee_invite(
)
await session.commit()
await session.refresh(employee)
await attach_employee_user_fields(session, [employee])
return employee