diff --git a/app/modules/documents/ui.py b/app/modules/documents/ui.py index 588d35b..68123ee 100644 --- a/app/modules/documents/ui.py +++ b/app/modules/documents/ui.py @@ -904,8 +904,19 @@ def partner_branch_storage_dashboard(request: Request): It intentionally reuses the same storage node data model and keeps the full Storage Nodes screen intact. Partner/Branch Manager users are automatically - scoped to their own branch. + scoped to their own branch. Staff users receive a clear 403 instead of a + generic dashboard page. """ + db = CommonSessionLocal() + try: + user, response = _require_user(request, db, "documents.view") + if response: + return response + scope = build_document_scope(request, db, user) + if not _can_manage_branch_storage(scope): + return _redirect_denied() + finally: + db.close() return storage_nodes(request) diff --git a/app/modules/email_integration/ui.py b/app/modules/email_integration/ui.py index 47fa559..7f0f7c2 100644 --- a/app/modules/email_integration/ui.py +++ b/app/modules/email_integration/ui.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Form, Request from fastapi.responses import RedirectResponse +from app.core.http_responses import forbidden_response from sqlalchemy import desc, func, or_, select from app.core.db.common import CommonSessionLocal @@ -72,7 +73,7 @@ def email_audit_dashboard_page(request: Request): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) now = datetime.now(timezone.utc) since_24h = now - timedelta(hours=24) @@ -197,7 +198,7 @@ def email_settings_page(request: Request, flash: str | None = None): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) if not tenant_id: return RedirectResponse(url="/system-settings", status_code=303) @@ -258,7 +259,7 @@ def save_email_settings( if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) setting = get_or_create_email_setting(db, int(tenant_id), branch_id, actor_user_id=int(user.id)) setting.smtp_host = smtp_host.strip() or None @@ -305,7 +306,7 @@ def test_email_settings(request: Request, csrf_token: str = Form(...), test_emai if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) recipient = (test_email or getattr(user, "email", "")).strip() send_template_email( @@ -333,7 +334,7 @@ def email_logs_page(request: Request): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) q = select(EmailLog).where(EmailLog.tenant_id == tenant_id) if branch_id: @@ -356,7 +357,7 @@ def email_queue_page(request: Request, flash: str | None = None): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) q = select(EmailLog).where(EmailLog.tenant_id == tenant_id, EmailLog.status.in_(["PENDING", "FAILED"])) if branch_id: @@ -379,7 +380,7 @@ def process_email_queue_page(request: Request, csrf_token: str = Form(...), limi if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) result = process_pending_email_queue(db, tenant_id=tenant_id, branch_id=branch_id, limit=limit) db.commit() @@ -397,7 +398,7 @@ def email_inbox_page(request: Request, flash: str | None = None): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) q = select(EmailIncomingMessage).where(EmailIncomingMessage.tenant_id == tenant_id) if branch_id: @@ -427,7 +428,7 @@ def fetch_email_inbox_page( if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) setting = get_or_create_email_setting(db, int(tenant_id), branch_id, actor_user_id=int(user.id)) result = fetch_incoming_emails( @@ -458,7 +459,7 @@ def email_inbox_detail_page(request: Request, message_id: int): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) filters = [EmailIncomingMessage.id == message_id, EmailIncomingMessage.tenant_id == tenant_id] if branch_id: @@ -503,7 +504,7 @@ def email_inbox_map_all(request: Request, csrf_token: str = Form(...), limit: in if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) result = auto_map_unmapped_emails(db, tenant_id=tenant_id, branch_id=branch_id, limit=limit) db.commit() @@ -529,7 +530,7 @@ def email_inbox_manual_map_page( if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) filters = [EmailIncomingMessage.id == message_id, EmailIncomingMessage.tenant_id == tenant_id] if branch_id: @@ -557,7 +558,7 @@ def email_templates_page(request: Request): if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) seed_default_email_templates(db, tenant_id=tenant_id, branch_id=branch_id) db.commit() @@ -590,7 +591,7 @@ def update_email_template( if not user: return _redirect_login() if not _user_can_manage_email(db, user): - return RedirectResponse(url="/employee/dashboard", status_code=303) + return forbidden_response(request, "Access denied: email settings require administrator permission") tenant_id, branch_id = _scope_from_request(request, user) row = db.execute( select(EmailTemplate).where( diff --git a/app/modules/marketplace/ui.py b/app/modules/marketplace/ui.py index d3feecf..0f79a15 100644 --- a/app/modules/marketplace/ui.py +++ b/app/modules/marketplace/ui.py @@ -2,6 +2,7 @@ from __future__ import annotations from fastapi import APIRouter, Form, Request from fastapi.responses import RedirectResponse +from app.core.http_responses import not_found_response from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf @@ -289,7 +290,7 @@ def lead_detail(request: Request, lead_id: int): return response lead = get_marketplace_lead(db, lead_id) if not lead: - return RedirectResponse(url="/marketplace/leads", status_code=303) + return not_found_response(request, "Marketplace lead not found") return _render(request, "modules/marketplace/templates/marketplace/lead_detail.html", db, user, title=f"Lead {lead.lead_no}", lead=lead, audit_firms=list_reference_audit_firms(db), branches=list_reference_branches(db, lead.assigned_tenant_id), partners=list_reference_partners(db, lead.assigned_tenant_id), can_assign=_has_perm(db, user, "marketplace_leads.assign"), can_update=_has_perm(db, user, "marketplace_leads.update"), can_convert=_has_perm(db, user, "marketplace_leads.convert")) finally: db.close() diff --git a/app/modules/work_detail/ui.py b/app/modules/work_detail/ui.py index 7a280cd..5bcb2a8 100644 --- a/app/modules/work_detail/ui.py +++ b/app/modules/work_detail/ui.py @@ -2,6 +2,7 @@ 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 @@ -38,6 +39,39 @@ def _fallback_for_user(db, user) -> str: 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() + + +@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() @@ -47,7 +81,7 @@ def unified_engagement_detail(request: Request, engagement_id: int): return RedirectResponse(url="/login", status_code=303) detail = load_unified_engagement_detail(db, request=request, user=user, engagement_id=engagement_id) if not detail: - return RedirectResponse(url=_fallback_for_user(db, user), status_code=303) + 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),