fix(marketplace): align admin api with backend endpoints
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user