- Add Cancelled as terminal status reachable from all active states; exclude
from SLA breach reporting and dashboard active counts; add to status pickers
- Persist customer phone on ticket create/update (was silently dropped);
add tickets.phone migration + legacy self-heal guard
- Add admin-only DELETE /api/tickets/{id} (removes timeline/photos/escalations)
- Add GET /api/auth/users for the assign-technician dropdown (was hardcoded)
- TicketOut now returns nested unit/category so the detail page stops showing
'—' for Unit/Property/Category
- Ticket numbering uses max+1 so deletions never re-issue a number
- New-issue form: require Category and (standard mode) Priority client-side
- Tests: 11 new cases covering cancellation, SLA exemption, phone, delete,
users endpoint, numbering
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""Authentication router — register, login, refresh, me."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user, require_roles
|
|
from app.models.user import User
|
|
from app.schemas.auth import (
|
|
LoginRequest,
|
|
RefreshRequest,
|
|
RegisterRequest,
|
|
TokenResponse,
|
|
UserOut,
|
|
)
|
|
from app.services import auth as auth_service
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
|
@router.get("/users", response_model=list[UserOut])
|
|
async def list_users(
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(get_current_user)],
|
|
) -> list[User]:
|
|
"""List users (id, name, role) for assignment pickers.
|
|
|
|
Previously the frontend hard-coded technician ids/names in detail.html;
|
|
this endpoint makes the assign dropdown data-driven so a seed change never
|
|
silently breaks technician assignment.
|
|
"""
|
|
result = await db.execute(select(User).order_by(User.full_name))
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.post("/register", response_model=UserOut, status_code=201)
|
|
async def register(
|
|
body: RegisterRequest,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
) -> User:
|
|
return await auth_service.register(db, body)
|
|
|
|
|
|
@router.post("/login", response_model=TokenResponse)
|
|
async def login(
|
|
body: LoginRequest,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
) -> TokenResponse:
|
|
access, refresh, _user = await auth_service.login(db, body.email, body.password)
|
|
return TokenResponse(access_token=access, refresh_token=refresh)
|
|
|
|
|
|
@router.post("/refresh", response_model=TokenResponse)
|
|
async def refresh(
|
|
body: RefreshRequest,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
) -> TokenResponse:
|
|
access, refresh = await auth_service.refresh_access_token(db, body.refresh_token)
|
|
return TokenResponse(access_token=access, refresh_token=refresh)
|
|
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
async def me(current_user: Annotated[User, Depends(get_current_user)]) -> User:
|
|
return current_user
|
|
|
|
|
|
@router.get("/admin-only", response_model=UserOut)
|
|
async def admin_only(
|
|
current_user: Annotated[User, Depends(require_roles("Admin/Jerome", "Admin/Wahab"))],
|
|
) -> User:
|
|
"""Example RBAC-protected endpoint — only Admins can access."""
|
|
return current_user
|