fix: auto-login after signup and context-aware token clearing

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 18:10:18 +01:00
parent 3c403ab1b8
commit 4298af9f79
6 changed files with 85 additions and 13 deletions

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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');

View File

View File

@@ -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');
}
/**

View File

@@ -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`;