shubhjn commited on
Commit
f8b9830
·
1 Parent(s): 0eed197

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 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
- /* config options here */
 
 
 
 
 
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
- prompt = request.message
 
 
 
 
 
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> |