Spaces:
Running
Running
| from fastapi import FastAPI, HTTPException, Header, Depends, Request | |
| from fastapi.responses import JSONResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import json | |
| import uvicorn | |
| import os | |
| from typing import List, Dict, Optional | |
| # ----------------------------- | |
| # CONFIGURATION & CONSTANTS | |
| # ----------------------------- | |
| DATA_FILE = "ddcet_dataset_raw.json" | |
| BLOG_FILE = "blogs.json" | |
| ADMIN_KEY_ENV = os.getenv("ADMIN_KEY", "secret123") # Set this in HF Spaces Secrets | |
| # ----------------------------- | |
| # LOAD DATA | |
| # ----------------------------- | |
| # Helper to load raw dataset safely | |
| def load_raw_data(): | |
| if not os.path.exists(DATA_FILE): | |
| # Fallback for demo if file missing | |
| return {"subjects": []} | |
| with open(DATA_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| raw_data = load_raw_data() | |
| def load_blogs(): | |
| if not os.path.exists(BLOG_FILE): | |
| return [] | |
| try: | |
| with open(BLOG_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except: | |
| return [] | |
| def save_blogs(data): | |
| with open(BLOG_FILE, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=4) | |
| blogs_data = load_blogs() | |
| # Create quick lookup maps for performance | |
| subjects_map: Dict[int, Dict] = {} | |
| units_map: Dict[int, Dict] = {} | |
| questions_map: Dict[int, List[Dict]] = {} | |
| if "subjects" in raw_data: | |
| for subj in raw_data["subjects"]: | |
| sid = subj["SubjectID"] | |
| subjects_map[sid] = {"SubjectID": sid, "SubjectName": subj["SubjectName"]} | |
| for unit in subj["units"]: | |
| uid = unit["UnitID"] | |
| units_map[uid] = {"UnitID": uid, "UnitName": unit.get("UnitName",""), "SubjectID": sid} | |
| questions_map[uid] = unit.get("questions", []) | |
| # ----------------------------- | |
| # AUTHENTICATION | |
| # ----------------------------- | |
| async def verify_admin(x_admin_key: str = Header(None)): | |
| """ | |
| Checks for x-admin-key in the header. | |
| """ | |
| if x_admin_key != ADMIN_KEY_ENV: | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| return x_admin_key | |
| # ----------------------------- | |
| # FASTAPI INSTANCE | |
| # ----------------------------- | |
| app = FastAPI(title="DDCET MCQ API Fast", version="1.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ----------------------------- | |
| # ENDPOINTS | |
| # ----------------------------- | |
| def get_subjects(): | |
| return list(subjects_map.values()) | |
| def get_units(subject_id: int): | |
| units = [u for u in units_map.values() if u["SubjectID"] == subject_id] | |
| if not units: | |
| raise HTTPException(status_code=404, detail="No units found for this subject") | |
| return units | |
| def get_questions(unit_id: int): | |
| qs = questions_map.get(unit_id) | |
| if qs is None: | |
| raise HTTPException(status_code=404, detail="No questions found for this unit") | |
| return qs | |
| def get_question(mcqid: int): | |
| for qlist in questions_map.values(): | |
| for q in qlist: | |
| if q["QuestionObject"]["MCQID"] == mcqid: | |
| return q | |
| raise HTTPException(status_code=404, detail="Question not found") | |
| def get_blogs(): | |
| return blogs_data | |
| # PROTECTED ENDPOINT | |
| def add_blog(blog: dict): | |
| blog["id"] = blog.get("id", len(blogs_data) + 1) | |
| # Ensure fields exist for the frontend | |
| blog["date"] = blog.get("date", "") | |
| blogs_data.insert(0, blog) # Add to top | |
| save_blogs(blogs_data) | |
| return {"message": "Blog added", "blog": blog} | |
| # ----------------------------- | |
| # ADMIN PANEL HTML UI | |
| # ----------------------------- | |
| def admin_panel(): | |
| html_content = """ | |
| <!DOCTYPE html> | |
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Admin Panel</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { background-color: #09090b; color: #e4e4e7; font-family: monospace; } | |
| .input-field { background: #18181b; border: 1px solid #27272a; color: white; padding: 0.5rem; width: 100%; outline: none; margin-bottom: 1rem; } | |
| .input-field:focus { border-color: #52525b; } | |
| .btn { background: #fafafa; color: black; padding: 0.5rem 1rem; font-weight: bold; cursor: pointer; transition: 0.2s; } | |
| .btn:hover { background: #d4d4d8; } | |
| .card { border: 1px solid #27272a; padding: 1rem; margin-bottom: 1rem; background: #0c0c0e; } | |
| </style> | |
| </head> | |
| <body class="max-w-3xl mx-auto p-6"> | |
| <div class="flex justify-between items-center mb-8 border-b border-zinc-800 pb-4"> | |
| <h1 class="text-xl font-bold tracking-widest uppercase">Admin /// Console</h1> | |
| <div class="flex gap-2"> | |
| <input type="password" id="apiKey" placeholder="Enter Admin Key" class="bg-zinc-900 border border-zinc-800 px-3 py-1 text-xs w-48 text-white"> | |
| <button onclick="saveKey()" class="text-xs border border-zinc-700 px-2 hover:bg-zinc-800">AUTH</button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <!-- ADD BLOG FORM --> | |
| <div> | |
| <h2 class="text-sm text-zinc-500 mb-4 uppercase">/// New Entry</h2> | |
| <form id="blogForm" onsubmit="submitBlog(event)"> | |
| <label class="text-xs text-zinc-500">Title</label> | |
| <input type="text" name="title" required class="input-field"> | |
| <label class="text-xs text-zinc-500">Image URL</label> | |
| <input type="url" name="image" required class="input-field"> | |
| <label class="text-xs text-zinc-500">Link URL</label> | |
| <input type="url" name="url" required class="input-field"> | |
| <label class="text-xs text-zinc-500">Summary</label> | |
| <textarea name="summary" rows="3" required class="input-field"></textarea> | |
| <label class="text-xs text-zinc-500">Full HTML Content</label> | |
| <textarea name="content_html" rows="6" required class="input-field"></textarea> | |
| <div class="flex gap-4"> | |
| <div class="w-1/2"> | |
| <label class="text-xs text-zinc-500">Author</label> | |
| <input type="text" name="author" value="Admin" class="input-field"> | |
| </div> | |
| <div class="w-1/2"> | |
| <label class="text-xs text-zinc-500">Date (YYYY-MM-DD)</label> | |
| <input type="date" name="date" class="input-field text-zinc-400"> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn w-full mt-2">PUBLISH DATA</button> | |
| </form> | |
| <h2 class="text-xs text-zinc-500 mt-6 mb-2 uppercase">/// Live Preview</h2> | |
| <div class="border border-zinc-800 bg-zinc-900 p-4 h-64 overflow-auto" id="livePreview"> | |
| <p class="text-zinc-600 text-xs">Start typing HTML content to preview...</p> | |
| </div> | |
| <!-- PREVIEW LIST --> | |
| <div> | |
| <h2 class="text-xs text-zinc-500 mb-4 uppercase">/// Database Entries</h2> | |
| <div id="blogList" class="space-y-2 h-[500px] overflow-y-auto pr-2"> | |
| <div class="text-zinc-600 text-xs animate-pulse">Loading stream...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const htmlField = document.querySelector('textarea[name="content_html"]'); | |
| const previewBox = document.getElementById('livePreview'); | |
| htmlField.addEventListener('input', () => { | |
| const content = htmlField.value.trim(); | |
| previewBox.innerHTML = content || '<p class="text-zinc-600 text-xs">Start typing HTML content to preview...</p>'; | |
| }); | |
| // Auth Logic | |
| function getAuth() { return localStorage.getItem('admin_key') || ''; } | |
| function saveKey() { | |
| const key = document.getElementById('apiKey').value; | |
| localStorage.setItem('admin_key', key); | |
| alert('Key stored locally.'); | |
| loadBlogs(); | |
| } | |
| document.getElementById('apiKey').value = getAuth(); | |
| // Fetch Blogs | |
| async function loadBlogs() { | |
| const res = await fetch('/blogs'); | |
| const data = await res.json(); | |
| const list = document.getElementById('blogList'); | |
| list.innerHTML = ''; | |
| data.forEach(b => { | |
| list.innerHTML += ` | |
| <div class="card group hover:border-zinc-600 transition-colors"> | |
| <div class="font-bold text-sm text-white">${b.title}</div> | |
| <div class="text-xs text-zinc-500 truncate mt-1">${b.summary}</div> | |
| <div class="mt-2 text-[10px] text-zinc-600 font-mono uppercase">${b.author} // ${b.date}</div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // Submit Blog | |
| async function submitBlog(e) { | |
| e.preventDefault(); | |
| const key = getAuth(); | |
| if(!key) { alert("Please enter Admin Key in top right corner"); return; } | |
| const formData = new FormData(e.target); | |
| const data = Object.fromEntries(formData.entries()); | |
| // Set default date if empty | |
| if(!data.date) data.date = new Date().toISOString().split('T')[0]; | |
| try { | |
| const res = await fetch('/blogs', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-admin-key': key | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| if(res.ok) { | |
| document.getElementById('msg').innerHTML = '<span class="text-green-500">Success: Entry added.</span>'; | |
| e.target.reset(); | |
| loadBlogs(); | |
| } else { | |
| const err = await res.json(); | |
| document.getElementById('msg').innerHTML = `<span class="text-red-500">Error: ${err.detail || 'Failed'}</span>`; | |
| } | |
| } catch(err) { | |
| console.error(err); | |
| } | |
| } | |
| // Init | |
| loadBlogs(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| # ----------------------------- | |
| # RUN SERVER | |
| # ----------------------------- | |
| if __name__ == "__main__": | |
| # In HF Spaces, PORT is usually 7860 | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run(app, host="0.0.0.0", port=port) |