Maksymilian Jankowski commited on
Commit
b62d4a1
·
1 Parent(s): abd051a
.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=msy_WJfku90Ruj2JHSv6ulQspIRhmputFHdpJOCO
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
- # Verify the JWT token with Supabase
44
- user = supabase.auth.get_user(credentials.credentials)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": 3
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