diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..e61f2f9f3aee1fdfbd31ef8f137ce54f92c5a946 --- /dev/null +++ b/.env @@ -0,0 +1,11 @@ +# .env file +DATABASE_URL=postgresql://postgres.sgctwsjvgrvbcjyvwprg:yash9897422911@aws-1-ap-southeast-1.pooler.supabase.com:6543/postgres +SUPABASE_URL=https://sgctwsjvgrvbcjyvwprg.supabase.co +SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNnY3R3c2p2Z3J2YmNqeXZ3cHJnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA3MzgzMDcsImV4cCI6MjA4NjMxNDMwN30.OlcoyXaDu1Hn6xApQAsjO0XeW2klcvJRp-4Elnx_OBg +SUPABASE_JWT_SECRET=ktkcGAWLUzVmgUdJFfyGROURYbyS9efKJU1dZXEzLRLuNkY8DX3HISRFvwosh3LSsuRbCtUI69eCYAI2UTcpug== +SECRET_KEY=django-insecure-change-me-later-for-production +DEBUG=True +ALLOWED_HOSTS=* + +SENDGRID_API_KEY=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 +DEFAULT_FROM_EMAIL=codewithyash124@gmail.com \ No newline at end of file diff --git a/ai_assistant/__init__.py b/ai_assistant/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ai_assistant/__pycache__/__init__.cpython-313.pyc b/ai_assistant/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d3d2049f1080d87b7af5301087fedbcab7723b6 Binary files /dev/null and b/ai_assistant/__pycache__/__init__.cpython-313.pyc differ diff --git a/ai_assistant/__pycache__/__init__.cpython-314.pyc b/ai_assistant/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9695dcd1667eaaaaa2d89921460f98bdd6ee9dee Binary files /dev/null and b/ai_assistant/__pycache__/__init__.cpython-314.pyc differ diff --git a/ai_assistant/__pycache__/admin.cpython-313.pyc b/ai_assistant/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16f4d40c169b93f1f9689cf7ee7ea5abdf0a8341 Binary files /dev/null and b/ai_assistant/__pycache__/admin.cpython-313.pyc differ diff --git a/ai_assistant/__pycache__/admin.cpython-314.pyc b/ai_assistant/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c498a20ee9114b4244141ea33e6418b9a6fb2e4b Binary files /dev/null and b/ai_assistant/__pycache__/admin.cpython-314.pyc differ diff --git a/ai_assistant/__pycache__/apps.cpython-313.pyc b/ai_assistant/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1a699b641efd0bd60f75b37aed73c3331ad6c35 Binary files /dev/null and b/ai_assistant/__pycache__/apps.cpython-313.pyc differ diff --git a/ai_assistant/__pycache__/apps.cpython-314.pyc b/ai_assistant/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df0b38421d80432222437c34bc1d50680428388d Binary files /dev/null and b/ai_assistant/__pycache__/apps.cpython-314.pyc differ diff --git a/ai_assistant/__pycache__/models.cpython-313.pyc b/ai_assistant/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ac6a1e66e7fbad7886b3941e00fc5dff6ac9863 Binary files /dev/null and b/ai_assistant/__pycache__/models.cpython-313.pyc differ diff --git a/ai_assistant/__pycache__/models.cpython-314.pyc b/ai_assistant/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f2148a5148f7e5bd288329e98bbc079ecc75499 Binary files /dev/null and b/ai_assistant/__pycache__/models.cpython-314.pyc differ diff --git a/ai_assistant/admin.py b/ai_assistant/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/ai_assistant/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/ai_assistant/apps.py b/ai_assistant/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..676bf8f04fb7386f5a20141e9b0fec49685289c5 --- /dev/null +++ b/ai_assistant/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AiAssistantConfig(AppConfig): + name = 'ai_assistant' diff --git a/ai_assistant/migrations/__init__.py b/ai_assistant/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ai_assistant/migrations/__pycache__/__init__.cpython-313.pyc b/ai_assistant/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbab088dde9a6b844f42df364d5808407fb14912 Binary files /dev/null and b/ai_assistant/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/ai_assistant/migrations/__pycache__/__init__.cpython-314.pyc b/ai_assistant/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99924001e5b70940d7c96103b0c32b69df17466b Binary files /dev/null and b/ai_assistant/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/ai_assistant/models.py b/ai_assistant/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/ai_assistant/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/ai_assistant/tests.py b/ai_assistant/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/ai_assistant/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ai_assistant/views.py b/ai_assistant/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/ai_assistant/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/aureon_backend/__init__.py b/aureon_backend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/aureon_backend/__pycache__/__init__.cpython-313.pyc b/aureon_backend/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5e1540f1624967e96524a8180bcb002a19696b1 Binary files /dev/null and b/aureon_backend/__pycache__/__init__.cpython-313.pyc differ diff --git a/aureon_backend/__pycache__/__init__.cpython-314.pyc b/aureon_backend/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88c8a2098d466f5958ababbd0a349167d2327fa0 Binary files /dev/null and b/aureon_backend/__pycache__/__init__.cpython-314.pyc differ diff --git a/aureon_backend/__pycache__/settings.cpython-313.pyc b/aureon_backend/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ccc75fc114d1ae457bb394afe441772a6c34ed8 Binary files /dev/null and b/aureon_backend/__pycache__/settings.cpython-313.pyc differ diff --git a/aureon_backend/__pycache__/settings.cpython-314.pyc b/aureon_backend/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..753eb3c3d876ef689113c65457e6ac12325b113d Binary files /dev/null and b/aureon_backend/__pycache__/settings.cpython-314.pyc differ diff --git a/aureon_backend/__pycache__/urls.cpython-313.pyc b/aureon_backend/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ae1fe4c0054f6d12474cf378373433c03e6d581 Binary files /dev/null and b/aureon_backend/__pycache__/urls.cpython-313.pyc differ diff --git a/aureon_backend/__pycache__/urls.cpython-314.pyc b/aureon_backend/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59d999fbca416b0c369b45272290a12aa4364560 Binary files /dev/null and b/aureon_backend/__pycache__/urls.cpython-314.pyc differ diff --git a/aureon_backend/__pycache__/wsgi.cpython-313.pyc b/aureon_backend/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c61c63bdbc3a79f8bce24a460f96b46f41ddde4 Binary files /dev/null and b/aureon_backend/__pycache__/wsgi.cpython-313.pyc differ diff --git a/aureon_backend/__pycache__/wsgi.cpython-314.pyc b/aureon_backend/__pycache__/wsgi.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e050828e3bab9397dee42266c66c19aa4d070fa3 Binary files /dev/null and b/aureon_backend/__pycache__/wsgi.cpython-314.pyc differ diff --git a/aureon_backend/asgi.py b/aureon_backend/asgi.py new file mode 100644 index 0000000000000000000000000000000000000000..16463655a91c0f1f957c17017bb1f10c9de556b1 --- /dev/null +++ b/aureon_backend/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for aureon_backend project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aureon_backend.settings') + +application = get_asgi_application() diff --git a/aureon_backend/settings.py b/aureon_backend/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..9d0c325ae52d6f71f8de291ede74e2effb163168 --- /dev/null +++ b/aureon_backend/settings.py @@ -0,0 +1,170 @@ +# backend/aureon_backend/settings.py + +import os +from pathlib import Path +import dj_database_url +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key') +DEBUG = os.getenv('DEBUG', 'True') == 'True' +ALLOWED_HOSTS = ['*'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'users', + 'finance', + 'ai_assistant', + 'importer', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'aureon_backend.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'aureon_backend.wsgi.application' + +DATABASES = { + 'default': dj_database_url.config( + default=os.getenv('DATABASE_URL'), + conn_max_age=600, + conn_health_checks=True, + ssl_require=True, + ) +} + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True +STATIC_URL = 'static/' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = 'users.User' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'users.authentication.SupabaseAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', +} + +# Supabase Configuration +SUPABASE_URL = os.getenv('SUPABASE_URL') +SUPABASE_KEY = os.getenv('SUPABASE_KEY') +SUPABASE_JWT_SECRET = os.getenv('SUPABASE_JWT_SECRET') # Optional - only needed for HS256 + +# CORS Configuration +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] +CORS_ALLOW_METHODS = [ + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', +] + +# CSRF Settings +CSRF_TRUSTED_ORIGINS = ['http://localhost:5173', 'http://127.0.0.1:5173'] +CSRF_COOKIE_SECURE = False +CSRF_COOKIE_HTTPONLY = False + +# Cache Configuration (required for JWKS key caching) +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake', + 'TIMEOUT': 3600, # 1 hour + 'OPTIONS': { + 'MAX_ENTRIES': 1000 + } + } +} + +# Email Settings (SendGrid) +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.sendgrid.net' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = 'apikey' +EMAIL_HOST_PASSWORD = os.getenv('SENDGRID_API_KEY', '') +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@aureon.com') + +# Logging Configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} \ No newline at end of file diff --git a/aureon_backend/urls.py b/aureon_backend/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..616fe4ef0dd08e306d56f72b69f19bbd0f980807 --- /dev/null +++ b/aureon_backend/urls.py @@ -0,0 +1,9 @@ +# backend/aureon_backend/urls.py +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/auth/', include('users.urls')), # This creates /api/auth/me/ + # path('api/finance/', include('finance.urls')), # We will uncomment this later +] diff --git a/aureon_backend/wsgi.py b/aureon_backend/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..1adffc8670357e2c01cc00fc920b721b2098909b --- /dev/null +++ b/aureon_backend/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for aureon_backend project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aureon_backend.settings') + +application = get_wsgi_application() diff --git a/debug_token.py b/debug_token.py new file mode 100644 index 0000000000000000000000000000000000000000..d52c028792abdbc7e3fe796ebae38c774398f1a2 --- /dev/null +++ b/debug_token.py @@ -0,0 +1,152 @@ +# backend/debug_token.py +""" +Run this script to debug JWT token issues: +python debug_token.py + +This will help you understand what's in your token and verify your JWT secret. +""" + +import jwt +import json +import sys +import os + +# Add the parent directory to the path so we can import settings +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Try to load Django settings +try: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aureon_backend.settings') + import django + django.setup() + from django.conf import settings + SUPABASE_JWT_SECRET = settings.SUPABASE_JWT_SECRET + print(f"✅ Loaded JWT_SECRET from Django settings") + print(f" JWT_SECRET (first 20 chars): {SUPABASE_JWT_SECRET[:20]}...") +except Exception as e: + print(f"⚠️ Could not load Django settings: {e}") + print(f" Please enter your JWT_SECRET manually below") + SUPABASE_JWT_SECRET = None + +print("\n" + "="*60) +print("SUPABASE JWT TOKEN DEBUGGER") +print("="*60) + +# Get token from user +print("\nPaste your Supabase token here:") +print("(You can get it from localStorage in your browser console)") +print("localStorage.getItem('supabase_token')") +print() +token = input("Token: ").strip() + +if not token: + print("❌ No token provided!") + sys.exit(1) + +print("\n" + "-"*60) +print("STEP 1: Decode Header (unverified)") +print("-"*60) + +try: + header = jwt.get_unverified_header(token) + print("✅ Token Header:") + print(json.dumps(header, indent=2)) + algorithm = header.get('alg', 'UNKNOWN') + print(f"\n🔍 Algorithm: {algorithm}") +except Exception as e: + print(f"❌ Could not decode header: {e}") + sys.exit(1) + +print("\n" + "-"*60) +print("STEP 2: Decode Payload (unverified)") +print("-"*60) + +try: + # Decode without verification to see the contents + payload = jwt.decode(token, options={"verify_signature": False}) + print("✅ Token Payload:") + print(json.dumps(payload, indent=2)) + + print(f"\n🔍 Key Information:") + print(f" User ID (sub): {payload.get('sub', 'NOT FOUND')}") + print(f" Email: {payload.get('email', 'NOT FOUND')}") + print(f" Audience: {payload.get('aud', 'NOT FOUND')}") + print(f" Issuer: {payload.get('iss', 'NOT FOUND')}") + + # Check expiration + import time + exp = payload.get('exp') + if exp: + exp_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(exp)) + is_expired = exp < time.time() + print(f" Expires: {exp_date} {'(EXPIRED!)' if is_expired else '(valid)'}") + +except Exception as e: + print(f"❌ Could not decode payload: {e}") + sys.exit(1) + +print("\n" + "-"*60) +print("STEP 3: Verify Signature") +print("-"*60) + +if not SUPABASE_JWT_SECRET: + print("Enter your SUPABASE_JWT_SECRET:") + SUPABASE_JWT_SECRET = input("JWT_SECRET: ").strip() + +if not SUPABASE_JWT_SECRET: + print("⚠️ No JWT_SECRET provided - skipping verification") +else: + try: + # Try to decode with verification + verified_payload = jwt.decode( + token, + SUPABASE_JWT_SECRET, + algorithms=[algorithm], + audience="authenticated" + ) + print("✅ Signature verification PASSED!") + print(" Your JWT_SECRET is CORRECT!") + + except jwt.InvalidSignatureError: + print("❌ Signature verification FAILED!") + print(" Your JWT_SECRET is WRONG!") + print("\n How to find the correct JWT_SECRET:") + print(" 1. Go to your Supabase Dashboard") + print(" 2. Settings → API") + print(" 3. Look for 'JWT Secret' (NOT the service key)") + print(" 4. Copy that value to your .env file as SUPABASE_JWT_SECRET") + + except jwt.ExpiredSignatureError: + print("❌ Token has EXPIRED!") + print(" Solution: Logout and login again to get a new token") + + except jwt.InvalidAudienceError: + print("❌ Invalid audience!") + print(" The token audience doesn't match 'authenticated'") + + except jwt.InvalidAlgorithmError as e: + print(f"❌ Algorithm error: {e}") + print(f" The token uses {algorithm} but it's not allowed") + + except Exception as e: + print(f"❌ Verification error: {type(e).__name__}: {e}") + +print("\n" + "="*60) +print("SUMMARY") +print("="*60) + +print(f"\n✓ Token structure: Valid") +print(f"✓ Algorithm: {algorithm}") +print(f"✓ User ID present: {'Yes' if payload.get('sub') else 'No'}") +print(f"✓ Email present: {'Yes' if payload.get('email') else 'No'}") + +if SUPABASE_JWT_SECRET: + print(f"\nNext steps:") + print(f"1. Make sure backend/aureon_backend/settings.py uses:") + print(f" algorithms=['{algorithm}']") + print(f"2. Make sure your .env has the correct JWT_SECRET") + print(f"3. Restart Django server") +else: + print(f"\n⚠️ Could not verify signature - JWT_SECRET not provided") + +print("\n") \ No newline at end of file diff --git a/finance/__init__.py b/finance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/finance/__pycache__/__init__.cpython-313.pyc b/finance/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..193453e2b1bc8753d383473230aa6a16dadc4543 Binary files /dev/null and b/finance/__pycache__/__init__.cpython-313.pyc differ diff --git a/finance/__pycache__/__init__.cpython-314.pyc b/finance/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25a86a019eeebd27d3d52cd026d1d2666b5c45b7 Binary files /dev/null and b/finance/__pycache__/__init__.cpython-314.pyc differ diff --git a/finance/__pycache__/admin.cpython-313.pyc b/finance/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e92390c53fe613a5ffbcac934e7302f2003b21e Binary files /dev/null and b/finance/__pycache__/admin.cpython-313.pyc differ diff --git a/finance/__pycache__/admin.cpython-314.pyc b/finance/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c90839e119d069c13adf50b12059247201cb92cb Binary files /dev/null and b/finance/__pycache__/admin.cpython-314.pyc differ diff --git a/finance/__pycache__/apps.cpython-313.pyc b/finance/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3acf8de841fa706112c26fb8f1134c596a155762 Binary files /dev/null and b/finance/__pycache__/apps.cpython-313.pyc differ diff --git a/finance/__pycache__/apps.cpython-314.pyc b/finance/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88da450ac3105bfe845d515f58ceaf9c1c4e422f Binary files /dev/null and b/finance/__pycache__/apps.cpython-314.pyc differ diff --git a/finance/__pycache__/models.cpython-313.pyc b/finance/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ddceffc8ae6c3cebadeacb85f5405216c47bf97 Binary files /dev/null and b/finance/__pycache__/models.cpython-313.pyc differ diff --git a/finance/__pycache__/models.cpython-314.pyc b/finance/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bd331423fc9430357d79cefb64e097780c4ad30 Binary files /dev/null and b/finance/__pycache__/models.cpython-314.pyc differ diff --git a/finance/admin.py b/finance/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/finance/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/finance/apps.py b/finance/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..06859cb9726ebbb2563e70983781af4f2672ec6a --- /dev/null +++ b/finance/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FinanceConfig(AppConfig): + name = 'finance' diff --git a/finance/migrations/__init__.py b/finance/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/finance/migrations/__pycache__/__init__.cpython-313.pyc b/finance/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e421c605496b55917cfb1ec69f0e53f4dd1a1e47 Binary files /dev/null and b/finance/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/finance/migrations/__pycache__/__init__.cpython-314.pyc b/finance/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5324f69a13a62e83744f832193a27f88420f58a7 Binary files /dev/null and b/finance/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/finance/models.py b/finance/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/finance/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/finance/tests.py b/finance/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/finance/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/finance/views.py b/finance/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/finance/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/importer/__init__.py b/importer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/importer/__pycache__/__init__.cpython-313.pyc b/importer/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6fba0817aac8389ad134db068cfb8e2d1675b78 Binary files /dev/null and b/importer/__pycache__/__init__.cpython-313.pyc differ diff --git a/importer/__pycache__/__init__.cpython-314.pyc b/importer/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26ba9b94d8e37a6f608eafaef085da4dc4ef5062 Binary files /dev/null and b/importer/__pycache__/__init__.cpython-314.pyc differ diff --git a/importer/__pycache__/admin.cpython-313.pyc b/importer/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22805da23d5c9a6e8362ebfbea9bd877f755c763 Binary files /dev/null and b/importer/__pycache__/admin.cpython-313.pyc differ diff --git a/importer/__pycache__/admin.cpython-314.pyc b/importer/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00dd5d817062c42f46ac37cc2d196850df206d8d Binary files /dev/null and b/importer/__pycache__/admin.cpython-314.pyc differ diff --git a/importer/__pycache__/apps.cpython-313.pyc b/importer/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9764f36f5441dbe72503190466e7aae1b3466f2d Binary files /dev/null and b/importer/__pycache__/apps.cpython-313.pyc differ diff --git a/importer/__pycache__/apps.cpython-314.pyc b/importer/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a394ce6f8cd3b53bc93c6a19ad6f74da6f03885 Binary files /dev/null and b/importer/__pycache__/apps.cpython-314.pyc differ diff --git a/importer/__pycache__/models.cpython-313.pyc b/importer/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..992e4dd643be515561f5aff59dcefc47429c83d7 Binary files /dev/null and b/importer/__pycache__/models.cpython-313.pyc differ diff --git a/importer/__pycache__/models.cpython-314.pyc b/importer/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3847c3581692cc36b408ac780da7250818d76879 Binary files /dev/null and b/importer/__pycache__/models.cpython-314.pyc differ diff --git a/importer/admin.py b/importer/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/importer/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/importer/apps.py b/importer/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f5525cb6468bb9d322eba5a5d554127dcf2c0a8c --- /dev/null +++ b/importer/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImporterConfig(AppConfig): + name = 'importer' diff --git a/importer/migrations/__init__.py b/importer/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/importer/migrations/__pycache__/__init__.cpython-313.pyc b/importer/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da279f03631a8e45a811a03a9399ef7c8c7206e3 Binary files /dev/null and b/importer/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/importer/migrations/__pycache__/__init__.cpython-314.pyc b/importer/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e209602644227abb262c38749ae79f3940ac9212 Binary files /dev/null and b/importer/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/importer/models.py b/importer/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/importer/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/importer/tests.py b/importer/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/importer/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/importer/views.py b/importer/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/importer/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/manage.py b/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..537c8ba5256d73f1aa8d2db8560f86d2e6d4fc24 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aureon_backend.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..eefb9642245760be04ef734401b3f475f40f0bc5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,158 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@supabase/supabase-js": "^2.95.3" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.95.3.tgz", + "integrity": "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.95.3.tgz", + "integrity": "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.95.3.tgz", + "integrity": "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.95.3.tgz", + "integrity": "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.95.3.tgz", + "integrity": "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.95.3.tgz", + "integrity": "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.95.3", + "@supabase/functions-js": "2.95.3", + "@supabase/postgrest-js": "2.95.3", + "@supabase/realtime-js": "2.95.3", + "@supabase/storage-js": "2.95.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d3a2b48a42792141c3de018e5990495489472b3f --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@supabase/supabase-js": "^2.95.3" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d236397862383de9c6b9350feb50c450f5363f6a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyJWT[crypto]>=2.8.0 +requests>=2.31.0 +cryptography>=41.0.0 \ No newline at end of file diff --git a/test_jwks.py b/test_jwks.py new file mode 100644 index 0000000000000000000000000000000000000000..2632fb85cba24f440a3d224c160b15c081483c23 --- /dev/null +++ b/test_jwks.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Find the correct JWKS endpoint for Supabase +Run this from the backend directory: python find_jwks_endpoint.py +""" + +import os +import sys +from pathlib import Path + +# Add Django project to path +BASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(BASE_DIR)) + +# Load environment variables +from dotenv import load_dotenv +load_dotenv() + +import requests + +print("=" * 60) +print("FINDING CORRECT JWKS ENDPOINT") +print("=" * 60) + +supabase_url = os.getenv('SUPABASE_URL') +supabase_key = os.getenv('SUPABASE_KEY') + +print(f"\n📍 SUPABASE_URL: {supabase_url}") + +if not supabase_url or not supabase_key: + print("❌ Missing SUPABASE_URL or SUPABASE_KEY in .env") + sys.exit(1) + +# Try multiple possible JWKS endpoints +endpoints_to_try = [ + "/auth/v1/jwks", + "/auth/v1/.well-known/jwks.json", + "/.well-known/jwks.json", + "/auth/v1/jwks.json", + "/.well-known/openid-configuration", # This might tell us where JWKS is +] + +headers = { + 'apikey': supabase_key, + 'Authorization': f'Bearer {supabase_key}' +} + +print("\n🔍 Trying different endpoints...\n") + +for endpoint in endpoints_to_try: + url = f"{supabase_url.rstrip('/')}{endpoint}" + print(f"Trying: {endpoint}") + + try: + response = requests.get(url, headers=headers, timeout=5) + print(f" Status: {response.status_code}") + + if response.status_code == 200: + print(f" ✅ FOUND IT!") + + try: + data = response.json() + + # Check if it's JWKS format + if 'keys' in data: + keys = data['keys'] + print(f" 🎯 This is the JWKS endpoint with {len(keys)} key(s)!") + + for i, key in enumerate(keys, 1): + print(f"\n Key #{i}:") + print(f" kid: {key.get('kid')}") + print(f" alg: {key.get('alg')}") + print(f" kty: {key.get('kty')}") + + print(f"\n{'='*60}") + print(f"✅ USE THIS ENDPOINT IN YOUR CODE:") + print(f" {url}") + print(f"{'='*60}") + break + + # Check if it's OpenID config + elif 'jwks_uri' in data: + print(f" 📝 OpenID config found!") + print(f" JWKS URI: {data['jwks_uri']}") + + # Try the JWKS URI + jwks_response = requests.get(data['jwks_uri'], headers=headers, timeout=5) + if jwks_response.status_code == 200: + jwks_data = jwks_response.json() + keys = jwks_data.get('keys', []) + print(f" ✅ Found {len(keys)} keys at JWKS URI!") + + print(f"\n{'='*60}") + print(f"✅ USE THIS ENDPOINT IN YOUR CODE:") + print(f" {data['jwks_uri']}") + print(f"{'='*60}") + break + else: + print(f" Response: {str(data)[:200]}") + except: + print(f" Response (not JSON): {response.text[:200]}") + else: + print(f" Status: {response.status_code} - {response.text[:100]}") + except Exception as e: + print(f" Error: {e}") + + print() + +print("\n" + "="*60) +print("ALTERNATIVE: Check Supabase Dashboard") +print("="*60) +print("\nIf no endpoint worked, your keys might only be accessible via:") +print("1. Supabase Dashboard → Settings → API → JWT Signing Keys") +print("2. Copy the public key manually") +print("3. Hard-code it in Django settings") +print("\n" + "="*60) \ No newline at end of file diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/users/__pycache__/__init__.cpython-313.pyc b/users/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..512c0b5be476d977d20ef0822b5e6259cd2eace9 Binary files /dev/null and b/users/__pycache__/__init__.cpython-313.pyc differ diff --git a/users/__pycache__/__init__.cpython-314.pyc b/users/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bd7979de0ed90d2436e3b483829bdd855b5c155 Binary files /dev/null and b/users/__pycache__/__init__.cpython-314.pyc differ diff --git a/users/__pycache__/admin.cpython-313.pyc b/users/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b99374a142436cb8cd71ae0555450c69a19e9237 Binary files /dev/null and b/users/__pycache__/admin.cpython-313.pyc differ diff --git a/users/__pycache__/admin.cpython-314.pyc b/users/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d01cdfbf7074bf86c646b03957225fd272e31a40 Binary files /dev/null and b/users/__pycache__/admin.cpython-314.pyc differ diff --git a/users/__pycache__/apps.cpython-313.pyc b/users/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bce7f73693fd23d8daef1b238fa773ceef14bbe9 Binary files /dev/null and b/users/__pycache__/apps.cpython-313.pyc differ diff --git a/users/__pycache__/apps.cpython-314.pyc b/users/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd2b0e08b0ecc4262bdd3f40214b260a1c3b3aa5 Binary files /dev/null and b/users/__pycache__/apps.cpython-314.pyc differ diff --git a/users/__pycache__/authentication.cpython-313.pyc b/users/__pycache__/authentication.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9430b9dcb8e66f2a2e12a26556619001607a201c Binary files /dev/null and b/users/__pycache__/authentication.cpython-313.pyc differ diff --git a/users/__pycache__/authentication.cpython-314.pyc b/users/__pycache__/authentication.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..495f6c8685f5f5c1b18ff206de298c3efe39d4b7 Binary files /dev/null and b/users/__pycache__/authentication.cpython-314.pyc differ diff --git a/users/__pycache__/models.cpython-313.pyc b/users/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65f78dcc9236ea3d1990c4ead70cd59b1aa32395 Binary files /dev/null and b/users/__pycache__/models.cpython-313.pyc differ diff --git a/users/__pycache__/models.cpython-314.pyc b/users/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acdd3f114b80b5cea2d87f6d9d7aaf4d040e3be0 Binary files /dev/null and b/users/__pycache__/models.cpython-314.pyc differ diff --git a/users/__pycache__/serializers.cpython-313.pyc b/users/__pycache__/serializers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd7474978a1a21f49ce2c8dfb176d533d23e202a Binary files /dev/null and b/users/__pycache__/serializers.cpython-313.pyc differ diff --git a/users/__pycache__/serializers.cpython-314.pyc b/users/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68cd95c501a71216b1dc7f70527b5d736d0e584c Binary files /dev/null and b/users/__pycache__/serializers.cpython-314.pyc differ diff --git a/users/__pycache__/signals.cpython-313.pyc b/users/__pycache__/signals.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da6d48a25e6e2c74b8a011085f1100ac6e826460 Binary files /dev/null and b/users/__pycache__/signals.cpython-313.pyc differ diff --git a/users/__pycache__/signals.cpython-314.pyc b/users/__pycache__/signals.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc948ee4dbe047540d6fc1bd649fd240b915a0c9 Binary files /dev/null and b/users/__pycache__/signals.cpython-314.pyc differ diff --git a/users/__pycache__/urls.cpython-313.pyc b/users/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f90e2f3e59db5e43d534b214e8d7e0032b0a13ea Binary files /dev/null and b/users/__pycache__/urls.cpython-313.pyc differ diff --git a/users/__pycache__/urls.cpython-314.pyc b/users/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fc62b8906b75c1abe590314ab752f3eef9a2c5a Binary files /dev/null and b/users/__pycache__/urls.cpython-314.pyc differ diff --git a/users/__pycache__/utils.cpython-314.pyc b/users/__pycache__/utils.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..680fdf8c5d8eaaa2e36f0e8558ff80692b4fbb9d Binary files /dev/null and b/users/__pycache__/utils.cpython-314.pyc differ diff --git a/users/__pycache__/views.cpython-313.pyc b/users/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00b46ed78fd55884092964a1a7977e0d83086116 Binary files /dev/null and b/users/__pycache__/views.cpython-313.pyc differ diff --git a/users/__pycache__/views.cpython-314.pyc b/users/__pycache__/views.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6441b34e34df9880c50606986bd6258b08dd80db Binary files /dev/null and b/users/__pycache__/views.cpython-314.pyc differ diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..514ecb0ef624c3f3cd02022b329dfe01b7351d45 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,9 @@ +# backend/users/apps.py +from django.apps import AppConfig + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' + + def ready(self): + import users.signals \ No newline at end of file diff --git a/users/authentication.py b/users/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..3246f414b1ee0fe84033da11e94c6b18fe307610 --- /dev/null +++ b/users/authentication.py @@ -0,0 +1,170 @@ +# backend/users/authentication.py +import jwt +import requests +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.cache import cache +from rest_framework import authentication +from rest_framework.exceptions import AuthenticationFailed + +User = get_user_model() + +class SupabaseAuthentication(authentication.BaseAuthentication): + + def get_jwks_keys(self): + """Fetch JWKS keys from Supabase (cached for 1 hour)""" + cache_key = 'supabase_jwks_keys' + keys = cache.get(cache_key) + + if keys is None: + try: + # Correct JWKS endpoint for new JWT Signing Keys + jwks_url = f"{settings.SUPABASE_URL}/auth/v1/.well-known/jwks.json" + + # Supabase requires the anon key + headers = { + 'apikey': settings.SUPABASE_KEY, + 'Authorization': f'Bearer {settings.SUPABASE_KEY}' + } + + print(f"[AUTH DEBUG] Fetching JWKS from: {jwks_url}") + response = requests.get(jwks_url, headers=headers, timeout=5) + response.raise_for_status() + jwks = response.json() + keys = jwks.get('keys', []) + + # Cache for 1 hour + cache.set(cache_key, keys, 3600) + print(f"[AUTH DEBUG] ✅ Cached {len(keys)} JWKS key(s)") + except requests.exceptions.HTTPError as e: + print(f"[AUTH DEBUG] ❌ HTTP error fetching JWKS: {e}") + print(f"[AUTH DEBUG] Response: {e.response.text if hasattr(e, 'response') else 'N/A'}") + keys = [] + except Exception as e: + print(f"[AUTH DEBUG] ❌ Failed to fetch JWKS: {e}") + keys = [] + else: + print(f"[AUTH DEBUG] Using cached JWKS keys ({len(keys)} key(s))") + + return keys + + def get_signing_key(self, token): + """Get the appropriate signing key for the token""" + try: + # Get token header + header = jwt.get_unverified_header(token) + algorithm = header.get('alg') + kid = header.get('kid') + + print(f"[AUTH DEBUG] Token algorithm: {algorithm}") + print(f"[AUTH DEBUG] Token kid: {kid}") + + # For ES256/RS256, we need to get the public key from JWKS + if algorithm.startswith('ES') or algorithm.startswith('RS'): + jwks_keys = self.get_jwks_keys() + + if not jwks_keys: + raise AuthenticationFailed('Could not fetch JWKS keys from Supabase') + + # Find the key matching the token's kid + signing_key = None + for key in jwks_keys: + if key.get('kid') == kid: + # Import the JWK as a public key + from jwt.algorithms import RSAAlgorithm, ECAlgorithm + + if algorithm.startswith('ES'): + signing_key = ECAlgorithm.from_jwk(key) + else: + signing_key = RSAAlgorithm.from_jwk(key) + + print(f"[AUTH DEBUG] ✅ Found matching JWKS key for kid: {kid}") + break + + if not signing_key: + raise AuthenticationFailed(f'No matching key found for kid: {kid}') + + return signing_key, algorithm + + # For HS256/HS384/HS512, use the JWT secret (fallback) + else: + if not hasattr(settings, 'SUPABASE_JWT_SECRET') or not settings.SUPABASE_JWT_SECRET: + raise AuthenticationFailed('JWT secret not configured for HS256 tokens') + return settings.SUPABASE_JWT_SECRET, algorithm + + except Exception as e: + print(f"[AUTH DEBUG] ❌ Error getting signing key: {e}") + raise AuthenticationFailed(f'Could not get signing key: {str(e)}') + + def authenticate(self, request): + # 1. Check for the Authorization header + auth_header = request.headers.get('Authorization') + + if not auth_header: + return None + + try: + # 2. Extract the token + if not auth_header.startswith('Bearer '): + raise AuthenticationFailed('Authorization header must start with Bearer') + + token = auth_header.split(' ')[1] + print(f"[AUTH DEBUG] Token received (first 50 chars): {token[:50]}...") + + # 3. Get the appropriate signing key and algorithm + signing_key, algorithm = self.get_signing_key(token) + + # 4. Decode and verify the token + payload = jwt.decode( + token, + signing_key, + algorithms=[algorithm], + audience="authenticated", + options={ + "verify_aud": True, + "verify_signature": True, + "verify_exp": True + } + ) + + print(f"[AUTH DEBUG] ✅ Token verified successfully with {algorithm}") + + except jwt.ExpiredSignatureError: + print(f"[AUTH DEBUG] ❌ Token has expired") + raise AuthenticationFailed('Token has expired') + except jwt.InvalidAudienceError: + print(f"[AUTH DEBUG] ❌ Invalid token audience") + raise AuthenticationFailed('Invalid token audience') + except jwt.DecodeError as e: + print(f"[AUTH DEBUG] ❌ Decode error: {str(e)}") + raise AuthenticationFailed(f'Invalid token: {str(e)}') + except Exception as e: + print(f"[AUTH DEBUG] ❌ Unexpected error: {type(e).__name__}: {str(e)}") + raise AuthenticationFailed(f'Authentication error: {str(e)}') + + # 5. Get the User's UUID and Email + user_id = payload.get('sub') + email = payload.get('email') + + print(f"[AUTH DEBUG] User ID: {user_id}") + print(f"[AUTH DEBUG] Email: {email}") + + if not user_id: + raise AuthenticationFailed('Token contained no user ID') + + # 6. Find or Create the User + try: + user, created = User.objects.get_or_create( + id=user_id, + defaults={'email': email, 'username': email} + ) + if created: + print(f"[AUTH DEBUG] ✅ Created new user: {email}") + else: + print(f"[AUTH DEBUG] ✅ Found existing user: {email}") + + return (user, None) + + except Exception as e: + print(f"[AUTH DEBUG] ❌ Database error: {str(e)}") + raise AuthenticationFailed(f'User creation failed: {str(e)}') \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..5bb172d32c0e22eb026266ba1d3606506bc26686 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 6.0.2 on 2026-02-10 19:46 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='FinancialProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('net_worth', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)), + ('cash_available', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)), + ('invested_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)), + ('credit_used', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)), + ('credit_limit', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)), + ('is_onboarded', models.BooleanField(default=False)), + ('last_updated', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/users/migrations/0002_financialprofile_email_otp_and_more.py b/users/migrations/0002_financialprofile_email_otp_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..443578a431714989d46e635b27f5706006af1ed5 --- /dev/null +++ b/users/migrations/0002_financialprofile_email_otp_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0.2 on 2026-02-13 07:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='financialprofile', + name='email_otp', + field=models.CharField(blank=True, max_length=6, null=True), + ), + migrations.AddField( + model_name='financialprofile', + name='is_email_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='financialprofile', + name='is_phone_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='financialprofile', + name='phone_number', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name='financialprofile', + name='phone_otp', + field=models.CharField(blank=True, max_length=6, null=True), + ), + ] diff --git a/users/migrations/0003_remove_financialprofile_is_phone_verified_and_more.py b/users/migrations/0003_remove_financialprofile_is_phone_verified_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..42cb8c604ebe9e7a30b7c13dff4d8f475fdb9a45 --- /dev/null +++ b/users/migrations/0003_remove_financialprofile_is_phone_verified_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.2 on 2026-02-14 14:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_financialprofile_email_otp_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='financialprofile', + name='is_phone_verified', + ), + migrations.RemoveField( + model_name='financialprofile', + name='phone_number', + ), + migrations.RemoveField( + model_name='financialprofile', + name='phone_otp', + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/users/migrations/__pycache__/0001_initial.cpython-313.pyc b/users/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a10bb6810bb41eea852e0019da5a9b7628897b38 Binary files /dev/null and b/users/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/users/migrations/__pycache__/0001_initial.cpython-314.pyc b/users/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0804fbd00e9e30e819785dff2212047b1099e1cd Binary files /dev/null and b/users/migrations/__pycache__/0001_initial.cpython-314.pyc differ diff --git a/users/migrations/__pycache__/0002_financialprofile_email_otp_and_more.cpython-314.pyc b/users/migrations/__pycache__/0002_financialprofile_email_otp_and_more.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8c7a08066ecd0e1f85dbbd9f56165b9d2464818 Binary files /dev/null and b/users/migrations/__pycache__/0002_financialprofile_email_otp_and_more.cpython-314.pyc differ diff --git a/users/migrations/__pycache__/0003_remove_financialprofile_is_phone_verified_and_more.cpython-314.pyc b/users/migrations/__pycache__/0003_remove_financialprofile_is_phone_verified_and_more.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..843e5d7354a8007f577cbfa0dbb5a4db5e048268 Binary files /dev/null and b/users/migrations/__pycache__/0003_remove_financialprofile_is_phone_verified_and_more.cpython-314.pyc differ diff --git a/users/migrations/__pycache__/__init__.cpython-313.pyc b/users/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ce9b4830a25bb7b76a60a005138f8dc9efbbe54 Binary files /dev/null and b/users/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/users/migrations/__pycache__/__init__.cpython-314.pyc b/users/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d58280680020550b60387f38b59871dd0955a48 Binary files /dev/null and b/users/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000000000000000000000000000000000000..7fd0d07e1d34ad4ebf253803e0ad44f23c813d06 --- /dev/null +++ b/users/models.py @@ -0,0 +1,31 @@ +# backend/users/models.py +import uuid +from django.db import models +from django.contrib.auth.models import AbstractUser + +class User(AbstractUser): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.email + +class FinancialProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + + # Financial Data + net_worth = models.DecimalField(max_digits=15, decimal_places=2, default=0.00) + cash_available = models.DecimalField(max_digits=15, decimal_places=2, default=0.00) + invested_amount = models.DecimalField(max_digits=15, decimal_places=2, default=0.00) + credit_used = models.DecimalField(max_digits=15, decimal_places=2, default=0.00) + credit_limit = models.DecimalField(max_digits=15, decimal_places=2, default=0.00) + + # Verification Data (Mobile removed) + is_email_verified = models.BooleanField(default=False) + email_otp = models.CharField(max_length=6, blank=True, null=True) + + # Onboarding status + is_onboarded = models.BooleanField(default=False) + last_updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Profile for {self.user.email}" \ No newline at end of file diff --git a/users/serializers.py b/users/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..10f6b7d44f61350a14c9841b366c480cbb0f4e1d --- /dev/null +++ b/users/serializers.py @@ -0,0 +1,23 @@ +# backend/users/serializers.py +from rest_framework import serializers +from django.contrib.auth import get_user_model +from .models import FinancialProfile + +User = get_user_model() + +class FinancialProfileSerializer(serializers.ModelSerializer): + class Meta: + model = FinancialProfile + fields = [ + 'net_worth', 'cash_available', 'invested_amount', + 'credit_used', 'credit_limit', 'is_onboarded', + 'is_email_verified' # Removed phone fields + ] + read_only_fields = ['is_email_verified'] + +class UserSerializer(serializers.ModelSerializer): + profile = FinancialProfileSerializer(read_only=True) + + class Meta: + model = User + fields = ['id', 'email', 'username', 'date_joined', 'profile'] \ No newline at end of file diff --git a/users/signals.py b/users/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..f5980d2e854abe79d679b9d01672dfede0a9090c --- /dev/null +++ b/users/signals.py @@ -0,0 +1,10 @@ +# backend/users/signals.py +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.conf import settings +from .models import FinancialProfile + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def create_user_profile(sender, instance, created, **kwargs): + if created: + FinancialProfile.objects.create(user=instance) \ No newline at end of file diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..37a3a9ba5556aa8c3db38b41c473b57aa3a873bb --- /dev/null +++ b/users/urls.py @@ -0,0 +1,23 @@ +# backend/users/urls.py +from django.urls import path +from .views import ( + UserProfileView, + SendEmailOTPView, + VerifyEmailOTPView, + GetUserMeView +) + +app_name = 'users' + +urlpatterns = [ + # Authentication + path('me/', GetUserMeView.as_view(), name='user-me'), + + # Profile + path('profile/', UserProfileView.as_view(), name='user-profile'), + + # Email verification + path('send-email-otp/', SendEmailOTPView.as_view(), name='send-email-otp'), + path('verify-email-otp/', VerifyEmailOTPView.as_view(), name='verify-email-otp'), + +] \ No newline at end of file diff --git a/users/utils.py b/users/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..13e76fd129452aa164a5e07d2b1da601bd9c78e0 --- /dev/null +++ b/users/utils.py @@ -0,0 +1,24 @@ +# backend/users/utils.py +import os +import random +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail +from django.conf import settings + +def generate_otp(): + return str(random.randint(100000, 999999)) + +def send_otp_via_sendgrid(recipient_email, otp): + message = Mail( + from_email=settings.DEFAULT_FROM_EMAIL, + to_emails=recipient_email, + subject='Your Aureon Verification Code', + plain_text_content=f'Your code is: {otp}' + ) + try: + sg = SendGridAPIClient(os.getenv('SENDGRID_API_KEY')) + response = sg.send(message) + return response.status_code + except Exception as e: + print(f"SendGrid Error: {e}") + return None \ No newline at end of file diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000000000000000000000000000000000000..0007a4a8e8e12656945f34d76b396a27fefeedc9 --- /dev/null +++ b/users/views.py @@ -0,0 +1,124 @@ +# backend/users/views.py +import random +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from django.core.mail import send_mail +from django.conf import settings +from .serializers import UserSerializer +from .models import FinancialProfile + +# --- 1. User Profile View --- +class UserProfileView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data) + + def patch(self, request): + """Update user profile information""" + user = request.user + profile = user.profile + + # Update any allowed fields + allowed_fields = ['net_worth', 'cash_available', 'invested_amount', + 'credit_used', 'credit_limit', 'phone_number'] + + for field in allowed_fields: + if field in request.data: + setattr(profile, field, request.data[field]) + + profile.save() + serializer = UserSerializer(user) + return Response(serializer.data) + +# --- 2. OTP Helper Function --- +def generate_otp(): + return str(random.randint(100000, 999999)) + +# --- 3. Send Email OTP View --- +class SendEmailOTPView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + + # Ensure profile exists + if not hasattr(user, 'profile'): + FinancialProfile.objects.create(user=user) + + # Generate OTP + otp = generate_otp() + user.profile.email_otp = otp + user.profile.save() + + # Send email using SendGrid + try: + subject = 'Aureon - Email Verification Code' + message = f''' +Hello {user.email.split('@')[0]}, + +Your verification code is: {otp} + +This code will expire in 10 minutes. + +If you didn't request this code, please ignore this email. + +Best regards, +Aureon Team + ''' + + send_mail( + subject=subject, + message=message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[user.email], + fail_silently=False, + ) + + return Response({ + 'message': 'OTP sent successfully to your email', + 'email': user.email + }) + + except Exception as e: + return Response({ + 'error': f'Failed to send email: {str(e)}' + }, status=500) + +# --- 4. Verify Email OTP View --- +class VerifyEmailOTPView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + otp = request.data.get('otp') + + if not otp: + return Response({ + 'error': 'OTP is required' + }, status=400) + + profile = request.user.profile + + if profile.email_otp == otp: + # Mark email as verified + profile.is_email_verified = True + profile.email_otp = None # Clear the OTP + profile.save() + + return Response({ + 'message': 'Email verified successfully', + 'is_email_verified': True + }) + else: + return Response({ + 'error': 'Invalid OTP' + }, status=400) + +class GetUserMeView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data) \ No newline at end of file