170 lines
6.3 KiB
Python
170 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Form, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from app.core.http_responses import forbidden_response, not_found_response
|
|
|
|
from app.core.db.common import CommonSessionLocal
|
|
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
|
from app.core.security.session_auth import get_current_user
|
|
from app.core.templating import templates
|
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
|
from app.modules.work_detail.service import get_task_for_action, load_unified_engagement_detail, save_task_comment, save_task_status
|
|
|
|
router = APIRouter(prefix="/work", tags=["unified-work-detail-ui"])
|
|
|
|
|
|
def _csrf_rejected(request: Request):
|
|
from app.core.http_responses import forbidden_response
|
|
return forbidden_response(request, "CSRF validation failed")
|
|
|
|
|
|
|
|
def _base_ctx(request: Request, db, user, **ctx):
|
|
base = {
|
|
"request": request,
|
|
"current_user": user,
|
|
"current_user_roles": get_user_roles(db, user.id),
|
|
"current_user_permissions": get_user_permissions(db, user.id),
|
|
"csrf_token": get_or_create_csrf_token(request),
|
|
}
|
|
base.update(ctx)
|
|
return base
|
|
|
|
|
|
def _fallback_for_user(db, user) -> str:
|
|
roles = set(get_user_roles(db, user.id))
|
|
if "Client" in roles:
|
|
return "/client/compliance"
|
|
if "Consultant" in roles:
|
|
return "/consultant/work"
|
|
if "Partner" in roles:
|
|
return "/partner/reviews"
|
|
if roles.intersection({"Manager", "Branch Manager"}):
|
|
return "/manager/work"
|
|
return "/employee/work"
|
|
|
|
|
|
@router.get("")
|
|
def unified_work_board(request: Request):
|
|
"""Compatibility alias for the common /work route.
|
|
|
|
Existing functionality remains in the Services Work Tracker module.
|
|
This route only prevents stale bookmarks/tests from receiving a 404.
|
|
"""
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
return RedirectResponse(url="/services/work-tracker", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
# Phase 3 security hardening: /work/engagements is a listing alias, not a create endpoint.
|
|
# Existing engagement creation remains in the services module.
|
|
@router.post("/engagements")
|
|
def unified_engagements_post_rejected(request: Request):
|
|
return _csrf_rejected(request)
|
|
|
|
|
|
@router.get("/engagements")
|
|
def unified_engagements_index(request: Request):
|
|
"""Compatibility alias for /work/engagements.
|
|
|
|
Engagement listing is maintained under /services/engagements.
|
|
"""
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
return RedirectResponse(url="/services/engagements", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/engagements/{engagement_id}")
|
|
def unified_engagement_detail(request: Request, engagement_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
if "Client" in set(get_user_roles(db, user.id)):
|
|
return forbidden_response(request, "Access denied")
|
|
detail = load_unified_engagement_detail(db, request=request, user=user, engagement_id=engagement_id)
|
|
if not detail:
|
|
return not_found_response(request, "Work engagement not found or access denied")
|
|
return templates.TemplateResponse(
|
|
"modules/work_detail/templates/work_detail/engagement_detail.html",
|
|
_base_ctx(request, db, user, title="Work Details", **detail),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/tasks/{task_id}/status")
|
|
async def unified_task_status_update(request: Request, task_id: int, csrf_token: str = Form(...), status: str = Form(...), priority: str = Form("")):
|
|
try:
|
|
validate_csrf(request, csrf_token)
|
|
except PermissionError:
|
|
return _csrf_rejected(request)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
task, access = get_task_for_action(db, user=user, task_id=task_id)
|
|
if not task or not access:
|
|
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
|
|
if not access.can_update_tasks:
|
|
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=not_allowed", status_code=303)
|
|
save_task_status(db, task=task, status=status, priority=priority, user_id=user.id)
|
|
db.commit()
|
|
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?updated=1", status_code=303)
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/tasks/{task_id}/comment")
|
|
async def unified_task_comment_add(
|
|
request: Request,
|
|
task_id: int,
|
|
csrf_token: str = Form(...),
|
|
comment_type: str = Form("internal_note"),
|
|
visibility: str = Form("internal"),
|
|
message: str = Form(""),
|
|
):
|
|
try:
|
|
validate_csrf(request, csrf_token)
|
|
except PermissionError:
|
|
return _csrf_rejected(request)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user = get_current_user(request, db=db)
|
|
if not user:
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
task, access = get_task_for_action(db, user=user, task_id=task_id)
|
|
if not task or not access:
|
|
return RedirectResponse(url=_fallback_for_user(db, user), status_code=303)
|
|
if not access.can_comment:
|
|
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=comment_not_allowed", status_code=303)
|
|
ok = save_task_comment(db, task=task, access=access, comment_type=comment_type, visibility=visibility, message=message, user_id=user.id)
|
|
if ok:
|
|
db.commit()
|
|
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?comment=sent", status_code=303)
|
|
db.rollback()
|
|
return RedirectResponse(url=f"/work/engagements/{task.subscription_id}?error=empty_comment", status_code=303)
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|