fix(marketplace): align admin api with backend endpoints

This commit is contained in:
unclecode
2025-10-08 15:57:15 +08:00
parent d2c7f345ab
commit 2c373f0642
3 changed files with 109 additions and 12 deletions

View File

@@ -1,10 +1,38 @@
// Admin Dashboard - Smart & Powerful // Admin Dashboard - Smart & Powerful
const { API_BASE, API_ORIGIN } = (() => { const { API_BASE, API_ORIGIN } = (() => {
const { hostname, port } = window.location; const cleanOrigin = (value) => value ? value.replace(/\/$/, '') : '';
if ((hostname === 'localhost' || hostname === '127.0.0.1') && port === '8000') { const params = new URLSearchParams(window.location.search);
const origin = 'http://127.0.0.1:8100'; const overrideParam = cleanOrigin(params.get('api_origin'));
return { API_BASE: `${origin}/api`, API_ORIGIN: origin };
let storedOverride = '';
try {
storedOverride = cleanOrigin(localStorage.getItem('marketplace_api_origin'));
} catch (error) {
storedOverride = '';
} }
let origin = overrideParam || storedOverride;
if (overrideParam && overrideParam !== storedOverride) {
try {
localStorage.setItem('marketplace_api_origin', overrideParam);
} catch (error) {
// ignore storage errors (private mode, etc.)
}
}
const { protocol, hostname, port } = window.location;
const isLocalHost = ['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname);
if (!origin && isLocalHost && port !== '8100') {
origin = `${protocol}//127.0.0.1:8100`;
}
if (origin) {
const normalized = cleanOrigin(origin);
return { API_BASE: `${normalized}/api`, API_ORIGIN: normalized };
}
return { API_BASE: '/api', API_ORIGIN: '' }; return { API_BASE: '/api', API_ORIGIN: '' };
})(); })();
@@ -185,7 +213,9 @@ class AdminDashboard {
} }
async loadStats() { async loadStats() {
const stats = await this.apiCall('/admin/stats'); const stats = await this.apiCall(`/admin/stats?_=${Date.now()}`, {
cache: 'no-store'
});
document.getElementById('stat-apps').textContent = stats.apps.total; document.getElementById('stat-apps').textContent = stats.apps.total;
document.getElementById('stat-featured').textContent = stats.apps.featured; document.getElementById('stat-featured').textContent = stats.apps.featured;
@@ -196,17 +226,24 @@ class AdminDashboard {
} }
async loadApps() { async loadApps() {
this.data.apps = await this.apiCall('/apps?limit=100'); this.data.apps = await this.apiCall(`/apps?limit=100&_=${Date.now()}`, {
cache: 'no-store'
});
this.renderAppsTable(this.data.apps); this.renderAppsTable(this.data.apps);
} }
async loadArticles() { async loadArticles() {
this.data.articles = await this.apiCall('/articles?limit=100'); this.data.articles = await this.apiCall(`/articles?limit=100&_=${Date.now()}`, {
cache: 'no-store'
});
this.renderArticlesTable(this.data.articles); this.renderArticlesTable(this.data.articles);
} }
async loadCategories() { async loadCategories() {
this.data.categories = await this.apiCall('/categories'); const cacheBuster = Date.now();
this.data.categories = await this.apiCall(`/categories?_=${cacheBuster}`, {
cache: 'no-store'
});
this.renderCategoriesTable(this.data.categories); this.renderCategoriesTable(this.data.categories);
} }
@@ -664,8 +701,10 @@ class AdminDashboard {
data.description = document.getElementById('form-description').value; data.description = document.getElementById('form-description').value;
data.category = document.getElementById('form-category').value; data.category = document.getElementById('form-category').value;
data.type = document.getElementById('form-type').value; data.type = document.getElementById('form-type').value;
data.rating = parseFloat(document.getElementById('form-rating').value); const rating = parseFloat(document.getElementById('form-rating').value);
data.downloads = parseInt(document.getElementById('form-downloads').value); const downloads = parseInt(document.getElementById('form-downloads').value, 10);
data.rating = Number.isFinite(rating) ? rating : 0;
data.downloads = Number.isFinite(downloads) ? downloads : 0;
data.image = document.getElementById('form-image').value; data.image = document.getElementById('form-image').value;
data.website_url = document.getElementById('form-website').value; data.website_url = document.getElementById('form-website').value;
data.github_url = document.getElementById('form-github').value; data.github_url = document.getElementById('form-github').value;
@@ -686,7 +725,8 @@ class AdminDashboard {
data.slug = this.generateSlug(data.name); data.slug = this.generateSlug(data.name);
data.icon = document.getElementById('form-icon').value; data.icon = document.getElementById('form-icon').value;
data.description = document.getElementById('form-description').value; data.description = document.getElementById('form-description').value;
data.order_index = parseInt(document.getElementById('form-order').value); const orderIndex = parseInt(document.getElementById('form-order').value, 10);
data.order_index = Number.isFinite(orderIndex) ? orderIndex : 0;
} else if (type === 'sponsors') { } else if (type === 'sponsors') {
data.company_name = document.getElementById('form-name').value; data.company_name = document.getElementById('form-name').value;
data.logo_url = document.getElementById('form-logo-url').value; data.logo_url = document.getElementById('form-logo-url').value;

View File

@@ -210,6 +210,6 @@
</div> </div>
</div> </div>
<script src="admin.js?v=1759327900"></script> <script src="admin.js?v=1759334000"></script>
</body> </body>
</html> </html>

View File

@@ -7,6 +7,7 @@ from typing import Optional, Dict, Any
import json import json
import hashlib import hashlib
import secrets import secrets
import re
from pathlib import Path from pathlib import Path
from database import DatabaseManager from database import DatabaseManager
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -58,6 +59,29 @@ def json_response(data, cache_time=3600):
} }
) )
def to_int(value, default=0):
"""Coerce incoming values to integers, falling back to default."""
if value is None:
return default
if isinstance(value, bool):
return int(value)
if isinstance(value, (int, float)):
return int(value)
if isinstance(value, str):
stripped = value.strip()
if not stripped:
return default
match = re.match(r"^-?\d+", stripped)
if match:
try:
return int(match.group())
except ValueError:
return default
return default
# ============= PUBLIC ENDPOINTS ============= # ============= PUBLIC ENDPOINTS =============
@app.get("/api/apps") @app.get("/api/apps")
@@ -141,6 +165,8 @@ async def get_article(slug: str):
async def get_categories(): async def get_categories():
"""Get all categories ordered by index""" """Get all categories ordered by index"""
categories = db.get_all('categories', limit=50) categories = db.get_all('categories', limit=50)
for category in categories:
category['order_index'] = to_int(category.get('order_index'), 0)
categories.sort(key=lambda x: x.get('order_index', 0)) categories.sort(key=lambda x: x.get('order_index', 0))
return json_response(categories, cache_time=7200) return json_response(categories, cache_time=7200)
@@ -360,6 +386,9 @@ async def delete_article(article_id: int):
async def create_category(category_data: Dict[str, Any]): async def create_category(category_data: Dict[str, Any]):
"""Create new category""" """Create new category"""
try: try:
category_data = dict(category_data)
category_data['order_index'] = to_int(category_data.get('order_index'), 0)
cursor = db.conn.cursor() cursor = db.conn.cursor()
columns = ', '.join(category_data.keys()) columns = ', '.join(category_data.keys())
placeholders = ', '.join(['?' for _ in category_data]) placeholders = ', '.join(['?' for _ in category_data])
@@ -374,6 +403,10 @@ async def create_category(category_data: Dict[str, Any]):
async def update_category(cat_id: int, category_data: Dict[str, Any]): async def update_category(cat_id: int, category_data: Dict[str, Any]):
"""Update category""" """Update category"""
try: try:
category_data = dict(category_data)
if 'order_index' in category_data:
category_data['order_index'] = to_int(category_data.get('order_index'), 0)
set_clause = ', '.join([f"{k} = ?" for k in category_data.keys()]) set_clause = ', '.join([f"{k} = ?" for k in category_data.keys()])
cursor = db.conn.cursor() cursor = db.conn.cursor()
cursor.execute(f"UPDATE categories SET {set_clause} WHERE id = ?", cursor.execute(f"UPDATE categories SET {set_clause} WHERE id = ?",
@@ -383,6 +416,18 @@ async def update_category(cat_id: int, category_data: Dict[str, Any]):
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@app.delete("/api/admin/categories/{cat_id}", dependencies=[Depends(verify_token)])
async def delete_category(cat_id: int):
"""Delete category"""
try:
cursor = db.conn.cursor()
cursor.execute("DELETE FROM categories WHERE id = ?", (cat_id,))
db.conn.commit()
return {"message": "Category deleted"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# Sponsors CRUD # Sponsors CRUD
@app.post("/api/admin/sponsors", dependencies=[Depends(verify_token)]) @app.post("/api/admin/sponsors", dependencies=[Depends(verify_token)])
async def create_sponsor(sponsor_data: Dict[str, Any]): async def create_sponsor(sponsor_data: Dict[str, Any]):
@@ -411,6 +456,18 @@ async def update_sponsor(sponsor_id: int, sponsor_data: Dict[str, Any]):
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@app.delete("/api/admin/sponsors/{sponsor_id}", dependencies=[Depends(verify_token)])
async def delete_sponsor(sponsor_id: int):
"""Delete sponsor"""
try:
cursor = db.conn.cursor()
cursor.execute("DELETE FROM sponsors WHERE id = ?", (sponsor_id,))
db.conn.commit()
return {"message": "Sponsor deleted"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/") @app.get("/")
async def root(): async def root():
"""API info""" """API info"""