Spaces:
Running
Running
feat: add server-side Supabase API endpoints
Browse files- Created profile.py route with complete auth API
- POST /api/auth/signin (email/password)
- POST /api/auth/signup (email/password)
- POST /api/auth/google (Google OAuth with idToken)
- POST /api/auth/reset-password
- POST /api/auth/signout
- Added profile management endpoints (GET, POST, PUT /api/profile)
- Added chat history endpoints (GET, POST, PUT, DELETE /api/history)
- Added subscription endpoint (POST /api/subscription/upgrade)
- Registered profile router in server.py
- All operations use server-side Supabase with service key for security
- Dockerfile +11 -1
- admin-dashboard/next.config.ts +6 -1
- src/api/routes/chat.py +7 -1
- src/api/routes/profile.py +271 -0
- src/api/server.py +22 -1
Dockerfile
CHANGED
|
@@ -3,9 +3,11 @@ FROM python:3.10-slim
|
|
| 3 |
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
-
# Install minimal system dependencies
|
| 7 |
RUN apt-get update && apt-get install -y \
|
| 8 |
curl \
|
|
|
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
# Copy requirements
|
|
@@ -20,6 +22,14 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 20 |
# Copy application code
|
| 21 |
COPY . .
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Create data directories
|
| 24 |
RUN mkdir -p data/knowledge data/processed checkpoints uploads/generated logs
|
| 25 |
|
|
|
|
| 3 |
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
+
# Install minimal system dependencies + Node.js for admin dashboard
|
| 7 |
RUN apt-get update && apt-get install -y \
|
| 8 |
curl \
|
| 9 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 10 |
+
&& apt-get install -y nodejs \
|
| 11 |
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
|
| 13 |
# Copy requirements
|
|
|
|
| 22 |
# Copy application code
|
| 23 |
COPY . .
|
| 24 |
|
| 25 |
+
# Build admin dashboard (static export)
|
| 26 |
+
WORKDIR /app/admin-dashboard
|
| 27 |
+
RUN npm install && npm run build
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
# Move static admin dashboard to serve location
|
| 31 |
+
RUN mkdir -p admin-static && cp -r admin-dashboard/out/* admin-static/
|
| 32 |
+
|
| 33 |
# Create data directories
|
| 34 |
RUN mkdir -p data/knowledge data/processed checkpoints uploads/generated logs
|
| 35 |
|
admin-dashboard/next.config.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
};
|
| 6 |
|
| 7 |
export default nextConfig;
|
|
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
+
output: 'export',
|
| 5 |
+
basePath: '/admin',
|
| 6 |
+
trailingSlash: true,
|
| 7 |
+
images: {
|
| 8 |
+
unoptimized: true,
|
| 9 |
+
},
|
| 10 |
};
|
| 11 |
|
| 12 |
export default nextConfig;
|
src/api/routes/chat.py
CHANGED
|
@@ -55,6 +55,7 @@ class ChatRequest(BaseModel):
|
|
| 55 |
max_tokens: int = 256
|
| 56 |
top_k: int = 50
|
| 57 |
top_p: float = 0.9
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
class ChatResponse(BaseModel):
|
|
@@ -83,7 +84,12 @@ async def chat(request: ChatRequest) -> ChatResponse:
|
|
| 83 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 84 |
|
| 85 |
sources = []
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
# Use RAG if enabled
|
| 89 |
if request.use_rag and state.rag is not None:
|
|
|
|
| 55 |
max_tokens: int = 256
|
| 56 |
top_k: int = 50
|
| 57 |
top_p: float = 0.9
|
| 58 |
+
system_prompt: Optional[str] = None # Personality prompt from client
|
| 59 |
|
| 60 |
|
| 61 |
class ChatResponse(BaseModel):
|
|
|
|
| 84 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 85 |
|
| 86 |
sources = []
|
| 87 |
+
|
| 88 |
+
# Build prompt with optional personality system prompt
|
| 89 |
+
if request.system_prompt:
|
| 90 |
+
prompt = f"[System: {request.system_prompt}]\n\nUser: {request.message}\n\nAssistant:"
|
| 91 |
+
else:
|
| 92 |
+
prompt = request.message
|
| 93 |
|
| 94 |
# Use RAG if enabled
|
| 95 |
if request.use_rag and state.rag is not None:
|
src/api/routes/profile.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Nova AI - Profile API Routes
|
| 3 |
+
User profile management with server-side Supabase
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
from typing import Optional
|
| 9 |
+
import os
|
| 10 |
+
from supabase import create_client, Client
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
# Initialize Supabase client
|
| 15 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 16 |
+
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_KEY", "") # Use service key on server
|
| 17 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ============================================================================
|
| 21 |
+
# AUTH ENDPOINTS
|
| 22 |
+
# ============================================================================
|
| 23 |
+
|
| 24 |
+
class SignInRequest(BaseModel):
|
| 25 |
+
email: str
|
| 26 |
+
password: str
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class SignUpRequest(BaseModel):
|
| 30 |
+
email: str
|
| 31 |
+
password: str
|
| 32 |
+
display_name: Optional[str] = None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class GoogleAuthRequest(BaseModel):
|
| 36 |
+
id_token: str
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class PasswordResetRequest(BaseModel):
|
| 40 |
+
email: str
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.post("/auth/signin")
|
| 44 |
+
async def sign_in(data: SignInRequest):
|
| 45 |
+
"""Sign in with email and password"""
|
| 46 |
+
try:
|
| 47 |
+
result = supabase.auth.sign_in_with_password({
|
| 48 |
+
"email": data.email,
|
| 49 |
+
"password": data.password
|
| 50 |
+
})
|
| 51 |
+
return {
|
| 52 |
+
"success": True,
|
| 53 |
+
"session": result.session.model_dump() if result.session else None,
|
| 54 |
+
"user": result.user.model_dump() if result.user else None
|
| 55 |
+
}
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return {"success": False, "error": str(e)}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@router.post("/auth/signup")
|
| 61 |
+
async def sign_up(data: SignUpRequest):
|
| 62 |
+
"""Sign up with email and password"""
|
| 63 |
+
try:
|
| 64 |
+
result = supabase.auth.sign_up({
|
| 65 |
+
"email": data.email,
|
| 66 |
+
"password": data.password,
|
| 67 |
+
"options": {
|
| 68 |
+
"data": {"display_name": data.display_name or data.email.split('@')[0]}
|
| 69 |
+
}
|
| 70 |
+
})
|
| 71 |
+
return {
|
| 72 |
+
"success": True,
|
| 73 |
+
"session": result.session.model_dump() if result.session else None,
|
| 74 |
+
"user": result.user.model_dump() if result.user else None
|
| 75 |
+
}
|
| 76 |
+
except Exception as e:
|
| 77 |
+
return {"success": False, "error": str(e)}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@router.post("/auth/google")
|
| 81 |
+
async def sign_in_with_google(data: GoogleAuthRequest):
|
| 82 |
+
"""Sign in with Google ID token"""
|
| 83 |
+
try:
|
| 84 |
+
result = supabase.auth.sign_in_with_id_token({
|
| 85 |
+
"provider": "google",
|
| 86 |
+
"token": data.id_token
|
| 87 |
+
})
|
| 88 |
+
return {
|
| 89 |
+
"success": True,
|
| 90 |
+
"session": result.session.model_dump() if result.session else None,
|
| 91 |
+
"user": result.user.model_dump() if result.user else None
|
| 92 |
+
}
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return {"success": False, "error": str(e)}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@router.post("/auth/reset-password")
|
| 98 |
+
async def reset_password(data: PasswordResetRequest):
|
| 99 |
+
"""Send password reset email"""
|
| 100 |
+
try:
|
| 101 |
+
supabase.auth.reset_password_for_email(data.email)
|
| 102 |
+
return {"success": True}
|
| 103 |
+
except Exception as e:
|
| 104 |
+
return {"success": False, "error": str(e)}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@router.post("/auth/signout")
|
| 108 |
+
async def sign_out():
|
| 109 |
+
"""Sign out current user"""
|
| 110 |
+
try:
|
| 111 |
+
supabase.auth.sign_out()
|
| 112 |
+
return {"success": True}
|
| 113 |
+
except Exception as e:
|
| 114 |
+
return {"success": False, "error": str(e)}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# ============================================================================
|
| 118 |
+
# PROFILE ENDPOINTS
|
| 119 |
+
# ============================================================================
|
| 120 |
+
async def create_profile(data: ProfileCreate):
|
| 121 |
+
"""Create a new user profile"""
|
| 122 |
+
try:
|
| 123 |
+
profile_data = {
|
| 124 |
+
"id": data.user_id,
|
| 125 |
+
"email": data.email,
|
| 126 |
+
"display_name": data.display_name or data.email.split('@')[0] if data.email else 'User',
|
| 127 |
+
"subscription_tier": "free",
|
| 128 |
+
"tokens_used": 0,
|
| 129 |
+
"tokens_limit": 20,
|
| 130 |
+
"consent_given": False,
|
| 131 |
+
"data_collection_consent": False,
|
| 132 |
+
"is_admin": False,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
result = supabase.table("profiles").insert(profile_data).execute()
|
| 136 |
+
return {"success": True, "profile": result.data[0] if result.data else None}
|
| 137 |
+
except Exception as e:
|
| 138 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
@router.get("/profile/{user_id}")
|
| 142 |
+
async def get_profile(user_id: str):
|
| 143 |
+
"""Get user profile"""
|
| 144 |
+
try:
|
| 145 |
+
result = supabase.table("profiles").select("*").eq("id", user_id).execute()
|
| 146 |
+
if not result.data:
|
| 147 |
+
return {"success": False, "error": "Profile not found"}
|
| 148 |
+
return {"success": True, "profile": result.data[0]}
|
| 149 |
+
except Exception as e:
|
| 150 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@router.put("/profile/{user_id}")
|
| 154 |
+
async def update_profile(user_id: str, updates: ProfileUpdate):
|
| 155 |
+
"""Update user profile"""
|
| 156 |
+
try:
|
| 157 |
+
update_data = {k: v for k, v in updates.dict().items() if v is not None}
|
| 158 |
+
update_data["updated_at"] = "now()"
|
| 159 |
+
|
| 160 |
+
result = supabase.table("profiles").update(update_data).eq("id", user_id).execute()
|
| 161 |
+
return {"success": True}
|
| 162 |
+
except Exception as e:
|
| 163 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@router.post("/profile/{user_id}/consent")
|
| 167 |
+
async def accept_consent(user_id: str, data_collection: bool):
|
| 168 |
+
"""Accept user consent"""
|
| 169 |
+
try:
|
| 170 |
+
update_data = {
|
| 171 |
+
"consent_given": True,
|
| 172 |
+
"consent_given_at": "now()",
|
| 173 |
+
"data_collection_consent": data_collection,
|
| 174 |
+
"terms_accepted_at": "now()",
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
supabase.table("profiles").update(update_data).eq("id", user_id).execute()
|
| 178 |
+
return {"success": True}
|
| 179 |
+
except Exception as e:
|
| 180 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@router.post("/profile/{user_id}/tokens/use")
|
| 184 |
+
async def use_tokens(user_id: str, amount: int):
|
| 185 |
+
"""Use tokens from user's balance"""
|
| 186 |
+
try:
|
| 187 |
+
result = supabase.rpc("use_tokens", {
|
| 188 |
+
"p_user_id": user_id,
|
| 189 |
+
"p_tokens": amount
|
| 190 |
+
}).execute()
|
| 191 |
+
return {"success": result.data, "remaining": 0}
|
| 192 |
+
except Exception as e:
|
| 193 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# History endpoints
|
| 197 |
+
@router.get("/history/{user_id}")
|
| 198 |
+
async def get_chat_histories(user_id: str):
|
| 199 |
+
"""Get all chat histories for a user"""
|
| 200 |
+
try:
|
| 201 |
+
result = supabase.table("chat_history").select("*").eq("user_id", user_id).order("updated_at", desc=True).execute()
|
| 202 |
+
return {"success": True, "histories": result.data}
|
| 203 |
+
except Exception as e:
|
| 204 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@router.post("/history")
|
| 208 |
+
async def create_chat(user_id: str, title: str, messages: list):
|
| 209 |
+
"""Create new chat history"""
|
| 210 |
+
try:
|
| 211 |
+
chat_data = {
|
| 212 |
+
"user_id": user_id,
|
| 213 |
+
"title": title,
|
| 214 |
+
"messages": messages,
|
| 215 |
+
}
|
| 216 |
+
result = supabase.table("chat_history").insert(chat_data).select("id").execute()
|
| 217 |
+
return {"success": True, "id": result.data[0]["id"] if result.data else None}
|
| 218 |
+
except Exception as e:
|
| 219 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@router.put("/history/{chat_id}")
|
| 223 |
+
async def update_chat(chat_id: str, title: Optional[str] = None, messages: Optional[list] = None):
|
| 224 |
+
"""Update chat history"""
|
| 225 |
+
try:
|
| 226 |
+
update_data = {}
|
| 227 |
+
if title:
|
| 228 |
+
update_data["title"] = title
|
| 229 |
+
if messages:
|
| 230 |
+
update_data["messages"] = messages
|
| 231 |
+
update_data["updated_at"] = "now()"
|
| 232 |
+
|
| 233 |
+
supabase.table("chat_history").update(update_data).eq("id", chat_id).execute()
|
| 234 |
+
return {"success": True}
|
| 235 |
+
except Exception as e:
|
| 236 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@router.delete("/history/{chat_id}")
|
| 240 |
+
async def delete_chat(chat_id: str):
|
| 241 |
+
"""Delete chat history"""
|
| 242 |
+
try:
|
| 243 |
+
supabase.table("chat_history").delete().eq("id", chat_id).execute()
|
| 244 |
+
return {"success": True}
|
| 245 |
+
except Exception as e:
|
| 246 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# Subscription endpoints
|
| 250 |
+
@router.post("/subscription/upgrade")
|
| 251 |
+
async def upgrade_to_pro(user_id: str, razorpay_subscription_id: str):
|
| 252 |
+
"""Upgrade user to pro"""
|
| 253 |
+
try:
|
| 254 |
+
# Update profile
|
| 255 |
+
supabase.table("profiles").update({
|
| 256 |
+
"subscription_tier": "pro",
|
| 257 |
+
"tokens_limit": 1000,
|
| 258 |
+
}).eq("id", user_id).execute()
|
| 259 |
+
|
| 260 |
+
# Create subscription record
|
| 261 |
+
import datetime
|
| 262 |
+
supabase.table("subscriptions").insert({
|
| 263 |
+
"user_id": user_id,
|
| 264 |
+
"tier": "pro",
|
| 265 |
+
"razorpay_subscription_id": razorpay_subscription_id,
|
| 266 |
+
"expires_at": (datetime.datetime.now() + datetime.timedelta(days=30)).isoformat(),
|
| 267 |
+
}).execute()
|
| 268 |
+
|
| 269 |
+
return {"success": True}
|
| 270 |
+
except Exception as e:
|
| 271 |
+
raise HTTPException(status_code=500, detail=str(e))
|
src/api/server.py
CHANGED
|
@@ -19,7 +19,7 @@ from fastapi.staticfiles import StaticFiles
|
|
| 19 |
from loguru import logger
|
| 20 |
|
| 21 |
# Import routes
|
| 22 |
-
from .routes import chat, image, files, knowledge, auth, analytics, models
|
| 23 |
|
| 24 |
# Keep-alive settings
|
| 25 |
KEEP_ALIVE_INTERVAL = 300 # 5 minutes
|
|
@@ -226,6 +226,11 @@ uploads_path = Path("uploads")
|
|
| 226 |
uploads_path.mkdir(exist_ok=True)
|
| 227 |
app.mount("/static", StaticFiles(directory=str(uploads_path)), name="static")
|
| 228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
# Include routers
|
| 230 |
app.include_router(chat.router, prefix="/api", tags=["Chat"])
|
| 231 |
app.include_router(image.router, prefix="/api", tags=["Image"])
|
|
@@ -234,6 +239,7 @@ app.include_router(knowledge.router, prefix="/api", tags=["Knowledge"])
|
|
| 234 |
app.include_router(auth.router, prefix="/api", tags=["Auth"])
|
| 235 |
app.include_router(analytics.router, prefix="/api", tags=["Analytics"])
|
| 236 |
app.include_router(models.router, prefix="/api", tags=["Models"])
|
|
|
|
| 237 |
|
| 238 |
|
| 239 |
@app.get("/")
|
|
@@ -355,6 +361,21 @@ async def root():
|
|
| 355 |
</div>
|
| 356 |
</div>
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
<div class="endpoints">
|
| 359 |
API: <a href="/api/health">/api/health</a> |
|
| 360 |
<a href="/api/chat">/api/chat</a> |
|
|
|
|
| 19 |
from loguru import logger
|
| 20 |
|
| 21 |
# Import routes
|
| 22 |
+
from .routes import chat, image, files, knowledge, auth, analytics, models, profile
|
| 23 |
|
| 24 |
# Keep-alive settings
|
| 25 |
KEEP_ALIVE_INTERVAL = 300 # 5 minutes
|
|
|
|
| 226 |
uploads_path.mkdir(exist_ok=True)
|
| 227 |
app.mount("/static", StaticFiles(directory=str(uploads_path)), name="static")
|
| 228 |
|
| 229 |
+
# Admin dashboard static files (built from Next.js)
|
| 230 |
+
admin_static_path = Path("admin-static")
|
| 231 |
+
if admin_static_path.exists():
|
| 232 |
+
app.mount("/admin", StaticFiles(directory=str(admin_static_path), html=True), name="admin")
|
| 233 |
+
|
| 234 |
# Include routers
|
| 235 |
app.include_router(chat.router, prefix="/api", tags=["Chat"])
|
| 236 |
app.include_router(image.router, prefix="/api", tags=["Image"])
|
|
|
|
| 239 |
app.include_router(auth.router, prefix="/api", tags=["Auth"])
|
| 240 |
app.include_router(analytics.router, prefix="/api", tags=["Analytics"])
|
| 241 |
app.include_router(models.router, prefix="/api", tags=["Models"])
|
| 242 |
+
app.include_router(profile.router, prefix="/api", tags=["Profile"])
|
| 243 |
|
| 244 |
|
| 245 |
@app.get("/")
|
|
|
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
|
| 364 |
+
<div class="admin-button" style="margin-top: 30px;">
|
| 365 |
+
<a href="/admin/" style="
|
| 366 |
+
display: inline-block;
|
| 367 |
+
background: linear-gradient(135deg, #7b2ff7, #00d4ff);
|
| 368 |
+
color: white;
|
| 369 |
+
padding: 15px 30px;
|
| 370 |
+
border-radius: 12px;
|
| 371 |
+
text-decoration: none;
|
| 372 |
+
font-weight: 600;
|
| 373 |
+
font-size: 1em;
|
| 374 |
+
box-shadow: 0 4px 15px rgba(123, 47, 247, 0.4);
|
| 375 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 376 |
+
">🔐 Admin Dashboard</a>
|
| 377 |
+
</div>
|
| 378 |
+
|
| 379 |
<div class="endpoints">
|
| 380 |
API: <a href="/api/health">/api/health</a> |
|
| 381 |
<a href="/api/chat">/api/chat</a> |
|