Files
service-finder/plans/logic_spec_fix_401_ledger_endpoint.md
2026-07-27 08:39:18 +00:00

5.3 KiB

Logic Spec: Fix 401 Unauthorized on Admin Ledger Endpoint

Date: 2026-07-25 Severity: P0 (Blocker — admin users cannot access Financial Ledger page) Related Issue: Frontend redirects to login screen on /finance/ledger


1. Root Cause Analysis

1.1 Authentication Architecture

The FastAPI backend uses OAuth2PasswordBearer (deps.py:27-29) which extracts the JWT token exclusively from the Authorization: Bearer <token> HTTP header. It does NOT read tokens from cookies.

# backend/app/api/deps.py:27-29
reusable_oauth2 = OAuth2PasswordBearer(
    tokenUrl=f"{settings.API_V1_STR}/auth/login"
)

The auth dependency chain for the ledger endpoint:

GET /admin/finance/ledger
  → RequirePermission("finance:view")          [deps.py:283]
    → get_current_active_user                  [deps.py:94]
      → get_current_user                       [deps.py:56]
        → get_current_token_payload            [deps.py:31]
          → reusable_oauth2 (OAuth2PasswordBearer)  [deps.py:27]
            → extracts token from Authorization: Bearer header

1.2 The Bug

The ledger.vue:348 uses native fetch():

const res = await fetch(`/api/v1/admin/finance/ledger?${params.toString()}`, {
    credentials: 'include',
})

This sends cookies (including access_token cookie) but does NOT send the Authorization: Bearer header. The OAuth2PasswordBearer dependency never sees a token → 401 Unauthorized.

1.3 Why Other Admin Pages Work

Every other admin page uses $fetch() (Nuxt's ofetch wrapper) with a getHeaders() helper that explicitly adds the Authorization header:

function getHeaders(): Record<string, string> {
    const tokenCookie = useCookie('access_token')
    return tokenCookie.value
        ? { Authorization: `Bearer ${tokenCookie.value}` }
        : {}
}

Examples:

The ledger.vue page omitted both $fetch and the getHeaders() helper — it's the only admin page with this pattern.

1.4 What the 401 Interceptor Does

The api.ts plugin overrides globalThis.fetch to detect 401 responses. When a 401 is detected on any non-login URL, it calls authStore.logout() and redirects to /login. This is why the user sees a redirect — the interceptor fires, clears the token, and forces a redirect.

The interceptor is working correctly. The problem is the request never had a token attached in the first place.


2. Fix Plan

2.1 Primary Fix: Frontend — ledger.vue

File to modify: frontend_admin/pages/finance/ledger.vue

Changes:

  1. Add getHeaders() helper function in the <script setup> block:
function getHeaders(): Record<string, string> {
    const tokenCookie = useCookie('access_token')
    return tokenCookie.value
        ? { Authorization: `Bearer ${tokenCookie.value}` }
        : {}
}
  1. Add useCookie import at the top:
import { useCookie } from '#app'
  1. Replace the native fetch() call with $fetch() and pass headers:
// BEFORE (broken):
const res = await fetch(`/api/v1/admin/finance/ledger?${params.toString()}`, {
    credentials: 'include',
})

// AFTER (fixed):
const data: LedgerResponse = await $fetch('/api/v1/admin/finance/ledger', {
    headers: getHeaders(),
    params: params,  // $fetch handles query params natively
})
  1. Adjust error handling — $fetch throws on non-2xx responses.

2.2 No Backend Changes Required

The finance_admin.py security dependencies are correct:

@router.get("/ledger", response_model=FinancialLedgerListResponse)
async def list_financial_ledger(
    ...
    current_user: User = Depends(deps.get_current_active_user),
    _ = Depends(deps.RequirePermission("finance:view")),
):

This is consistent with working endpoints like:

The 401 is purely a frontend auth header omission — the backend auth chain and permission check are correct.


3. Verification Steps

  1. After applying the fix, the frontend dev server should be running.
  2. Log in as admin (admin@profibot.hu / Admin123!).
  3. Navigate to /finance/ledger.
  4. Verify:
    • No 401 error in browser DevTools Network tab
    • Authorization: Bearer <token> header is present in the request
    • Ledger data loads with pagination, filtering
    • No redirect to /login

4. Impact Assessment

Component Change Risk
ledger.vue Add getHeaders(), useCookie import, switch to $fetch Low — exact same pattern used by 20+ other admin pages
Backend None N/A
Other pages None N/A