171 lines
5.4 KiB
Markdown
171 lines
5.4 KiB
Markdown
# ✅ Admin Services Fixed - January 11, 2026
|
|
|
|
## 🔴 Problem
|
|
|
|
The Admin Dashboard was unable to load services, showing:
|
|
|
|
```
|
|
Access to XMLHttpRequest at 'http://localhost:8181/api/admin/services?include_inactive=true'
|
|
from origin 'http://localhost:5300' has been blocked by CORS policy:
|
|
No 'Access-Control-Allow-Origin' header is present on the requested resource.
|
|
|
|
GET http://localhost:8181/api/admin/services?include_inactive=true net::ERR_FAILED 500 (Internal Server Error)
|
|
```
|
|
|
|
## 🔍 Root Cause
|
|
|
|
The `/api/admin/services` endpoint was throwing a **500 Internal Server Error** because:
|
|
|
|
- The `service_to_dict()` function tries to access `service.images` relationship
|
|
- The admin endpoint query did NOT eager-load the `images` relationship
|
|
- When SQLAlchemy tried to access the lazy-loaded relationship, it failed in async context
|
|
- This caused the 500 error, which then prevented CORS headers from being sent
|
|
|
|
## ✅ Solution
|
|
|
|
Added eager loading to the admin services endpoint query:
|
|
|
|
**Before:**
|
|
|
|
```python
|
|
@api_router.get("/admin/services")
|
|
async def admin_get_services(include_inactive: bool = False, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
query = select(Service)
|
|
if not include_inactive:
|
|
query = query.where(Service.is_active == True)
|
|
query = query.order_by(desc(Service.created_at))
|
|
result = await db.execute(query)
|
|
services = result.scalars().all()
|
|
return [service_to_dict(s) for s in services]
|
|
```
|
|
|
|
**After:**
|
|
|
|
```python
|
|
@api_router.get("/admin/services")
|
|
async def admin_get_services(include_inactive: bool = False, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
query = select(Service).options(selectinload(Service.images)) # ✅ ADDED THIS
|
|
if not include_inactive:
|
|
query = query.where(Service.is_active == True)
|
|
query = query.order_by(desc(Service.created_at))
|
|
result = await db.execute(query)
|
|
services = result.scalars().all()
|
|
return [service_to_dict(s) for s in services]
|
|
```
|
|
|
|
## 📊 Verification
|
|
|
|
### Admin Services Endpoint Test
|
|
|
|
```bash
|
|
$ curl http://localhost:8181/api/admin/services?include_inactive=true -H "Authorization: Bearer $TOKEN"
|
|
|
|
✅ SUCCESS! Returns all 8 services:
|
|
1. Updated Test Service ($149.99) - setup
|
|
2. Updated Repair Service ($149.99) - repair
|
|
3. Data Recovery ($199.99) - data
|
|
4. Virus Removal ($89.99) - software
|
|
5. Screen Repair ($149.99) - repair
|
|
6. Device Setup ($59.99) - setup
|
|
7. Hardware Upgrade ($49.99) - upgrade
|
|
8. Battery Replacement ($79.99) - repair
|
|
```
|
|
|
|
### Backend Logs
|
|
|
|
```
|
|
INFO: 127.0.0.1:31132 - "GET /api/admin/services?include_inactive=true HTTP/1.1" 200 OK
|
|
```
|
|
|
|
## 🎯 Status
|
|
|
|
| Endpoint | Status | Response | Services |
|
|
|----------|--------|----------|----------|
|
|
| `/api/services` | ✅ Working | 200 OK | 8 services |
|
|
| `/api/admin/services` | ✅ **NOW FIXED** | 200 OK | 8 services |
|
|
|
|
## 🧪 Testing
|
|
|
|
### Test Admin Dashboard
|
|
|
|
```
|
|
http://localhost:5300/admin
|
|
```
|
|
|
|
1. Login with admin credentials
|
|
2. Navigate to **Services** tab
|
|
3. Services should now load correctly with all 8 services displayed
|
|
|
|
### Backend Direct Test
|
|
|
|
```bash
|
|
cd /media/pts/Website/PromptTech_Solution_Site/backend
|
|
source venv/bin/activate
|
|
TOKEN=$(python test_upload.py 2>/dev/null | grep "eyJ" | head -1 | tr -d ' ')
|
|
curl -s http://localhost:8181/api/admin/services?include_inactive=true \
|
|
-H "Authorization: Bearer $TOKEN" | jq
|
|
```
|
|
|
|
## 📝 Technical Details
|
|
|
|
### The Issue with Async SQLAlchemy
|
|
|
|
- SQLAlchemy async sessions cannot lazy-load relationships
|
|
- When accessing `service.images` without eager loading, it tries to issue a new query
|
|
- In async context, this fails because the session is not in an active transaction
|
|
- Result: AttributeError or DetachedInstanceError
|
|
|
|
### The Solution
|
|
|
|
- **Eager Loading**: Use `.options(selectinload(Service.images))`
|
|
- This loads all images in a single additional query (efficient)
|
|
- Images are available immediately when `service_to_dict()` accesses them
|
|
- No lazy loading = No async errors
|
|
|
|
### Why CORS Error Appeared
|
|
|
|
- The 500 error prevented the response from being sent
|
|
- FastAPI's CORS middleware only adds headers to successful responses
|
|
- Browser saw no CORS headers and blocked the request
|
|
- User saw CORS error, but real issue was the 500 error underneath
|
|
|
|
## 🔧 Files Modified
|
|
|
|
- **backend/server.py** (line 1363): Added `selectinload(Service.images)` to admin services query
|
|
|
|
## ✨ Comparison with Public Endpoint
|
|
|
|
**Public Services Endpoint** (Already Working):
|
|
|
|
```python
|
|
@api_router.get("/services")
|
|
async def get_services(category: Optional[str] = None, db: AsyncSession = Depends(get_db)):
|
|
query = select(Service).where(Service.is_active == True)
|
|
if category and category != "all":
|
|
query = query.where(Service.category == category)
|
|
query = query.options(selectinload(Service.reviews).selectinload(Review.user))
|
|
# ... rest of query
|
|
```
|
|
|
|
✅ This one already had eager loading for reviews, so it worked fine.
|
|
|
|
**Admin Endpoint** (Was Missing Eager Loading):
|
|
|
|
- Was not loading images relationship
|
|
- Now fixed with same pattern
|
|
|
|
## 🎉 Result
|
|
|
|
- ✅ Admin services endpoint returns 200 OK
|
|
- ✅ All 8 services load correctly
|
|
- ✅ CORS headers present
|
|
- ✅ Admin Dashboard services tab functional
|
|
- ✅ No more 500 errors
|
|
|
|
---
|
|
|
|
**Fixed By**: GitHub Copilot
|
|
**Date**: January 11, 2026, 10:59 PM CST
|
|
**Issue**: Admin services 500 error + CORS blocking
|
|
**Resolution**: Added eager loading for Service.images relationship
|