Spaces:
Paused
Paused
Maksymilian Jankowski commited on
Commit ·
ea12465
1
Parent(s): ccf7dda
deploy fix
Browse files- Dockerfile +11 -0
- check_deployment.py +145 -0
- main.py +54 -21
- requirements.txt +4 -63
Dockerfile
CHANGED
|
@@ -39,5 +39,16 @@ EXPOSE 7860
|
|
| 39 |
ENV PYTHONUNBUFFERED=1
|
| 40 |
ENV PORT=7860
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# Command to run the application
|
| 43 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 39 |
ENV PYTHONUNBUFFERED=1
|
| 40 |
ENV PORT=7860
|
| 41 |
|
| 42 |
+
# Add environment variable validation
|
| 43 |
+
ENV SUPABASE_URL=${SUPABASE_URL}
|
| 44 |
+
ENV SUPABASE_KEY=${SUPABASE_KEY}
|
| 45 |
+
ENV JWT_SECRET_KEY=${JWT_SECRET_KEY:-your-secret-key-change-this-in-production}
|
| 46 |
+
ENV OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 47 |
+
ENV STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
| 48 |
+
|
| 49 |
+
# Add health check
|
| 50 |
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 51 |
+
CMD curl -f http://localhost:7860/ || exit 1
|
| 52 |
+
|
| 53 |
# Command to run the application
|
| 54 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
check_deployment.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Deployment troubleshooting script for 3DAI API Backend
|
| 4 |
+
Run this script to check your deployment configuration
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
def check_environment_variables():
|
| 12 |
+
"""Check if all required environment variables are set"""
|
| 13 |
+
print("🔍 Checking Environment Variables...")
|
| 14 |
+
|
| 15 |
+
required_vars = [
|
| 16 |
+
"SUPABASE_URL",
|
| 17 |
+
"SUPABASE_KEY",
|
| 18 |
+
"OPENAI_API_KEY",
|
| 19 |
+
"STRIPE_SECRET_KEY"
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
optional_vars = [
|
| 23 |
+
"JWT_SECRET_KEY"
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
missing_vars = []
|
| 27 |
+
|
| 28 |
+
for var in required_vars:
|
| 29 |
+
value = os.getenv(var)
|
| 30 |
+
if not value:
|
| 31 |
+
missing_vars.append(var)
|
| 32 |
+
print(f"❌ {var}: Not set")
|
| 33 |
+
else:
|
| 34 |
+
# Show partial value for security
|
| 35 |
+
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
| 36 |
+
print(f"✅ {var}: {masked_value}")
|
| 37 |
+
|
| 38 |
+
for var in optional_vars:
|
| 39 |
+
value = os.getenv(var)
|
| 40 |
+
if not value:
|
| 41 |
+
print(f"⚠️ {var}: Not set (using default)")
|
| 42 |
+
else:
|
| 43 |
+
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
| 44 |
+
print(f"✅ {var}: {masked_value}")
|
| 45 |
+
|
| 46 |
+
return missing_vars
|
| 47 |
+
|
| 48 |
+
def check_dependencies():
|
| 49 |
+
"""Check if all required Python packages are installed"""
|
| 50 |
+
print("\n📦 Checking Dependencies...")
|
| 51 |
+
|
| 52 |
+
required_packages = [
|
| 53 |
+
"fastapi",
|
| 54 |
+
"uvicorn",
|
| 55 |
+
"supabase",
|
| 56 |
+
"httpx",
|
| 57 |
+
"stripe",
|
| 58 |
+
"pydantic",
|
| 59 |
+
"python-jose",
|
| 60 |
+
"passlib"
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
missing_packages = []
|
| 64 |
+
|
| 65 |
+
for package in required_packages:
|
| 66 |
+
try:
|
| 67 |
+
__import__(package.replace("-", "_"))
|
| 68 |
+
print(f"✅ {package}: Installed")
|
| 69 |
+
except ImportError:
|
| 70 |
+
missing_packages.append(package)
|
| 71 |
+
print(f"❌ {package}: Not installed")
|
| 72 |
+
|
| 73 |
+
return missing_packages
|
| 74 |
+
|
| 75 |
+
def check_supabase_connection():
|
| 76 |
+
"""Test Supabase connection"""
|
| 77 |
+
print("\n🗄️ Testing Supabase Connection...")
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
from supabase import create_client
|
| 81 |
+
|
| 82 |
+
url = os.getenv("SUPABASE_URL")
|
| 83 |
+
key = os.getenv("SUPABASE_KEY")
|
| 84 |
+
|
| 85 |
+
if not url or not key:
|
| 86 |
+
print("❌ Supabase credentials not set")
|
| 87 |
+
return False
|
| 88 |
+
|
| 89 |
+
client = create_client(url, key)
|
| 90 |
+
|
| 91 |
+
# Test a simple query
|
| 92 |
+
result = client.from_("User").select("user_id").limit(1).execute()
|
| 93 |
+
print("✅ Supabase connection successful")
|
| 94 |
+
return True
|
| 95 |
+
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(f"❌ Supabase connection failed: {str(e)}")
|
| 98 |
+
return False
|
| 99 |
+
|
| 100 |
+
def main():
|
| 101 |
+
"""Main troubleshooting function"""
|
| 102 |
+
print("🚀 3DAI API Backend Deployment Check")
|
| 103 |
+
print("=" * 50)
|
| 104 |
+
|
| 105 |
+
# Load environment variables
|
| 106 |
+
load_dotenv()
|
| 107 |
+
|
| 108 |
+
# Check environment variables
|
| 109 |
+
missing_env_vars = check_environment_variables()
|
| 110 |
+
|
| 111 |
+
# Check dependencies
|
| 112 |
+
missing_packages = check_dependencies()
|
| 113 |
+
|
| 114 |
+
# Check Supabase connection
|
| 115 |
+
supabase_ok = check_supabase_connection()
|
| 116 |
+
|
| 117 |
+
# Summary
|
| 118 |
+
print("\n📋 Summary:")
|
| 119 |
+
print("=" * 30)
|
| 120 |
+
|
| 121 |
+
if missing_env_vars:
|
| 122 |
+
print(f"❌ Missing environment variables: {', '.join(missing_env_vars)}")
|
| 123 |
+
print(" Fix: Set these in your deployment environment or .env file")
|
| 124 |
+
|
| 125 |
+
if missing_packages:
|
| 126 |
+
print(f"❌ Missing packages: {', '.join(missing_packages)}")
|
| 127 |
+
print(" Fix: Run 'pip install -r requirements.txt'")
|
| 128 |
+
|
| 129 |
+
if not supabase_ok:
|
| 130 |
+
print("❌ Supabase connection failed")
|
| 131 |
+
print(" Fix: Check your SUPABASE_URL and SUPABASE_KEY")
|
| 132 |
+
|
| 133 |
+
if not missing_env_vars and not missing_packages and supabase_ok:
|
| 134 |
+
print("✅ All checks passed! Your deployment should work.")
|
| 135 |
+
print("\n🔧 If sync-supabase-user still doesn't work, check:")
|
| 136 |
+
print(" 1. Network connectivity to your deployed service")
|
| 137 |
+
print(" 2. API endpoint URL is correct")
|
| 138 |
+
print(" 3. Check application logs for specific errors")
|
| 139 |
+
print(" 4. Verify your Supabase database schema matches the code")
|
| 140 |
+
else:
|
| 141 |
+
print("\n❌ Issues found. Fix the above problems and try again.")
|
| 142 |
+
sys.exit(1)
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
main()
|
main.py
CHANGED
|
@@ -174,10 +174,17 @@ async def sync_supabase_user(request: SyncUserRequest, supabase_token: str = Dep
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
user_data = {
|
| 183 |
"user_id": supabase_user["id"],
|
|
@@ -188,47 +195,73 @@ async def sync_supabase_user(request: SyncUserRequest, supabase_token: str = Dep
|
|
| 188 |
}
|
| 189 |
|
| 190 |
if existing_user.data:
|
|
|
|
| 191 |
# Update existing user with any new information
|
| 192 |
if request.full_name:
|
| 193 |
-
|
| 194 |
-
"
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
else:
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
# Generate backend JWT token
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
# Get updated user profile
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
"access_token": backend_token,
|
| 217 |
"token_type": "bearer",
|
| 218 |
"user": {
|
| 219 |
"id": supabase_user["id"],
|
| 220 |
"email": supabase_user["email"],
|
| 221 |
-
"profile":
|
| 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"
|
| 231 |
-
raise HTTPException(status_code=
|
| 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"])
|
|
|
|
| 174 |
"""
|
| 175 |
try:
|
| 176 |
# Verify the Supabase token
|
| 177 |
+
logging.info(f"Verifying Supabase token for user sync")
|
| 178 |
supabase_user = verify_supabase_token(supabase_token)
|
| 179 |
+
logging.info(f"Token verified for user: {supabase_user.get('email', 'unknown')}")
|
| 180 |
|
| 181 |
# Check if user already exists in our database
|
| 182 |
+
try:
|
| 183 |
+
existing_user = supabase.from_("User").select("*").eq("user_id", supabase_user["id"]).execute()
|
| 184 |
+
logging.info(f"Database query completed. Existing user found: {bool(existing_user.data)}")
|
| 185 |
+
except Exception as db_error:
|
| 186 |
+
logging.error(f"Database query failed: {str(db_error)}")
|
| 187 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(db_error)}")
|
| 188 |
|
| 189 |
user_data = {
|
| 190 |
"user_id": supabase_user["id"],
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
if existing_user.data:
|
| 198 |
+
logging.info("Updating existing user profile")
|
| 199 |
# Update existing user with any new information
|
| 200 |
if request.full_name:
|
| 201 |
+
try:
|
| 202 |
+
supabase.from_("User").update({
|
| 203 |
+
"fullname": request.full_name
|
| 204 |
+
}).eq("user_id", supabase_user["id"]).execute()
|
| 205 |
+
logging.info("User profile updated successfully")
|
| 206 |
+
except Exception as update_error:
|
| 207 |
+
logging.error(f"Failed to update user profile: {str(update_error)}")
|
| 208 |
+
# Don't fail the entire operation for profile update errors
|
| 209 |
else:
|
| 210 |
+
logging.info("Creating new user profile")
|
| 211 |
+
try:
|
| 212 |
+
# Create new user profile
|
| 213 |
+
supabase.from_("User").insert(user_data).execute()
|
| 214 |
+
logging.info("User profile created successfully")
|
| 215 |
+
|
| 216 |
+
# Initialize credits for new user
|
| 217 |
+
supabase.from_("User_Credit_Account").insert({
|
| 218 |
+
"user_id": supabase_user["id"],
|
| 219 |
+
"num_of_available_gens": 3 # Give new users 3 free credits
|
| 220 |
+
}).execute()
|
| 221 |
+
logging.info("User credits initialized successfully")
|
| 222 |
+
except Exception as create_error:
|
| 223 |
+
logging.error(f"Failed to create user profile or credits: {str(create_error)}")
|
| 224 |
+
raise HTTPException(status_code=500, detail=f"Failed to create user profile: {str(create_error)}")
|
| 225 |
|
| 226 |
# Generate backend JWT token
|
| 227 |
+
try:
|
| 228 |
+
backend_token = create_backend_jwt_token({
|
| 229 |
+
"id": supabase_user["id"],
|
| 230 |
+
"email": supabase_user["email"]
|
| 231 |
+
})
|
| 232 |
+
logging.info("Backend JWT token generated successfully")
|
| 233 |
+
except Exception as token_error:
|
| 234 |
+
logging.error(f"Failed to generate backend token: {str(token_error)}")
|
| 235 |
+
raise HTTPException(status_code=500, detail=f"Token generation failed: {str(token_error)}")
|
| 236 |
|
| 237 |
# Get updated user profile
|
| 238 |
+
try:
|
| 239 |
+
user_profile = supabase.from_("User").select("*").eq("user_id", supabase_user["id"]).single().execute()
|
| 240 |
+
profile_data = user_profile.data if user_profile.data else user_data
|
| 241 |
+
except Exception as profile_error:
|
| 242 |
+
logging.warning(f"Failed to fetch updated profile: {str(profile_error)}")
|
| 243 |
+
profile_data = user_data # Use the original user_data as fallback
|
| 244 |
|
| 245 |
+
response_data = {
|
| 246 |
"access_token": backend_token,
|
| 247 |
"token_type": "bearer",
|
| 248 |
"user": {
|
| 249 |
"id": supabase_user["id"],
|
| 250 |
"email": supabase_user["email"],
|
| 251 |
+
"profile": profile_data
|
| 252 |
},
|
| 253 |
"message": "User synced successfully"
|
| 254 |
}
|
| 255 |
|
| 256 |
+
logging.info("User sync completed successfully")
|
| 257 |
+
return response_data
|
| 258 |
+
|
| 259 |
except HTTPException:
|
| 260 |
# Re-raise HTTP exceptions as-is
|
| 261 |
raise
|
| 262 |
except Exception as e:
|
| 263 |
+
logging.error(f"Unexpected error in sync_supabase_user: {str(e)}", exc_info=True)
|
| 264 |
+
raise HTTPException(status_code=500, detail=f"Internal server error during user sync: {str(e)}")
|
| 265 |
|
| 266 |
# TODO: The signup also creates a profile, so this endpoint should either be removed or /signup updated.
|
| 267 |
@app.post("/auth/complete-profile", tags=["Authentication"])
|
requirements.txt
CHANGED
|
@@ -8,66 +8,7 @@ openai
|
|
| 8 |
supabase>=2.0.0
|
| 9 |
python-jose[cryptography]>=3.3.0
|
| 10 |
passlib[bcrypt]>=1.7.4
|
| 11 |
-
stripe>=5.0.
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 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
|
|
|
|
| 8 |
supabase>=2.0.0
|
| 9 |
python-jose[cryptography]>=3.3.0
|
| 10 |
passlib[bcrypt]>=1.7.4
|
| 11 |
+
stripe>=5.0.0
|
| 12 |
+
python-multipart>=0.0.20
|
| 13 |
+
requests>=2.32.3
|
| 14 |
+
PyJWT>=2.10.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|