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
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Response
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
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
|
from app.services.platform_signup_service import platform_signup_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -111,6 +112,7 @@ class CompleteSignupResponse(BaseModel):
|
|||||||
vendor_id: int
|
vendor_id: int
|
||||||
redirect_url: str
|
redirect_url: str
|
||||||
trial_ends_at: 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
|
@router.post("/signup/complete", response_model=CompleteSignupResponse) # public
|
||||||
async def complete_signup(
|
async def complete_signup(
|
||||||
request: CompleteSignupRequest,
|
request: CompleteSignupRequest,
|
||||||
|
response: Response,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> CompleteSignupResponse:
|
) -> CompleteSignupResponse:
|
||||||
"""
|
"""
|
||||||
Complete signup after card collection.
|
Complete signup after card collection.
|
||||||
|
|
||||||
Step 5: Verify SetupIntent, attach payment method, create subscription.
|
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(
|
result = platform_signup_service.complete_signup(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -228,12 +232,27 @@ async def complete_signup(
|
|||||||
setup_intent_id=request.setup_intent_id,
|
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(
|
return CompleteSignupResponse(
|
||||||
success=result.success,
|
success=result.success,
|
||||||
vendor_code=result.vendor_code,
|
vendor_code=result.vendor_code,
|
||||||
vendor_id=result.vendor_id,
|
vendor_id=result.vendor_id,
|
||||||
redirect_url=result.redirect_url,
|
redirect_url=result.redirect_url,
|
||||||
trial_ends_at=result.trial_ends_at,
|
trial_ends_at=result.trial_ends_at,
|
||||||
|
access_token=result.access_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class SignupCompletionResult:
|
|||||||
vendor_id: int
|
vendor_id: int
|
||||||
redirect_url: str
|
redirect_url: str
|
||||||
trial_ends_at: 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)
|
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_id = session.get("user_id")
|
||||||
user = db.query(User).filter(User.id == user_id).first() if user_id else None
|
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
|
# Send welcome email
|
||||||
if user and vendor:
|
if user and vendor:
|
||||||
tier_code = session.get("tier_code", TierCode.ESSENTIAL.value)
|
tier_code = session.get("tier_code", TierCode.ESSENTIAL.value)
|
||||||
@@ -627,6 +641,7 @@ class PlatformSignupService:
|
|||||||
vendor_id=vendor_id,
|
vendor_id=vendor_id,
|
||||||
redirect_url=f"/vendor/{vendor_code}/onboarding",
|
redirect_url=f"/vendor/{vendor_code}/onboarding",
|
||||||
trial_ends_at=trial_ends_at.isoformat(),
|
trial_ends_at=trial_ends_at.isoformat(),
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -511,6 +511,12 @@ function signupWizard() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (response.ok) {
|
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;
|
window.location.href = '/signup/success?vendor_code=' + data.vendor_code;
|
||||||
} else {
|
} else {
|
||||||
alert(data.detail || 'Failed to complete signup');
|
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() {
|
clearTokens() {
|
||||||
apiLog.info('Clearing all authentication tokens...');
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
apiLog.info('Clearing authentication tokens for path:', currentPath);
|
||||||
|
|
||||||
const tokensBefore = {
|
const tokensBefore = {
|
||||||
admin_token: !!localStorage.getItem('admin_token'),
|
admin_token: !!localStorage.getItem('admin_token'),
|
||||||
admin_user: !!localStorage.getItem('admin_user'),
|
admin_user: !!localStorage.getItem('admin_user'),
|
||||||
vendor_token: !!localStorage.getItem('vendor_token'),
|
vendor_token: !!localStorage.getItem('vendor_token'),
|
||||||
vendor_user: !!localStorage.getItem('vendor_user'),
|
vendor_user: !!localStorage.getItem('vendor_user'),
|
||||||
|
customer_token: !!localStorage.getItem('customer_token'),
|
||||||
token: !!localStorage.getItem('token')
|
token: !!localStorage.getItem('token')
|
||||||
};
|
};
|
||||||
apiLog.debug('Tokens before clear:', tokensBefore);
|
apiLog.debug('Tokens before clear:', tokensBefore);
|
||||||
|
|
||||||
|
// 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_token');
|
||||||
localStorage.removeItem('admin_user');
|
localStorage.removeItem('admin_user');
|
||||||
localStorage.removeItem('vendor_token');
|
localStorage.removeItem('vendor_token');
|
||||||
localStorage.removeItem('vendor_user');
|
localStorage.removeItem('vendor_user');
|
||||||
|
localStorage.removeItem('customer_token');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
localStorage.removeItem('vendorCode');
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
const tokensAfter = {
|
const tokensAfter = {
|
||||||
admin_token: !!localStorage.getItem('admin_token'),
|
admin_token: !!localStorage.getItem('admin_token'),
|
||||||
admin_user: !!localStorage.getItem('admin_user'),
|
admin_user: !!localStorage.getItem('admin_user'),
|
||||||
vendor_token: !!localStorage.getItem('vendor_token'),
|
vendor_token: !!localStorage.getItem('vendor_token'),
|
||||||
vendor_user: !!localStorage.getItem('vendor_user'),
|
vendor_user: !!localStorage.getItem('vendor_user'),
|
||||||
|
customer_token: !!localStorage.getItem('customer_token'),
|
||||||
token: !!localStorage.getItem('token')
|
token: !!localStorage.getItem('token')
|
||||||
};
|
};
|
||||||
apiLog.debug('Tokens after clear:', tokensAfter);
|
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) {
|
} catch (error) {
|
||||||
console.error('⚠️ Logout API error (continuing anyway):', error);
|
console.error('⚠️ Logout API error (continuing anyway):', error);
|
||||||
} finally {
|
} finally {
|
||||||
// Clear all tokens and data
|
// Clear vendor tokens only (not admin or customer tokens)
|
||||||
console.log('🧹 Clearing tokens...');
|
console.log('🧹 Clearing vendor tokens...');
|
||||||
localStorage.removeItem('vendor_token');
|
localStorage.removeItem('vendor_token');
|
||||||
|
localStorage.removeItem('vendor_user');
|
||||||
localStorage.removeItem('currentUser');
|
localStorage.removeItem('currentUser');
|
||||||
localStorage.removeItem('vendorCode');
|
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...');
|
console.log('🔄 Redirecting to login...');
|
||||||
window.location.href = `/vendor/${this.vendorCode}/login`;
|
window.location.href = `/vendor/${this.vendorCode}/login`;
|
||||||
|
|||||||
Reference in New Issue
Block a user