felhasználói felületre pü
This commit is contained in:
156
plans/logic_spec_fix_401_ledger_endpoint.md
Normal file
156
plans/logic_spec_fix_401_ledger_endpoint.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 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`](backend/app/api/deps.py:27)) which extracts the JWT token **exclusively** from the `Authorization: Bearer <token>` HTTP header. It does **NOT** read tokens from cookies.
|
||||
|
||||
```python
|
||||
# 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`](frontend_admin/pages/finance/ledger.vue:348) uses **native `fetch()`**:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
function getHeaders(): Record<string, string> {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
return tokenCookie.value
|
||||
? { Authorization: `Bearer ${tokenCookie.value}` }
|
||||
: {}
|
||||
}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- [`commission-rules.vue:694`](frontend_admin/pages/finance/commission-rules.vue:694) — has `getHeaders()`
|
||||
- [`providers/index.vue:288`](frontend_admin/pages/providers/index.vue:288) — has `getHeaders()`
|
||||
- [`gamification/ledger.vue:244`](frontend_admin/pages/gamification/ledger.vue:244) — has `getHeaders()`
|
||||
|
||||
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](frontend_admin/plugins/api.ts:5) 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`](frontend_admin/pages/finance/ledger.vue)
|
||||
|
||||
**File to modify:** `frontend_admin/pages/finance/ledger.vue`
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. Add `getHeaders()` helper function in the `<script setup>` block:
|
||||
|
||||
```typescript
|
||||
function getHeaders(): Record<string, string> {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
return tokenCookie.value
|
||||
? { Authorization: `Bearer ${tokenCookie.value}` }
|
||||
: {}
|
||||
}
|
||||
```
|
||||
|
||||
2. Add `useCookie` import at the top:
|
||||
|
||||
```typescript
|
||||
import { useCookie } from '#app'
|
||||
```
|
||||
|
||||
3. Replace the native `fetch()` call with `$fetch()` and pass headers:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
})
|
||||
```
|
||||
|
||||
4. Adjust error handling — `$fetch` throws on non-2xx responses.
|
||||
|
||||
### 2.2 No Backend Changes Required
|
||||
|
||||
The [`finance_admin.py`](backend/app/api/v1/endpoints/finance_admin.py:81-96) security dependencies are **correct**:
|
||||
|
||||
```python
|
||||
@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`](backend/app/api/v1/endpoints/admin_commission.py:72): `current_user: User = Depends(get_current_staff)`
|
||||
- [`finance_admin.py:30-31`](backend/app/api/v1/endpoints/finance_admin.py:30): `list_issuers` uses 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
|
||||
|
||||
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 |
|
||||
Reference in New Issue
Block a user