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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
0
docs/deployment/gitlab.md
Normal file
0
docs/deployment/gitlab.md
Normal 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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
static/vendor/js/init-alpine.js
vendored
7
static/vendor/js/init-alpine.js
vendored
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user