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:
commission-rules.vue:694— hasgetHeaders()providers/index.vue:288— hasgetHeaders()gamification/ledger.vue:244— hasgetHeaders()
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:
- 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}` }
: {}
}
- Add
useCookieimport at the top:
import { useCookie } from '#app'
- 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
})
- Adjust error handling —
$fetchthrows 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:
admin_commission.py:72:current_user: User = Depends(get_current_staff)finance_admin.py:30-31:list_issuersuses identical pattern and works
The 401 is purely a frontend auth header omission — the backend auth chain and permission check are correct.
3. Verification Steps
- After applying the fix, the frontend dev server should be running.
- Log in as admin (
admin@profibot.hu/Admin123!). - Navigate to
/finance/ledger. - 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 |