admin felület különválasztva

This commit is contained in:
Roo
2026-06-24 11:29:45 +00:00
parent 71ef33bb85
commit 80a5d67f79
462 changed files with 87873 additions and 312 deletions

View File

@@ -0,0 +1,266 @@
# P0 Architect Report: Backoffice SSO & Router Architecture
**Date:** 2026-06-24
**Scope:** Cross-subdomain cookie SSO, backend RBAC dependencies, frontend admin routing
**Auditor:** Fast Coder (Core Developer)
---
## 1. AUDIT: Cross-Subdomain Cookies (Backend Auth)
### File: [`backend/app/api/v1/endpoints/auth.py`](backend/app/api/v1/endpoints/auth.py)
#### Current State (Lines 127134, 221228)
Both the `/login` and `/verify-email` endpoints set the `refresh_token` cookie like this:
```python
response.set_cookie(
key="refresh_token",
value=refresh,
httponly=True,
secure=True,
samesite="lax",
max_age=max_age_sec
)
```
#### ❌ Finding: `domain` parameter is MISSING
The `domain` parameter is **not set** on either `set_cookie()` call. This means the cookie is scoped to the **exact origin** that issued it.
**Impact for SSO:**
- If the user logs in at `app.servicefinder.hu`, the `refresh_token` cookie is only sent to `app.servicefinder.hu`.
- When the user navigates to `admin.servicefinder.hu`, the browser **will not send** the cookie because the domain doesn't match.
- This breaks cross-subdomain SSO entirely — the admin subdomain cannot silently refresh the access token.
#### Required Fix
Add `domain=".servicefinder.hu"` (with leading dot for subdomain wildcard) to both `set_cookie()` calls:
```python
response.set_cookie(
key="refresh_token",
value=refresh,
httponly=True,
secure=True,
samesite="lax",
domain=".servicefinder.hu", # ← ADD THIS
max_age=max_age_sec
)
```
**Considerations:**
- The domain should be **dynamic** based on environment (e.g., `.servicefinder.hu` for production, `localhost` for dev).
- Use `settings.COOKIE_DOMAIN` or derive it from `settings.FRONTEND_BASE_URL`.
- `samesite="lax"` is correct — it allows the cookie to be sent on top-level navigations between subdomains.
---
## 2. AUDIT: Backend Dependencies (Deps for Staff)
### File: [`backend/app/api/deps.py`](backend/app/api/deps.py)
#### 2a. Existing `get_current_admin` (Lines 160177)
```python
async def get_current_admin(current_user: User = Depends(get_current_user)) -> User:
allowed_roles = {
UserRole.SUPERADMIN,
UserRole.ADMIN,
UserRole.MODERATOR,
}
if current_user.role not in allowed_roles:
raise HTTPException(status_code=403, detail="...")
return current_user
```
**✅ Good:** This already exists and allows `SUPERADMIN`, `ADMIN`, and `MODERATOR`.
**❌ Gap:** It does **not** include `SALES_REP` or `SERVICE_MGR`. The Architect's vision is that the Backoffice (`admin.servicefinder.hu`) should admit all "staff" roles — not just superadmins.
#### 2b. Existing `RequireSystemCapability` (Lines 184218)
```python
def RequireSystemCapability(capability_name: str):
async def system_capability_checker(current_user: User = Depends(get_current_user)) -> bool:
if current_user.role == UserRole.SUPERADMIN:
return True
role_key = current_user.role.value
if not role_has_capability(role_key, capability_name):
raise HTTPException(status_code=403, detail=f"...")
return True
return system_capability_checker
```
**✅ Good:** This is the scalable, fine-grained approach. Each endpoint can declare exactly which capability it needs.
**❌ Gap:** There is no **middle-ground** dependency — something between `get_current_admin` (hardcoded role set) and `RequireSystemCapability` (fine-grained). We need a `RequireRole` dependency that accepts a list of allowed roles.
#### 2c. Proposed: `RequireRole` Dependency
Add to [`backend/app/api/deps.py`](backend/app/api/deps.py):
```python
def RequireRole(allowed_roles: List[UserRole]):
"""
🎯 Flexible role-checking dependency.
Accepts a list of UserRole values. If the current user's role is
in the list, access is granted. Otherwise, 403 Forbidden.
Usage:
@router.get("/admin/sales")
async def sales_dashboard(
current_user: User = Depends(get_current_user),
_ = Depends(RequireRole([UserRole.SUPERADMIN, UserRole.SALES_REP]))
):
"""
async def role_checker(
current_user: User = Depends(get_current_user),
) -> bool:
if current_user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="AUTH.INSUFFICIENT_PERMISSIONS"
)
return True
return role_checker
```
And a **convenience** `get_current_staff` that admits all internal staff:
```python
STAFF_ROLES = {
UserRole.SUPERADMIN,
UserRole.ADMIN,
UserRole.MODERATOR,
UserRole.SALES_REP,
UserRole.SERVICE_MGR,
}
async def get_current_staff(
current_user: User = Depends(get_current_user)
) -> User:
if current_user.role not in STAFF_ROLES:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="AUTH.STAFF_ONLY"
)
return current_user
```
---
## 3. AUDIT: Frontend Architecture (Admin Domain Routing)
### File: [`frontend/src/router/index.ts`](frontend/src/router/index.ts)
#### 3a. Current Admin Route (Lines 112137)
```typescript
{
path: '/admin',
component: () => import('../layouts/AdminLayout.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
children: [
{ path: '', name: 'admin-dashboard', component: () => import('../views/admin/AdminDashboardView.vue') },
{ path: 'users', name: 'admin-users', component: () => import('../views/admin/AdminUsersView.vue') },
{ path: 'packages', name: 'admin-packages', component: () => import('../views/admin/AdminPackagesView.vue') },
{ path: 'services', name: 'admin-services', component: () => import('../views/admin/AdminServicesView.vue') },
]
}
```
**✅ Good:** The admin routes exist under a single `/admin` path with `requiresAdmin` meta.
#### 3b. Current Route Guard (Lines 166177)
```typescript
if (to.meta.requiresAdmin) {
const role = authStore.user?.role
const adminRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR']
if (!role || !adminRoles.includes(role.toUpperCase())) {
return next('/dashboard')
}
}
```
**❌ Finding: The guard is too restrictive.** It only admits `SUPERADMIN`, `ADMIN`, and `MODERATOR`. `SALES_REP` and `SERVICE_MGR` are blocked from the admin panel entirely.
**❌ Finding: No separate subdomain routing.** The admin UI is served under the same SPA at `/admin` path, not as a separate deployment for `admin.servicefinder.hu`. This means:
- The same Vite build serves both `app.servicefinder.hu` and `admin.servicefinder.hu`.
- The Nginx reverse proxy must route `admin.servicefinder.hu` → same SPA, but the router handles the `/admin` prefix.
- This is **fine for Phase 1** but limits independent scaling/deployment.
#### 3c. Auth Store `isAdmin` Getter (Lines 103110)
```typescript
const isAdmin = computed(() => {
const role = user.value?.role
if (!role) return false
const adminRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR']
return adminRoles.includes(role.toUpperCase())
})
```
**❌ Same gap:** `SALES_REP` and `SERVICE_MGR` are not recognized as admin/staff.
#### 3d. AdminLayout Component
**File:** [`frontend/src/layouts/AdminLayout.vue`](frontend/src/layouts/AdminLayout.vue)
**✅ Good:** The layout has a proper sidebar with navigation links, user profile section, and slot for header controls. It references `LanguageSwitcher`, `ModeSwitcher`, and `HeaderProfile`.
**❌ Finding:** The sidebar links include routes that don't exist in the router (`/admin/vehicles`, `/admin/translations`, `/admin/system`, `/admin/audit`). These will produce 404s when clicked.
---
## 4. ACTION PLAN
### Phase 1: Fix Cookie Domain for SSO (Backend)
| # | Task | File | Details |
|---|------|------|---------|
| 1.1 | Add `COOKIE_DOMAIN` to settings | [`backend/app/core/config.py`](backend/app/core/config.py) | Add `COOKIE_DOMAIN: str = ".servicefinder.hu"` field |
| 1.2 | Add `domain` param to `/login` cookie | [`backend/app/api/v1/endpoints/auth.py:127`](backend/app/api/v1/endpoints/auth.py:127) | `response.set_cookie(..., domain=settings.COOKIE_DOMAIN)` |
| 1.3 | Add `domain` param to `/verify-email` cookie | [`backend/app/api/v1/endpoints/auth.py:221`](backend/app/api/v1/endpoints/auth.py:221) | Same fix |
### Phase 2: Create Scalable Backend Dependencies
| # | Task | File | Details |
|---|------|------|---------|
| 2.1 | Add `RequireRole` dependency | [`backend/app/api/deps.py`](backend/app/api/deps.py) | Generic role-list checker |
| 2.2 | Add `get_current_staff` dependency | [`backend/app/api/deps.py`](backend/app/api/deps.py) | Admits SUPERADMIN, ADMIN, MODERATOR, SALES_REP, SERVICE_MGR |
| 2.3 | Update `get_current_admin` to use STAFF_ROLES | [`backend/app/api/deps.py`](backend/app/api/deps.py) | Or keep as-is for superadmin-only endpoints |
### Phase 3: Update Frontend Router Guard & Auth Store
| # | Task | File | Details |
|---|------|------|---------|
| 3.1 | Update `requiresAdmin` guard to admit all staff roles | [`frontend/src/router/index.ts:168`](frontend/src/router/index.ts:168) | Change `adminRoles` to include `SALES_REP`, `SERVICE_MGR` |
| 3.2 | Update `isAdmin` getter in auth store | [`frontend/src/stores/auth.ts:103`](frontend/src/stores/auth.ts:103) | Same role expansion |
| 3.3 | Add missing admin routes or remove dead sidebar links | [`frontend/src/layouts/AdminLayout.vue`](frontend/src/layouts/AdminLayout.vue) | Add routes for vehicles, translations, system, audit — or remove sidebar links |
| 3.4 | Consider role-based sidebar visibility | [`frontend/src/layouts/AdminLayout.vue`](frontend/src/layouts/AdminLayout.vue) | SALES_REP should only see sales-related nav items |
### Phase 4: Nginx / Reverse Proxy (Infrastructure)
| # | Task | Details |
|---|------|---------|
| 4.1 | Ensure `admin.servicefinder.hu` → same SPA | Nginx config must serve the same frontend build for both subdomains |
| 4.2 | Ensure CORS allows `admin.servicefinder.hu` | Already in [`backend/app/core/config.py:93`](backend/app/core/config.py:93) ✅ |
---
## Summary of Findings
| Area | Status | Issue |
|------|--------|-------|
| Cookie `domain` | ❌ Missing | SSO broken between subdomains |
| `get_current_admin` | ⚠️ Too narrow | Missing SALES_REP, SERVICE_MGR |
| `RequireSystemCapability` | ✅ Good | Fine-grained, scalable |
| `RequireRole` | ❌ Missing | No flexible role-list dependency |
| Frontend `requiresAdmin` guard | ⚠️ Too narrow | Blocks SALES_REP, SERVICE_MGR |
| Frontend `isAdmin` getter | ⚠️ Too narrow | Same gap |
| AdminLayout sidebar links | ❌ Dead links | Routes for vehicles/translations/system/audit don't exist |
| CORS origins | ✅ Good | `admin.servicefinder.hu` already whitelisted |