Spaces:
Paused
Paused
Maksymilian Jankowski commited on
Commit ·
b62d4a1
1
Parent(s): abd051a
updates
Browse files- .env +6 -3
- .gitignore +232 -0
- __pycache__/auth.cpython-313.pyc +0 -0
- __pycache__/main.cpython-313.pyc +0 -0
- auth.py +86 -4
- main.py +255 -2
- requirements.txt +64 -1
.env
CHANGED
|
@@ -1,7 +1,10 @@
|
|
| 1 |
# OpenAI API Key
|
| 2 |
-
OPENAI_API_KEY="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"
|
|
|
|
| 3 |
# Google API Key (for Gemini)
|
| 4 |
GOOGLE_API_KEY="AIzaSyCH4rdbuK1cfRpwkhpnpxbdWnrO8RsxAD8"
|
| 5 |
-
MESHY_API_KEY=
|
| 6 |
SUPABASE_URL="https://blhjlpokxsdalllewjbx.supabase.co"
|
| 7 |
-
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJsaGpscG9reHNkYWxsbGV3amJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDgyNjY0MjksImV4cCI6MjA2Mzg0MjQyOX0.TsoeN7Lid7gmlTJ18Ayp31LEt6Xpyg5VbKt4J5EJGNE"
|
|
|
|
|
|
|
|
|
| 1 |
# OpenAI API Key
|
| 2 |
+
#OPENAI_API_KEY="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"
|
| 3 |
+
OPENAI_API_KEY="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"
|
| 4 |
# Google API Key (for Gemini)
|
| 5 |
GOOGLE_API_KEY="AIzaSyCH4rdbuK1cfRpwkhpnpxbdWnrO8RsxAD8"
|
| 6 |
+
MESHY_API_KEY="msy_UnUQpybrhKHfDdgIbpkYw4BEBWZ2WV2wEImI"
|
| 7 |
SUPABASE_URL="https://blhjlpokxsdalllewjbx.supabase.co"
|
| 8 |
+
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJsaGpscG9reHNkYWxsbGV3amJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDgyNjY0MjksImV4cCI6MjA2Mzg0MjQyOX0.TsoeN7Lid7gmlTJ18Ayp31LEt6Xpyg5VbKt4J5EJGNE"
|
| 9 |
+
STRIPE_SECRET_KEY=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
|
| 10 |
+
#STRIPE_WEBHOOK_SECRET=
|
.gitignore
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
pip-wheel-metadata/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
MANIFEST
|
| 29 |
+
|
| 30 |
+
# PyInstaller
|
| 31 |
+
*.manifest
|
| 32 |
+
*.spec
|
| 33 |
+
|
| 34 |
+
# Installer logs
|
| 35 |
+
pip-log.txt
|
| 36 |
+
pip-delete-this-directory.txt
|
| 37 |
+
|
| 38 |
+
# Unit test / coverage reports
|
| 39 |
+
htmlcov/
|
| 40 |
+
.tox/
|
| 41 |
+
.nox/
|
| 42 |
+
.coverage
|
| 43 |
+
.coverage.*
|
| 44 |
+
.cache
|
| 45 |
+
nosetests.xml
|
| 46 |
+
coverage.xml
|
| 47 |
+
*.cover
|
| 48 |
+
*.py,cover
|
| 49 |
+
.hypothesis/
|
| 50 |
+
.pytest_cache/
|
| 51 |
+
|
| 52 |
+
# Translations
|
| 53 |
+
*.mo
|
| 54 |
+
*.pot
|
| 55 |
+
|
| 56 |
+
# Django stuff:
|
| 57 |
+
*.log
|
| 58 |
+
local_settings.py
|
| 59 |
+
db.sqlite3
|
| 60 |
+
db.sqlite3-journal
|
| 61 |
+
|
| 62 |
+
# Flask stuff:
|
| 63 |
+
instance/
|
| 64 |
+
.webassets-cache
|
| 65 |
+
|
| 66 |
+
# Scrapy stuff:
|
| 67 |
+
.scrapy
|
| 68 |
+
|
| 69 |
+
# Sphinx documentation
|
| 70 |
+
docs/_build/
|
| 71 |
+
|
| 72 |
+
# PyBuilder
|
| 73 |
+
target/
|
| 74 |
+
|
| 75 |
+
# Jupyter Notebook
|
| 76 |
+
.ipynb_checkpoints
|
| 77 |
+
|
| 78 |
+
# IPython
|
| 79 |
+
profile_default/
|
| 80 |
+
ipython_config.py
|
| 81 |
+
|
| 82 |
+
# pyenv
|
| 83 |
+
.python-version
|
| 84 |
+
|
| 85 |
+
# pipenv
|
| 86 |
+
Pipfile.lock
|
| 87 |
+
|
| 88 |
+
# PEP 582
|
| 89 |
+
__pypackages__/
|
| 90 |
+
|
| 91 |
+
# Celery stuff
|
| 92 |
+
celerybeat-schedule
|
| 93 |
+
celerybeat.pid
|
| 94 |
+
|
| 95 |
+
# SageMath parsed files
|
| 96 |
+
*.sage.py
|
| 97 |
+
|
| 98 |
+
# Environments
|
| 99 |
+
.env
|
| 100 |
+
.venv
|
| 101 |
+
env/
|
| 102 |
+
venv/
|
| 103 |
+
ENV/
|
| 104 |
+
env.bak/
|
| 105 |
+
venv.bak/
|
| 106 |
+
|
| 107 |
+
# Spyder project settings
|
| 108 |
+
.spyderproject
|
| 109 |
+
.spyproject
|
| 110 |
+
|
| 111 |
+
# Rope project settings
|
| 112 |
+
.ropeproject
|
| 113 |
+
|
| 114 |
+
# mkdocs documentation
|
| 115 |
+
/site
|
| 116 |
+
|
| 117 |
+
# mypy
|
| 118 |
+
.mypy_cache/
|
| 119 |
+
.dmypy.json
|
| 120 |
+
dmypy.json
|
| 121 |
+
|
| 122 |
+
# Pyre type checker
|
| 123 |
+
.pyre/
|
| 124 |
+
|
| 125 |
+
# Node.js dependencies
|
| 126 |
+
node_modules/
|
| 127 |
+
npm-debug.log*
|
| 128 |
+
yarn-debug.log*
|
| 129 |
+
yarn-error.log*
|
| 130 |
+
|
| 131 |
+
# Runtime data
|
| 132 |
+
pids
|
| 133 |
+
*.pid
|
| 134 |
+
*.seed
|
| 135 |
+
*.pid.lock
|
| 136 |
+
|
| 137 |
+
# Coverage directory used by tools like istanbul
|
| 138 |
+
coverage/
|
| 139 |
+
|
| 140 |
+
# nyc test coverage
|
| 141 |
+
.nyc_output
|
| 142 |
+
|
| 143 |
+
# Grunt intermediate storage
|
| 144 |
+
.grunt
|
| 145 |
+
|
| 146 |
+
# Bower dependency directory
|
| 147 |
+
bower_components
|
| 148 |
+
|
| 149 |
+
# node-waf configuration
|
| 150 |
+
.lock-wscript
|
| 151 |
+
|
| 152 |
+
# Compiled binary addons
|
| 153 |
+
build/Release
|
| 154 |
+
|
| 155 |
+
# Dependency directories
|
| 156 |
+
jspm_packages/
|
| 157 |
+
|
| 158 |
+
# TypeScript v1 declaration files
|
| 159 |
+
typings/
|
| 160 |
+
|
| 161 |
+
# Optional npm cache directory
|
| 162 |
+
.npm
|
| 163 |
+
|
| 164 |
+
# Optional eslint cache
|
| 165 |
+
.eslintcache
|
| 166 |
+
|
| 167 |
+
# Optional REPL history
|
| 168 |
+
.node_repl_history
|
| 169 |
+
|
| 170 |
+
# Output of 'npm pack'
|
| 171 |
+
*.tgz
|
| 172 |
+
|
| 173 |
+
# Yarn Integrity file
|
| 174 |
+
.yarn-integrity
|
| 175 |
+
|
| 176 |
+
# parcel-bundler cache
|
| 177 |
+
.cache
|
| 178 |
+
.parcel-cache
|
| 179 |
+
|
| 180 |
+
# next.js build output
|
| 181 |
+
.next
|
| 182 |
+
|
| 183 |
+
# nuxt.js build output
|
| 184 |
+
.nuxt
|
| 185 |
+
|
| 186 |
+
# vuepress build output
|
| 187 |
+
.vuepress/dist
|
| 188 |
+
|
| 189 |
+
# Serverless directories
|
| 190 |
+
.serverless/
|
| 191 |
+
|
| 192 |
+
# FuseBox cache
|
| 193 |
+
.fusebox/
|
| 194 |
+
|
| 195 |
+
# DynamoDB Local files
|
| 196 |
+
.dynamodb/
|
| 197 |
+
|
| 198 |
+
# TernJS port file
|
| 199 |
+
.tern-port
|
| 200 |
+
|
| 201 |
+
# IDE and Editor files
|
| 202 |
+
.vscode/
|
| 203 |
+
.idea/
|
| 204 |
+
*.swp
|
| 205 |
+
*.swo
|
| 206 |
+
*~
|
| 207 |
+
|
| 208 |
+
# OS generated files
|
| 209 |
+
.DS_Store
|
| 210 |
+
.DS_Store?
|
| 211 |
+
._*
|
| 212 |
+
.Spotlight-V100
|
| 213 |
+
.Trashes
|
| 214 |
+
ehthumbs.db
|
| 215 |
+
Thumbs.db
|
| 216 |
+
|
| 217 |
+
# Logs
|
| 218 |
+
logs
|
| 219 |
+
*.log
|
| 220 |
+
|
| 221 |
+
# Database files
|
| 222 |
+
*.db
|
| 223 |
+
*.sqlite
|
| 224 |
+
*.sqlite3
|
| 225 |
+
|
| 226 |
+
# Temporary files
|
| 227 |
+
*.tmp
|
| 228 |
+
*.temp
|
| 229 |
+
|
| 230 |
+
# Backup files
|
| 231 |
+
*.bak
|
| 232 |
+
*.backup
|
__pycache__/auth.cpython-313.pyc
DELETED
|
Binary file (3.95 kB)
|
|
|
__pycache__/main.cpython-313.pyc
DELETED
|
Binary file (21.3 kB)
|
|
|
auth.py
CHANGED
|
@@ -5,6 +5,8 @@ from typing import Optional
|
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from pydantic import BaseModel
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# Load environment variables
|
| 10 |
load_dotenv()
|
|
@@ -37,11 +39,36 @@ class TokenData(BaseModel):
|
|
| 37 |
|
| 38 |
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
|
| 39 |
"""
|
| 40 |
-
Validate JWT token and return the current user
|
|
|
|
| 41 |
"""
|
|
|
|
|
|
|
|
|
|
| 42 |
try:
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if not user or not user.user:
|
| 46 |
raise HTTPException(
|
| 47 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
@@ -49,12 +76,14 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
|
|
| 49 |
headers={"WWW-Authenticate": "Bearer"},
|
| 50 |
)
|
| 51 |
# Use the user info from Supabase Auth directly
|
|
|
|
| 52 |
return User(
|
| 53 |
id=user.user.id,
|
| 54 |
email=user.user.email,
|
| 55 |
role="user" # Default role
|
| 56 |
)
|
| 57 |
except Exception as e:
|
|
|
|
| 58 |
raise HTTPException(
|
| 59 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 60 |
detail=f"Authentication failed: {str(e)}",
|
|
@@ -82,4 +111,57 @@ def require_role(required_role: str):
|
|
| 82 |
detail="Not enough permissions"
|
| 83 |
)
|
| 84 |
return current_user
|
| 85 |
-
return role_checker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from pydantic import BaseModel
|
| 8 |
+
import jwt
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
|
| 11 |
# Load environment variables
|
| 12 |
load_dotenv()
|
|
|
|
| 39 |
|
| 40 |
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
|
| 41 |
"""
|
| 42 |
+
Validate JWT token and return the current user.
|
| 43 |
+
Supports both backend JWT tokens and Supabase tokens.
|
| 44 |
"""
|
| 45 |
+
token = credentials.credentials
|
| 46 |
+
|
| 47 |
+
# First, try to decode as a backend JWT token
|
| 48 |
try:
|
| 49 |
+
secret_key = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-this")
|
| 50 |
+
payload = jwt.decode(token, secret_key, algorithms=["HS256"])
|
| 51 |
+
user_id = payload.get("sub")
|
| 52 |
+
email = payload.get("email")
|
| 53 |
+
|
| 54 |
+
if user_id and email:
|
| 55 |
+
print(f"✅ Successfully authenticated with backend JWT token for user: {email}")
|
| 56 |
+
return User(
|
| 57 |
+
id=user_id,
|
| 58 |
+
email=email,
|
| 59 |
+
role="user"
|
| 60 |
+
)
|
| 61 |
+
except jwt.InvalidTokenError:
|
| 62 |
+
# If JWT decoding fails, try Supabase token verification
|
| 63 |
+
print("🔄 Backend JWT decode failed, trying Supabase token...")
|
| 64 |
+
pass
|
| 65 |
+
except Exception as e:
|
| 66 |
+
# Log other JWT errors but continue to Supabase fallback
|
| 67 |
+
print(f"⚠️ JWT decode error: {str(e)}")
|
| 68 |
+
|
| 69 |
+
# Fallback to Supabase token verification
|
| 70 |
+
try:
|
| 71 |
+
user = supabase.auth.get_user(token)
|
| 72 |
if not user or not user.user:
|
| 73 |
raise HTTPException(
|
| 74 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
| 76 |
headers={"WWW-Authenticate": "Bearer"},
|
| 77 |
)
|
| 78 |
# Use the user info from Supabase Auth directly
|
| 79 |
+
print(f"✅ Successfully authenticated with Supabase token for user: {user.user.email}")
|
| 80 |
return User(
|
| 81 |
id=user.user.id,
|
| 82 |
email=user.user.email,
|
| 83 |
role="user" # Default role
|
| 84 |
)
|
| 85 |
except Exception as e:
|
| 86 |
+
print(f"❌ Both authentication methods failed: {str(e)}")
|
| 87 |
raise HTTPException(
|
| 88 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 89 |
detail=f"Authentication failed: {str(e)}",
|
|
|
|
| 111 |
detail="Not enough permissions"
|
| 112 |
)
|
| 113 |
return current_user
|
| 114 |
+
return role_checker
|
| 115 |
+
|
| 116 |
+
async def get_supabase_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
| 117 |
+
"""
|
| 118 |
+
Extract Supabase token from Authorization header
|
| 119 |
+
"""
|
| 120 |
+
return credentials.credentials
|
| 121 |
+
|
| 122 |
+
def verify_supabase_token(token: str) -> dict:
|
| 123 |
+
"""
|
| 124 |
+
Verify Supabase token and return user data
|
| 125 |
+
"""
|
| 126 |
+
try:
|
| 127 |
+
user_response = supabase.auth.get_user(token)
|
| 128 |
+
if not user_response or not user_response.user:
|
| 129 |
+
raise HTTPException(
|
| 130 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 131 |
+
detail="Invalid Supabase token"
|
| 132 |
+
)
|
| 133 |
+
# Return user data as dictionary for easier handling
|
| 134 |
+
return {
|
| 135 |
+
"id": user_response.user.id,
|
| 136 |
+
"email": user_response.user.email,
|
| 137 |
+
"user_metadata": user_response.user.user_metadata or {},
|
| 138 |
+
"app_metadata": user_response.user.app_metadata or {},
|
| 139 |
+
"created_at": user_response.user.created_at,
|
| 140 |
+
"updated_at": user_response.user.updated_at
|
| 141 |
+
}
|
| 142 |
+
except Exception as e:
|
| 143 |
+
raise HTTPException(
|
| 144 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 145 |
+
detail=f"Token verification failed: {str(e)}"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
def create_backend_jwt_token(user_data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
| 149 |
+
"""
|
| 150 |
+
Create a backend JWT token for the user
|
| 151 |
+
"""
|
| 152 |
+
if expires_delta:
|
| 153 |
+
expire = datetime.utcnow() + expires_delta
|
| 154 |
+
else:
|
| 155 |
+
expire = datetime.utcnow() + timedelta(days=7) # Token expires in 7 days
|
| 156 |
+
|
| 157 |
+
to_encode = {
|
| 158 |
+
"sub": user_data["id"],
|
| 159 |
+
"email": user_data["email"],
|
| 160 |
+
"exp": expire,
|
| 161 |
+
"iat": datetime.utcnow()
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
# Use a secret key for JWT signing (you should set this in your .env file)
|
| 165 |
+
secret_key = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-this")
|
| 166 |
+
encoded_jwt = jwt.encode(to_encode, secret_key, algorithm="HS256")
|
| 167 |
+
return encoded_jwt
|
main.py
CHANGED
|
@@ -6,15 +6,19 @@ from contextlib import asynccontextmanager
|
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
from pydantic import BaseModel
|
| 9 |
-
from auth import get_current_active_user, User, supabase
|
| 10 |
import logging
|
| 11 |
import random
|
| 12 |
from io import BytesIO
|
| 13 |
from urllib.parse import unquote
|
| 14 |
from fastapi.responses import StreamingResponse
|
|
|
|
|
|
|
| 15 |
|
| 16 |
load_dotenv()
|
| 17 |
|
|
|
|
|
|
|
| 18 |
class Settings:
|
| 19 |
def __init__(self):
|
| 20 |
self.mesh_api_key = "msy_slVFWXjDQvc2BR8ltSacK79YshK9KCXkaV3F"
|
|
@@ -23,6 +27,12 @@ class Settings:
|
|
| 23 |
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
| 24 |
if not self.openai_api_key:
|
| 25 |
raise RuntimeError("OPENAI_API_KEY environment variable not set")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
settings = Settings()
|
| 28 |
|
|
@@ -63,6 +73,12 @@ class SignInRequest(BaseModel):
|
|
| 63 |
email: str
|
| 64 |
password: str
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
class CompleteProfileRequest(BaseModel):
|
| 67 |
address: str
|
| 68 |
fullname: str
|
|
@@ -85,6 +101,13 @@ class PlaceOrderRequest(BaseModel):
|
|
| 85 |
payment_status: str = "pending"
|
| 86 |
transaction_id: str = None
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
# Auth endpoints
|
| 89 |
@app.post("/auth/signup", tags=["Authentication"])
|
| 90 |
async def signup(request: SignUpRequest):
|
|
@@ -111,7 +134,7 @@ async def signup(request: SignUpRequest):
|
|
| 111 |
# Initialize credits
|
| 112 |
supabase.from_("User_Credit_Account").insert({
|
| 113 |
"user_id": response.user.id,
|
| 114 |
-
"num_of_available_gens":
|
| 115 |
}).execute()
|
| 116 |
|
| 117 |
print(f"User created successfully: {response.user}")
|
|
@@ -144,6 +167,69 @@ async def signin(request: SignInRequest):
|
|
| 144 |
logging.error(f"SignIn error: {str(e)}")
|
| 145 |
raise HTTPException(status_code=401, detail=f"Invalid credentials: {str(e)}")
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
# TODO: The signup also creates a profile, so this endpoint should either be removed or /signup updated.
|
| 148 |
@app.post("/auth/complete-profile", tags=["Authentication"])
|
| 149 |
async def complete_profile(request: CompleteProfileRequest, current_user: User = Depends(get_current_active_user)):
|
|
@@ -204,6 +290,28 @@ async def req_img_to_3d(
|
|
| 204 |
base64.b64encode(content).decode())
|
| 205 |
payload = {"image_url": data_uri}
|
| 206 |
headers = {"Authorization": f"Bearer {settings.mesh_api_key}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
resp = await app.state.client.post(
|
| 208 |
"https://api.meshy.ai/openapi/v1/image-to-3d",
|
| 209 |
json=payload,
|
|
@@ -308,6 +416,8 @@ async def req_text_to_3d(
|
|
| 308 |
"""
|
| 309 |
# Credit check and decrement
|
| 310 |
await check_and_decrement_credits(current_user.id)
|
|
|
|
|
|
|
| 311 |
|
| 312 |
# Save initial record to database immediately after credit check
|
| 313 |
initial_record_id = None
|
|
@@ -378,6 +488,26 @@ async def req_text_to_3d(
|
|
| 378 |
# Create Meshy Text-to-3D task
|
| 379 |
meshy_payload = {"mode": "preview", "prompt": reframed}
|
| 380 |
print(f"Sending to Meshy API: {meshy_payload}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
meshy_resp = await app.state.client.post(
|
| 383 |
"https://api.meshy.ai/openapi/v2/text-to-3d",
|
|
@@ -536,6 +666,110 @@ async def purchase_credits(request: PurchaseCreditsRequest, current_user: User =
|
|
| 536 |
supabase.from_("User_Credit_Account").insert({"user_id": current_user.id, "num_of_available_gens": new_credits}).execute()
|
| 537 |
return {"message": "Credits purchased successfully.", "total_credits": new_credits}
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
# User dashboard endpoints
|
| 540 |
@app.get("/user/profile", tags=["User"])
|
| 541 |
async def get_profile(current_user: User = Depends(get_current_active_user)):
|
|
@@ -567,6 +801,25 @@ async def get_model_by_task_id(task_id: str, current_user: User = Depends(get_cu
|
|
| 567 |
raise HTTPException(status_code=404, detail="Model not found")
|
| 568 |
return model.data
|
| 569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
@app.post("/user/models/{task_id}/refresh", tags=["User"])
|
| 571 |
async def refresh_model_status(
|
| 572 |
task_id: str,
|
|
|
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
from pydantic import BaseModel
|
| 9 |
+
from auth import get_current_active_user, User, supabase, get_supabase_token, verify_supabase_token, create_backend_jwt_token
|
| 10 |
import logging
|
| 11 |
import random
|
| 12 |
from io import BytesIO
|
| 13 |
from urllib.parse import unquote
|
| 14 |
from fastapi.responses import StreamingResponse
|
| 15 |
+
import stripe
|
| 16 |
+
from typing import Optional
|
| 17 |
|
| 18 |
load_dotenv()
|
| 19 |
|
| 20 |
+
dev_mode = False
|
| 21 |
+
|
| 22 |
class Settings:
|
| 23 |
def __init__(self):
|
| 24 |
self.mesh_api_key = "msy_slVFWXjDQvc2BR8ltSacK79YshK9KCXkaV3F"
|
|
|
|
| 27 |
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
| 28 |
if not self.openai_api_key:
|
| 29 |
raise RuntimeError("OPENAI_API_KEY environment variable not set")
|
| 30 |
+
self.stripe_secret_key = os.getenv("STRIPE_SECRET_KEY")
|
| 31 |
+
if not self.stripe_secret_key:
|
| 32 |
+
raise RuntimeError("STRIPE_SECRET_KEY environment variable not set")
|
| 33 |
+
|
| 34 |
+
# Initialize Stripe
|
| 35 |
+
stripe.api_key = self.stripe_secret_key
|
| 36 |
|
| 37 |
settings = Settings()
|
| 38 |
|
|
|
|
| 73 |
email: str
|
| 74 |
password: str
|
| 75 |
|
| 76 |
+
class SyncUserRequest(BaseModel):
|
| 77 |
+
supabase_user_id: str
|
| 78 |
+
email: str
|
| 79 |
+
full_name: Optional[str] = None
|
| 80 |
+
avatar_url: Optional[str] = None
|
| 81 |
+
|
| 82 |
class CompleteProfileRequest(BaseModel):
|
| 83 |
address: str
|
| 84 |
fullname: str
|
|
|
|
| 101 |
payment_status: str = "pending"
|
| 102 |
transaction_id: str = None
|
| 103 |
|
| 104 |
+
class CreatePaymentIntentRequest(BaseModel):
|
| 105 |
+
plan: str
|
| 106 |
+
|
| 107 |
+
class ConfirmPaymentRequest(BaseModel):
|
| 108 |
+
payment_intent_id: str
|
| 109 |
+
plan: str
|
| 110 |
+
|
| 111 |
# Auth endpoints
|
| 112 |
@app.post("/auth/signup", tags=["Authentication"])
|
| 113 |
async def signup(request: SignUpRequest):
|
|
|
|
| 134 |
# Initialize credits
|
| 135 |
supabase.from_("User_Credit_Account").insert({
|
| 136 |
"user_id": response.user.id,
|
| 137 |
+
"num_of_available_gens": 0
|
| 138 |
}).execute()
|
| 139 |
|
| 140 |
print(f"User created successfully: {response.user}")
|
|
|
|
| 167 |
logging.error(f"SignIn error: {str(e)}")
|
| 168 |
raise HTTPException(status_code=401, detail=f"Invalid credentials: {str(e)}")
|
| 169 |
|
| 170 |
+
@app.post("/auth/sync-supabase-user", tags=["Authentication"])
|
| 171 |
+
async def sync_supabase_user(request: SyncUserRequest, supabase_token: str = Depends(get_supabase_token)):
|
| 172 |
+
"""
|
| 173 |
+
Sync a Supabase OAuth user (e.g., Google OAuth) with the backend database and return a backend JWT token.
|
| 174 |
+
"""
|
| 175 |
+
try:
|
| 176 |
+
# Verify the Supabase token
|
| 177 |
+
supabase_user = verify_supabase_token(supabase_token)
|
| 178 |
+
|
| 179 |
+
# Check if user already exists in our database
|
| 180 |
+
existing_user = supabase.from_("User").select("*").eq("user_id", supabase_user["id"]).execute()
|
| 181 |
+
|
| 182 |
+
user_data = {
|
| 183 |
+
"user_id": supabase_user["id"],
|
| 184 |
+
"email": supabase_user["email"],
|
| 185 |
+
"address": None,
|
| 186 |
+
"fullname": request.full_name,
|
| 187 |
+
"phone_number": None
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
if existing_user.data:
|
| 191 |
+
# Update existing user with any new information
|
| 192 |
+
if request.full_name:
|
| 193 |
+
supabase.from_("User").update({
|
| 194 |
+
"fullname": request.full_name
|
| 195 |
+
}).eq("user_id", supabase_user["id"]).execute()
|
| 196 |
+
else:
|
| 197 |
+
# Create new user profile
|
| 198 |
+
supabase.from_("User").insert(user_data).execute()
|
| 199 |
+
|
| 200 |
+
# Initialize credits for new user
|
| 201 |
+
supabase.from_("User_Credit_Account").insert({
|
| 202 |
+
"user_id": supabase_user["id"],
|
| 203 |
+
"num_of_available_gens": 3 # Give new users 3 free credits
|
| 204 |
+
}).execute()
|
| 205 |
+
|
| 206 |
+
# Generate backend JWT token
|
| 207 |
+
backend_token = create_backend_jwt_token({
|
| 208 |
+
"id": supabase_user["id"],
|
| 209 |
+
"email": supabase_user["email"]
|
| 210 |
+
})
|
| 211 |
+
|
| 212 |
+
# Get updated user profile
|
| 213 |
+
user_profile = supabase.from_("User").select("*").eq("user_id", supabase_user["id"]).single().execute()
|
| 214 |
+
|
| 215 |
+
return {
|
| 216 |
+
"access_token": backend_token,
|
| 217 |
+
"token_type": "bearer",
|
| 218 |
+
"user": {
|
| 219 |
+
"id": supabase_user["id"],
|
| 220 |
+
"email": supabase_user["email"],
|
| 221 |
+
"profile": user_profile.data if user_profile.data else user_data
|
| 222 |
+
},
|
| 223 |
+
"message": "User synced successfully"
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
except HTTPException:
|
| 227 |
+
# Re-raise HTTP exceptions as-is
|
| 228 |
+
raise
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logging.error(f"Sync user error: {str(e)}")
|
| 231 |
+
raise HTTPException(status_code=400, detail=f"Failed to sync user: {str(e)}")
|
| 232 |
+
|
| 233 |
# TODO: The signup also creates a profile, so this endpoint should either be removed or /signup updated.
|
| 234 |
@app.post("/auth/complete-profile", tags=["Authentication"])
|
| 235 |
async def complete_profile(request: CompleteProfileRequest, current_user: User = Depends(get_current_active_user)):
|
|
|
|
| 290 |
base64.b64encode(content).decode())
|
| 291 |
payload = {"image_url": data_uri}
|
| 292 |
headers = {"Authorization": f"Bearer {settings.mesh_api_key}"}
|
| 293 |
+
|
| 294 |
+
if dev_mode:
|
| 295 |
+
try:
|
| 296 |
+
supabase.from_("Generated_Models").insert({
|
| 297 |
+
"user_id": current_user.id,
|
| 298 |
+
"meshy_api_job_id": "0197458e-d2f6-7ff0-a211-b660ad057140",
|
| 299 |
+
"model_name": f"Image to 3D - {image.filename}",
|
| 300 |
+
"prompts_and_models_config": {
|
| 301 |
+
"generation_type": "image_to_3d",
|
| 302 |
+
"source_filename": image.filename,
|
| 303 |
+
"content_type": image.content_type,
|
| 304 |
+
"status": "processing",
|
| 305 |
+
"meshy_response": "dev_mode"
|
| 306 |
+
}
|
| 307 |
+
}).execute()
|
| 308 |
+
except Exception as e:
|
| 309 |
+
logging.error(f"Failed to save model to database: {str(e)}")
|
| 310 |
+
# Don't fail the request if database save fails
|
| 311 |
+
return
|
| 312 |
+
|
| 313 |
+
return {"id": "0197458e-d2f6-7ff0-a211-b660ad057140", "status": "processing", "meshy_response": "dev_mode"}
|
| 314 |
+
|
| 315 |
resp = await app.state.client.post(
|
| 316 |
"https://api.meshy.ai/openapi/v1/image-to-3d",
|
| 317 |
json=payload,
|
|
|
|
| 416 |
"""
|
| 417 |
# Credit check and decrement
|
| 418 |
await check_and_decrement_credits(current_user.id)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
|
| 422 |
# Save initial record to database immediately after credit check
|
| 423 |
initial_record_id = None
|
|
|
|
| 488 |
# Create Meshy Text-to-3D task
|
| 489 |
meshy_payload = {"mode": "preview", "prompt": reframed}
|
| 490 |
print(f"Sending to Meshy API: {meshy_payload}")
|
| 491 |
+
|
| 492 |
+
if dev_mode:
|
| 493 |
+
try:
|
| 494 |
+
supabase.from_("Generated_Models").update({
|
| 495 |
+
"meshy_api_job_id": "0197458e-d2f6-7ff0-a211-b660ad057140",
|
| 496 |
+
"prompts_and_models_config": {
|
| 497 |
+
"generation_type": "text_to_3d",
|
| 498 |
+
"original_prompt": prompt.text,
|
| 499 |
+
"reframed_prompt": reframed,
|
| 500 |
+
"status": "processing",
|
| 501 |
+
"stage": "generating",
|
| 502 |
+
"meshy_response": "dev_mode"
|
| 503 |
+
}
|
| 504 |
+
}).eq("generated_model_id", initial_record_id).execute()
|
| 505 |
+
except Exception as e:
|
| 506 |
+
print(f"Failed to update database with final response: {str(e)}")
|
| 507 |
+
# Don't fail the request if database save fails
|
| 508 |
+
|
| 509 |
+
return {"id": initial_record_id, "meshy_task_id": "0197458e-d2f6-7ff0-a211-b660ad057140", "status": "processing", "original_prompt": prompt.text, "reframed_prompt": reframed, "meshy_response": "dev_mode"}
|
| 510 |
+
|
| 511 |
|
| 512 |
meshy_resp = await app.state.client.post(
|
| 513 |
"https://api.meshy.ai/openapi/v2/text-to-3d",
|
|
|
|
| 666 |
supabase.from_("User_Credit_Account").insert({"user_id": current_user.id, "num_of_available_gens": new_credits}).execute()
|
| 667 |
return {"message": "Credits purchased successfully.", "total_credits": new_credits}
|
| 668 |
|
| 669 |
+
# Stripe Payment endpoints
|
| 670 |
+
@app.post("/payment/create-payment-intent", tags=["Payment"])
|
| 671 |
+
async def create_payment_intent(
|
| 672 |
+
request: CreatePaymentIntentRequest,
|
| 673 |
+
current_user: User = Depends(get_current_active_user)
|
| 674 |
+
):
|
| 675 |
+
"""
|
| 676 |
+
Create a Stripe payment intent for purchasing credits.
|
| 677 |
+
"""
|
| 678 |
+
try:
|
| 679 |
+
# Define plan pricing (only one plan for now)
|
| 680 |
+
plan_pricing = {
|
| 681 |
+
"credits_15": {"credits": 15, "amount": 300, "currency": "gbp"} # £3.00 in pence
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
if request.plan not in plan_pricing:
|
| 685 |
+
raise HTTPException(status_code=400, detail="Invalid plan selected")
|
| 686 |
+
|
| 687 |
+
plan = plan_pricing[request.plan]
|
| 688 |
+
|
| 689 |
+
# Create payment intent
|
| 690 |
+
intent = stripe.PaymentIntent.create(
|
| 691 |
+
amount=plan["amount"],
|
| 692 |
+
currency=plan["currency"],
|
| 693 |
+
metadata={
|
| 694 |
+
"user_id": current_user.id,
|
| 695 |
+
"plan": request.plan,
|
| 696 |
+
"credits": plan["credits"]
|
| 697 |
+
}
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
return {
|
| 701 |
+
"client_secret": intent.client_secret,
|
| 702 |
+
"amount": plan["amount"],
|
| 703 |
+
"currency": plan["currency"],
|
| 704 |
+
"credits": plan["credits"]
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
except stripe.error.StripeError as e:
|
| 708 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 709 |
+
except Exception as e:
|
| 710 |
+
logging.error(f"Failed to create payment intent: {str(e)}")
|
| 711 |
+
raise HTTPException(status_code=500, detail="Failed to create payment intent")
|
| 712 |
+
|
| 713 |
+
@app.post("/payment/confirm-payment", tags=["Payment"])
|
| 714 |
+
async def confirm_payment(
|
| 715 |
+
request: ConfirmPaymentRequest,
|
| 716 |
+
current_user: User = Depends(get_current_active_user)
|
| 717 |
+
):
|
| 718 |
+
"""
|
| 719 |
+
Confirm payment and add credits to user account.
|
| 720 |
+
"""
|
| 721 |
+
try:
|
| 722 |
+
# Retrieve the payment intent to verify it succeeded
|
| 723 |
+
intent = stripe.PaymentIntent.retrieve(request.payment_intent_id)
|
| 724 |
+
|
| 725 |
+
if intent.status != "succeeded":
|
| 726 |
+
raise HTTPException(status_code=400, detail="Payment has not succeeded")
|
| 727 |
+
|
| 728 |
+
# Verify the payment belongs to the current user
|
| 729 |
+
if intent.metadata.get("user_id") != current_user.id:
|
| 730 |
+
raise HTTPException(status_code=403, detail="Payment does not belong to current user")
|
| 731 |
+
|
| 732 |
+
# Check if this payment has already been processed
|
| 733 |
+
existing_record = supabase.from_("Credit_Order_History").select("*").eq("transaction_id", request.payment_intent_id).execute()
|
| 734 |
+
if existing_record.data:
|
| 735 |
+
return {"message": "Payment already processed", "total_credits": None}
|
| 736 |
+
|
| 737 |
+
# Get plan details from metadata
|
| 738 |
+
credits_to_add = int(intent.metadata.get("credits", 0))
|
| 739 |
+
amount_paid = intent.amount / 100 # Convert from pence to pounds
|
| 740 |
+
|
| 741 |
+
# Record the purchase in history
|
| 742 |
+
supabase.from_("Credit_Order_History").insert({
|
| 743 |
+
"user_id": current_user.id,
|
| 744 |
+
"price": amount_paid,
|
| 745 |
+
"number_of_generations": credits_to_add,
|
| 746 |
+
"order_date": "now()",
|
| 747 |
+
"payment_status": "paid",
|
| 748 |
+
"transaction_id": request.payment_intent_id
|
| 749 |
+
}).execute()
|
| 750 |
+
|
| 751 |
+
# Update user credits
|
| 752 |
+
credit = supabase.from_("User_Credit_Account").select("num_of_available_gens").eq("user_id", current_user.id).single().execute()
|
| 753 |
+
current_credits = credit.data["num_of_available_gens"] if credit.data else 0
|
| 754 |
+
new_credits = current_credits + credits_to_add
|
| 755 |
+
|
| 756 |
+
if credit.data:
|
| 757 |
+
supabase.from_("User_Credit_Account").update({"num_of_available_gens": new_credits}).eq("user_id", current_user.id).execute()
|
| 758 |
+
else:
|
| 759 |
+
supabase.from_("User_Credit_Account").insert({"user_id": current_user.id, "num_of_available_gens": new_credits}).execute()
|
| 760 |
+
|
| 761 |
+
return {
|
| 762 |
+
"message": "Credits added successfully",
|
| 763 |
+
"credits_added": credits_to_add,
|
| 764 |
+
"total_credits": new_credits
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
except stripe.error.StripeError as e:
|
| 768 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 769 |
+
except Exception as e:
|
| 770 |
+
logging.error(f"Failed to confirm payment: {str(e)}")
|
| 771 |
+
raise HTTPException(status_code=500, detail="Failed to confirm payment")
|
| 772 |
+
|
| 773 |
# User dashboard endpoints
|
| 774 |
@app.get("/user/profile", tags=["User"])
|
| 775 |
async def get_profile(current_user: User = Depends(get_current_active_user)):
|
|
|
|
| 801 |
raise HTTPException(status_code=404, detail="Model not found")
|
| 802 |
return model.data
|
| 803 |
|
| 804 |
+
@app.delete("/user/models/{generated_model_id}", tags=["User"])
|
| 805 |
+
async def delete_model(
|
| 806 |
+
generated_model_id: str,
|
| 807 |
+
current_user: User = Depends(get_current_active_user)
|
| 808 |
+
):
|
| 809 |
+
"""
|
| 810 |
+
Delete a generated model for the current user.
|
| 811 |
+
"""
|
| 812 |
+
try:
|
| 813 |
+
delete_result = supabase.from_("Generated_Models").delete().eq("generated_model_id", generated_model_id).eq("user_id", current_user.id).execute()
|
| 814 |
+
|
| 815 |
+
if not delete_result.data:
|
| 816 |
+
raise HTTPException(status_code=404, detail="Model not found or you do not have permission to delete it.")
|
| 817 |
+
|
| 818 |
+
return {"message": "Model deleted successfully."}
|
| 819 |
+
except Exception as e:
|
| 820 |
+
logging.error(f"Failed to delete model {generated_model_id}: {str(e)}")
|
| 821 |
+
raise HTTPException(status_code=500, detail="Failed to delete model.")
|
| 822 |
+
|
| 823 |
@app.post("/user/models/{task_id}/refresh", tags=["User"])
|
| 824 |
async def refresh_model_status(
|
| 825 |
task_id: str,
|
requirements.txt
CHANGED
|
@@ -7,4 +7,67 @@ python-dotenv>=0.21.0
|
|
| 7 |
openai
|
| 8 |
supabase>=2.0.0
|
| 9 |
python-jose[cryptography]>=3.3.0
|
| 10 |
-
passlib[bcrypt]>=1.7.4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
openai
|
| 8 |
supabase>=2.0.0
|
| 9 |
python-jose[cryptography]>=3.3.0
|
| 10 |
+
passlib[bcrypt]>=1.7.4
|
| 11 |
+
stripe>=5.0.0annotated-types==0.7.0
|
| 12 |
+
anyio==4.9.0
|
| 13 |
+
fastapi==0.115.12
|
| 14 |
+
idna==3.10
|
| 15 |
+
pydantic==2.11.5
|
| 16 |
+
pydantic_core==2.33.2
|
| 17 |
+
sniffio==1.3.1
|
| 18 |
+
starlette==0.46.2
|
| 19 |
+
typing-inspection==0.4.1
|
| 20 |
+
typing_extensions==4.14.0
|
| 21 |
+
aiohappyeyeballs==2.6.1
|
| 22 |
+
aiohttp==3.12.11
|
| 23 |
+
aiosignal==1.3.2
|
| 24 |
+
annotated-types==0.7.0
|
| 25 |
+
anyio==4.9.0
|
| 26 |
+
attrs==25.3.0
|
| 27 |
+
certifi==2025.4.26
|
| 28 |
+
charset-normalizer==3.4.2
|
| 29 |
+
click==8.2.1
|
| 30 |
+
colorama==0.4.6
|
| 31 |
+
deprecation==2.1.0
|
| 32 |
+
dotenv==0.9.9
|
| 33 |
+
fastapi==0.115.12
|
| 34 |
+
frozenlist==1.6.2
|
| 35 |
+
gotrue==2.12.0
|
| 36 |
+
h11==0.16.0
|
| 37 |
+
h2==4.2.0
|
| 38 |
+
hpack==4.1.0
|
| 39 |
+
httpcore==1.0.9
|
| 40 |
+
httpx==0.28.1
|
| 41 |
+
hyperframe==6.1.0
|
| 42 |
+
idna==3.10
|
| 43 |
+
iniconfig==2.1.0
|
| 44 |
+
multidict==6.4.4
|
| 45 |
+
packaging==25.0
|
| 46 |
+
pluggy==1.6.0
|
| 47 |
+
postgrest==1.0.2
|
| 48 |
+
propcache==0.3.1
|
| 49 |
+
pydantic==2.11.5
|
| 50 |
+
pydantic_core==2.33.2
|
| 51 |
+
Pygments==2.19.1
|
| 52 |
+
PyJWT==2.10.1
|
| 53 |
+
pytest==8.4.0
|
| 54 |
+
pytest-mock==3.14.1
|
| 55 |
+
python-dateutil==2.9.0.post0
|
| 56 |
+
python-dotenv==1.1.0
|
| 57 |
+
python-multipart==0.0.20
|
| 58 |
+
realtime==2.4.3
|
| 59 |
+
requests==2.32.3
|
| 60 |
+
six==1.17.0
|
| 61 |
+
sniffio==1.3.1
|
| 62 |
+
starlette==0.46.2
|
| 63 |
+
storage3==0.11.3
|
| 64 |
+
StrEnum==0.4.15
|
| 65 |
+
stripe==12.2.0
|
| 66 |
+
supabase==2.15.2
|
| 67 |
+
supafunc==0.9.4
|
| 68 |
+
typing-inspection==0.4.1
|
| 69 |
+
typing_extensions==4.14.0
|
| 70 |
+
urllib3==2.4.0
|
| 71 |
+
uvicorn==0.34.3
|
| 72 |
+
websockets==14.2
|
| 73 |
+
yarl==1.20.0
|