From 4298af9f79160027505205f7d4cbcbbe0d51553d Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sun, 28 Dec 2025 18:10:18 +0100 Subject: [PATCH] fix: auto-login after signup and context-aware token clearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the "Authorization header required for API calls" error during vendor onboarding after signup. Changes: - Generate JWT access token on signup completion - Set vendor_token cookie for page navigation - Return access_token in signup response for localStorage - Store vendor_token in localStorage after signup completion - Make clearTokens() context-aware to prevent cross-portal interference - Fix vendor logout to not clear admin/customer tokens ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/api/v1/platform/signup.py | 21 ++++++++++- app/services/platform_signup_service.py | 17 ++++++++- app/templates/platform/signup.html | 6 ++++ docs/deployment/gitlab.md | 0 static/shared/js/api-client.js | 47 ++++++++++++++++++++----- static/vendor/js/init-alpine.js | 7 ++-- 6 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 docs/deployment/gitlab.md diff --git a/app/api/v1/platform/signup.py b/app/api/v1/platform/signup.py index caff2b43..3f63534b 100644 --- a/app/api/v1/platform/signup.py +++ b/app/api/v1/platform/signup.py @@ -14,11 +14,12 @@ All endpoints are public (no authentication required). import logging -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Response from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session from app.core.database import get_db +from app.core.environment import should_use_secure_cookies from app.services.platform_signup_service import platform_signup_service router = APIRouter() @@ -111,6 +112,7 @@ class CompleteSignupResponse(BaseModel): vendor_id: int redirect_url: str trial_ends_at: str + access_token: str | None = None # JWT token for automatic login # ============================================================================= @@ -215,12 +217,14 @@ async def setup_payment(request: SetupPaymentRequest) -> SetupPaymentResponse: @router.post("/signup/complete", response_model=CompleteSignupResponse) # public async def complete_signup( request: CompleteSignupRequest, + response: Response, db: Session = Depends(get_db), ) -> CompleteSignupResponse: """ Complete signup after card collection. Step 5: Verify SetupIntent, attach payment method, create subscription. + Also sets HTTP-only cookie for page navigation and returns token for localStorage. """ result = platform_signup_service.complete_signup( db=db, @@ -228,12 +232,27 @@ async def complete_signup( setup_intent_id=request.setup_intent_id, ) + # Set HTTP-only cookie for page navigation (same as login does) + # This enables the user to access vendor pages immediately after signup + if result.access_token: + response.set_cookie( + key="vendor_token", + value=result.access_token, + httponly=True, # JavaScript cannot access (XSS protection) + secure=should_use_secure_cookies(), # HTTPS only in production/staging + samesite="lax", # CSRF protection + max_age=3600 * 24, # 24 hours + path="/vendor", # RESTRICTED TO VENDOR ROUTES ONLY + ) + logger.info(f"Set vendor_token cookie for new vendor {result.vendor_code}") + return CompleteSignupResponse( success=result.success, vendor_code=result.vendor_code, vendor_id=result.vendor_id, redirect_url=result.redirect_url, trial_ends_at=result.trial_ends_at, + access_token=result.access_token, ) diff --git a/app/services/platform_signup_service.py b/app/services/platform_signup_service.py index 392d5f3d..87c29a70 100644 --- a/app/services/platform_signup_service.py +++ b/app/services/platform_signup_service.py @@ -96,6 +96,7 @@ class SignupCompletionResult: vendor_id: int redirect_url: str trial_ends_at: str + access_token: str | None = None # JWT token for automatic login # ============================================================================= @@ -606,10 +607,23 @@ class PlatformSignupService: else datetime.now(UTC) + timedelta(days=30) ) - # Get user for welcome email + # Get user for welcome email and token generation user_id = session.get("user_id") user = db.query(User).filter(User.id == user_id).first() if user_id else None + # Generate access token for automatic login after signup + access_token = None + if user and vendor: + # Create vendor-scoped JWT token (user is owner since they just signed up) + token_data = self.auth_manager.create_access_token( + user=user, + vendor_id=vendor.id, + vendor_code=vendor.vendor_code, + vendor_role="Owner", # New signup is always the owner + ) + access_token = token_data["access_token"] + logger.info(f"Generated access token for new vendor user {user.email}") + # Send welcome email if user and vendor: tier_code = session.get("tier_code", TierCode.ESSENTIAL.value) @@ -627,6 +641,7 @@ class PlatformSignupService: vendor_id=vendor_id, redirect_url=f"/vendor/{vendor_code}/onboarding", trial_ends_at=trial_ends_at.isoformat(), + access_token=access_token, ) diff --git a/app/templates/platform/signup.html b/app/templates/platform/signup.html index e3abee0e..e79de3e1 100644 --- a/app/templates/platform/signup.html +++ b/app/templates/platform/signup.html @@ -511,6 +511,12 @@ function signupWizard() { const data = await response.json(); if (response.ok) { + // Store access token for automatic login + if (data.access_token) { + localStorage.setItem('vendor_token', data.access_token); + localStorage.setItem('vendorCode', data.vendor_code); + console.log('Vendor token stored for automatic login'); + } window.location.href = '/signup/success?vendor_code=' + data.vendor_code; } else { alert(data.detail || 'Failed to complete signup'); diff --git a/docs/deployment/gitlab.md b/docs/deployment/gitlab.md new file mode 100644 index 00000000..e69de29b diff --git a/static/shared/js/api-client.js b/static/shared/js/api-client.js index 37c252da..62679e58 100644 --- a/static/shared/js/api-client.js +++ b/static/shared/js/api-client.js @@ -233,35 +233,66 @@ class APIClient { } /** - * Clear authentication tokens + * Clear authentication tokens for current context only. + * + * Uses path-based detection to clear only the relevant token: + * - /admin/* paths clear admin_token + * - /vendor/* paths clear vendor_token + * - /shop/* paths clear customer_token + * - Other paths clear all tokens (fallback) */ clearTokens() { - apiLog.info('Clearing all authentication tokens...'); + const currentPath = window.location.pathname; + + apiLog.info('Clearing authentication tokens for path:', currentPath); const tokensBefore = { admin_token: !!localStorage.getItem('admin_token'), admin_user: !!localStorage.getItem('admin_user'), vendor_token: !!localStorage.getItem('vendor_token'), vendor_user: !!localStorage.getItem('vendor_user'), + customer_token: !!localStorage.getItem('customer_token'), token: !!localStorage.getItem('token') }; apiLog.debug('Tokens before clear:', tokensBefore); - localStorage.removeItem('admin_token'); - localStorage.removeItem('admin_user'); - localStorage.removeItem('vendor_token'); - localStorage.removeItem('vendor_user'); - localStorage.removeItem('token'); + // Context-aware token clearing to prevent cross-context interference + if (currentPath.startsWith('/admin/') || currentPath.startsWith('/api/v1/admin/')) { + apiLog.info('Clearing admin tokens only'); + localStorage.removeItem('admin_token'); + localStorage.removeItem('admin_user'); + } else if (currentPath.startsWith('/vendor/') || currentPath.startsWith('/api/v1/vendor/')) { + apiLog.info('Clearing vendor tokens only'); + localStorage.removeItem('vendor_token'); + localStorage.removeItem('vendor_user'); + localStorage.removeItem('currentUser'); + localStorage.removeItem('vendorCode'); + } else if (currentPath.includes('/shop/') || currentPath.startsWith('/api/v1/shop/')) { + apiLog.info('Clearing customer tokens only'); + localStorage.removeItem('customer_token'); + } else { + // Fallback: clear all tokens for unknown paths + apiLog.info('Unknown path context, clearing all tokens'); + localStorage.removeItem('admin_token'); + localStorage.removeItem('admin_user'); + localStorage.removeItem('vendor_token'); + localStorage.removeItem('vendor_user'); + localStorage.removeItem('customer_token'); + localStorage.removeItem('currentUser'); + localStorage.removeItem('vendorCode'); + localStorage.removeItem('token'); + } const tokensAfter = { admin_token: !!localStorage.getItem('admin_token'), admin_user: !!localStorage.getItem('admin_user'), vendor_token: !!localStorage.getItem('vendor_token'), vendor_user: !!localStorage.getItem('vendor_user'), + customer_token: !!localStorage.getItem('customer_token'), token: !!localStorage.getItem('token') }; apiLog.debug('Tokens after clear:', tokensAfter); - apiLog.info('All tokens cleared'); + apiLog.info('Context-specific tokens cleared'); } /** diff --git a/static/vendor/js/init-alpine.js b/static/vendor/js/init-alpine.js index 02fb7822..307546ff 100644 --- a/static/vendor/js/init-alpine.js +++ b/static/vendor/js/init-alpine.js @@ -106,12 +106,13 @@ function data() { } catch (error) { console.error('โš ๏ธ Logout API error (continuing anyway):', error); } finally { - // Clear all tokens and data - console.log('๐Ÿงน Clearing tokens...'); + // Clear vendor tokens only (not admin or customer tokens) + console.log('๐Ÿงน Clearing vendor tokens...'); localStorage.removeItem('vendor_token'); + localStorage.removeItem('vendor_user'); localStorage.removeItem('currentUser'); localStorage.removeItem('vendorCode'); - localStorage.clear(); // Clear everything to be safe + // Note: Do NOT use localStorage.clear() - it would clear admin/customer tokens too console.log('๐Ÿ”„ Redirecting to login...'); window.location.href = `/vendor/${this.vendorCode}/login`;