This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from datetime import UTC, datetime
|
||||
import secrets
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -48,6 +49,7 @@ from app.schemas.service_center import (
|
||||
)
|
||||
from app.services.notifications import notify_platform_moderators
|
||||
from app.services.odometer import validate_odometer_change
|
||||
from app.services.rate_limit import check_rate_limit
|
||||
from app.services.vehicle_identity import mask_license_plate, mask_vin
|
||||
|
||||
router = APIRouter(prefix="/service-centers", tags=["service-centers"])
|
||||
@@ -162,12 +164,17 @@ async def my_service_centers(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> list[ServiceCenter]:
|
||||
result = await session.execute(
|
||||
select(ServiceCenter)
|
||||
select(ServiceCenter, ServiceEmployee.role, ServiceEmployee.status)
|
||||
.join(ServiceEmployee, ServiceEmployee.service_center_id == ServiceCenter.id)
|
||||
.where(ServiceEmployee.user_id == current_user.id, ServiceEmployee.status == "active")
|
||||
.order_by(ServiceCenter.created_at.desc())
|
||||
)
|
||||
return list(result.scalars())
|
||||
centers = []
|
||||
for center, role, employee_status in result.all():
|
||||
center.employee_role = role
|
||||
center.employee_status = employee_status
|
||||
centers.append(center)
|
||||
return centers
|
||||
|
||||
|
||||
@router.get("/public", response_model=list[ServiceCenterPublicRead])
|
||||
@@ -253,9 +260,11 @@ async def submit_verification(
|
||||
async def invite_employee(
|
||||
service_center_id: int,
|
||||
payload: ServiceEmployeeInvite,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
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_employee(session, service_center_id, current_user, {"owner", "manager"})
|
||||
user = await get_or_create_telegram_user(session, telegram_id=payload.telegram_id)
|
||||
result = await session.execute(
|
||||
@@ -272,18 +281,95 @@ async def invite_employee(
|
||||
role=payload.role,
|
||||
permissions=payload.permissions,
|
||||
status="invited",
|
||||
invite_token=secrets.token_urlsafe(32),
|
||||
invite_expires_at=datetime.now(UTC) + timedelta(hours=payload.expires_in_hours),
|
||||
)
|
||||
session.add(employee)
|
||||
else:
|
||||
employee.role = payload.role
|
||||
employee.permissions = payload.permissions
|
||||
employee.status = "invited"
|
||||
employee.invite_token = secrets.token_urlsafe(32)
|
||||
employee.invite_expires_at = datetime.now(UTC) + timedelta(hours=payload.expires_in_hours)
|
||||
employee.invite_revoked_at = None
|
||||
employee.activated_at = None
|
||||
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)
|
||||
return employee
|
||||
|
||||
|
||||
@router.post("/employees/invites/{invite_token}/accept", response_model=ServiceEmployeeRead)
|
||||
async def accept_employee_invite(
|
||||
invite_token: str,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEmployee:
|
||||
result = await session.execute(
|
||||
select(ServiceEmployee).where(ServiceEmployee.invite_token == invite_token)
|
||||
)
|
||||
employee = result.scalar_one_or_none()
|
||||
if employee is None:
|
||||
raise HTTPException(status_code=404, detail="Invite not found")
|
||||
if employee.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Invite belongs to another Telegram account")
|
||||
if employee.status != "invited":
|
||||
raise HTTPException(status_code=409, detail="Invite is not active")
|
||||
if employee.invite_revoked_at is not None:
|
||||
raise HTTPException(status_code=409, detail="Invite was revoked")
|
||||
if employee.invite_expires_at:
|
||||
expires_at = employee.invite_expires_at
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=UTC)
|
||||
else:
|
||||
expires_at = None
|
||||
if expires_at and expires_at <= datetime.now(UTC):
|
||||
employee.status = "expired"
|
||||
await log_audit(session, actor=current_user, action="service_employee.invite_expired", target_type="service_employee", target_id=employee.id)
|
||||
await session.commit()
|
||||
raise HTTPException(status_code=409, detail="Invite expired")
|
||||
employee.status = "active"
|
||||
employee.activated_at = datetime.now(UTC)
|
||||
employee.invite_token = None
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="service_employee.invite_accept",
|
||||
target_type="service_employee",
|
||||
target_id=employee.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
return employee
|
||||
|
||||
|
||||
@router.post("/employees/{employee_id}/revoke-invite", response_model=ServiceEmployeeRead)
|
||||
async def revoke_employee_invite(
|
||||
employee_id: int,
|
||||
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_employee(session, employee.service_center_id, current_user, {"owner", "manager"})
|
||||
if employee.status != "invited":
|
||||
raise HTTPException(status_code=409, detail="Only invited employees can be revoked")
|
||||
employee.status = "revoked"
|
||||
employee.invite_revoked_at = datetime.now(UTC)
|
||||
employee.invite_token = None
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="service_employee.invite_revoke",
|
||||
target_type="service_employee",
|
||||
target_id=employee.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
return employee
|
||||
|
||||
|
||||
@router.get("/{service_center_id}/visits", response_model=list[ServiceVisitRead])
|
||||
async def service_center_visits(
|
||||
service_center_id: int,
|
||||
@@ -355,6 +441,7 @@ async def create_visit(
|
||||
visit = ServiceVisit(
|
||||
service_center_id=service_center_id,
|
||||
vehicle_id=payload.vehicle_id,
|
||||
owner_id=vehicle.owner_id,
|
||||
created_by_employee_id=employee.id,
|
||||
visit_date=payload.visit_date,
|
||||
odometer=payload.odometer,
|
||||
@@ -374,9 +461,11 @@ async def create_visit(
|
||||
async def request_vehicle_access(
|
||||
service_center_id: int,
|
||||
payload: VehicleSearchRequest,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> VehicleSearchResult:
|
||||
await check_rate_limit(scope="vehicle_access_request", limit=20, window_seconds=3600, request=request, user=current_user, session=session)
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager", "receptionist"})
|
||||
await ensure_service_center_approved(session, service_center_id)
|
||||
stmt = select(Car)
|
||||
@@ -610,9 +699,11 @@ async def service_center_reviews(
|
||||
async def create_service_center_review(
|
||||
service_center_id: int,
|
||||
payload: ServiceCenterReviewCreate,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceCenterReview:
|
||||
await check_rate_limit(scope="service_review", limit=10, window_seconds=3600, request=request, user=current_user, session=session)
|
||||
await ensure_service_center_approved(session, service_center_id)
|
||||
result = await session.execute(
|
||||
select(ServiceCenterReview).where(
|
||||
|
||||
Reference in New Issue
Block a user