Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -91,10 +91,21 @@ class ApiClient {
'/tenants/register', // Tenant registration - creating new tenant, no existing tenant context
];
// Additional public endpoints that don't require authentication at all (including registration)
const publicAuthEndpoints = [
'/auth/start-registration', // Registration step 1 - SetupIntent creation
'/auth/complete-registration', // Registration step 2 - Completion after 3DS
'/auth/verify-email', // Email verification
];
const isPublicEndpoint = publicEndpoints.some(endpoint =>
config.url?.includes(endpoint)
);
const isPublicAuthEndpoint = publicAuthEndpoints.some(endpoint =>
config.url?.includes(endpoint)
);
const isNoTenantEndpoint = noTenantEndpoints.some(endpoint =>
config.url?.includes(endpoint)
);
@@ -104,19 +115,19 @@ class ApiClient {
const isDemoMode = !!demoSessionId;
// Only add auth token for non-public endpoints
if (this.authToken && !isPublicEndpoint) {
if (this.authToken && !isPublicEndpoint && !isPublicAuthEndpoint) {
config.headers.Authorization = `Bearer ${this.authToken}`;
console.log('🔑 [API Client] Adding Authorization header for:', config.url);
} else if (!isPublicEndpoint && !isDemoMode) {
} else if (!isPublicEndpoint && !isPublicAuthEndpoint && !isDemoMode) {
// Only warn if NOT in demo mode - demo mode uses X-Demo-Session-Id header instead
console.warn('⚠️ [API Client] No auth token available for:', config.url, 'authToken:', this.authToken ? 'exists' : 'missing');
}
// Add tenant ID only for endpoints that require it
if (this.tenantId && !isPublicEndpoint && !isNoTenantEndpoint) {
if (this.tenantId && !isPublicEndpoint && !isPublicAuthEndpoint && !isNoTenantEndpoint) {
config.headers['X-Tenant-ID'] = this.tenantId;
console.log('🔍 [API Client] Adding X-Tenant-ID header:', this.tenantId, 'for URL:', config.url);
} else if (!isPublicEndpoint && !isNoTenantEndpoint) {
} else if (!isPublicEndpoint && !isPublicAuthEndpoint && !isNoTenantEndpoint) {
console.warn('⚠️ [API Client] No tenant ID set for endpoint:', config.url);
}

View File

@@ -1,5 +1,6 @@
/**
* Auth React Query hooks
* Updated for atomic registration architecture with 3DS support
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { authService } from '../services/auth';
@@ -11,7 +12,10 @@ import {
PasswordReset,
UserResponse,
UserUpdate,
TokenVerification
TokenVerification,
RegistrationStartResponse,
RegistrationCompletionResponse,
RegistrationVerification,
} from '../types/auth';
import { ApiError } from '../client';
import { useAuthStore } from '../../stores/auth.store';
@@ -61,15 +65,15 @@ export const useVerifyToken = (
};
// Mutations
export const useRegister = (
options?: UseMutationOptions<TokenResponse, ApiError, UserRegistration>
export const useStartRegistration = (
options?: UseMutationOptions<RegistrationStartResponse, ApiError, UserRegistration>
) => {
const queryClient = useQueryClient();
return useMutation<TokenResponse, ApiError, UserRegistration>({
mutationFn: (userData: UserRegistration) => authService.register(userData),
return useMutation<RegistrationStartResponse, ApiError, UserRegistration>({
mutationFn: (userData: UserRegistration) => authService.startRegistration(userData),
onSuccess: (data) => {
// Update profile query with new user data
// If no 3DS required, update profile query with new user data
if (data.user) {
queryClient.setQueryData(authKeys.profile(), data.user);
}
@@ -78,6 +82,32 @@ export const useRegister = (
});
};
/**
* Hook for completing registration after 3DS verification
* This is the second step in the atomic registration flow
*/
export const useCompleteRegistration = (
options?: UseMutationOptions<RegistrationCompletionResponse, ApiError, RegistrationVerification>
) => {
const queryClient = useQueryClient();
return useMutation<RegistrationCompletionResponse, ApiError, RegistrationVerification>({
mutationFn: (verificationData: RegistrationVerification) => authService.completeRegistration(verificationData),
onSuccess: (data) => {
// Update profile query with new user data
if (data.user) {
queryClient.setQueryData(authKeys.profile(), data.user);
}
// Invalidate all queries to refresh data
queryClient.invalidateQueries({ queryKey: ['auth'] });
},
...options,
});
};
export const useLogin = (
options?: UseMutationOptions<TokenResponse, ApiError, UserLogin>
) => {

View File

@@ -436,7 +436,6 @@ export {
useAuthProfile,
useAuthHealth,
useVerifyToken,
useRegister,
useLogin,
useRefreshToken,
useLogout,

View File

@@ -2,14 +2,13 @@
// frontend/src/api/services/auth.ts
// ================================================================
/**
* Auth Service - Complete backend alignment
* Auth Service - Atomic Registration Architecture
*
* Backend API structure (3-tier architecture):
* - ATOMIC: users.py
* - OPERATIONS: auth_operations.py, onboarding_progress.py
*
* Last Updated: 2025-10-05
* Status: Complete - Zero drift with backend
* Last Updated: 2025-01-14
* Status: Complete - SetupIntent-first registration flow with 3DS support
*/
import { apiClient } from '../client';
import {
@@ -23,37 +22,42 @@ import {
UserUpdate,
TokenVerificationResponse,
AuthHealthResponse,
RegistrationStartResponse,
RegistrationCompletionResponse,
RegistrationVerification,
} from '../types/auth';
export class AuthService {
private readonly baseUrl = '/auth';
// ===================================================================
// ATOMIC REGISTRATION: SetupIntent-First Approach
// These methods implement the secure registration flow with 3DS support
// ===================================================================
/**
* Start secure registration flow with SetupIntent-first approach
* This is the FIRST step in the atomic registration flow
* Backend: services/auth/app/api/auth_operations.py:start_registration()
*/
async startRegistration(userData: UserRegistration): Promise<RegistrationStartResponse> {
return apiClient.post<RegistrationStartResponse>(`${this.baseUrl}/start-registration`, userData);
}
/**
* Complete registration after 3DS verification
* This is the SECOND step in the atomic registration flow
* Backend: services/auth/app/api/auth_operations.py:complete_registration()
*/
async completeRegistration(verificationData: RegistrationVerification): Promise<RegistrationCompletionResponse> {
return apiClient.post<RegistrationCompletionResponse>(`${this.baseUrl}/complete-registration`, verificationData);
}
// ===================================================================
// OPERATIONS: Authentication
// Backend: services/auth/app/api/auth_operations.py
// ===================================================================
async register(userData: UserRegistration): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/register`, userData);
}
async registerWithSubscription(userData: UserRegistration): Promise<UserRegistrationWithSubscriptionResponse> {
return apiClient.post<UserRegistrationWithSubscriptionResponse>(`${this.baseUrl}/register-with-subscription`, userData);
}
async completeRegistrationAfterSetupIntent(completionData: {
email: string;
password: string;
full_name: string;
setup_intent_id: string;
plan_id: string;
payment_method_id: string;
billing_interval: 'monthly' | 'yearly';
coupon_code?: string;
}): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/complete-registration-after-setup-intent`, completionData);
}
async login(loginData: UserLogin): Promise<TokenResponse> {
return apiClient.post<TokenResponse>(`${this.baseUrl}/login`, loginData);
}
@@ -136,7 +140,7 @@ export class AuthService {
});
}
async getAccountDeletionInfo(): Promise<any> {
async getAccountDeletionInfo(): Promise<Record<string, unknown>> {
return apiClient.get(`${this.baseUrl}/me/account/deletion-info`);
}
@@ -152,15 +156,15 @@ export class AuthService {
analytics_consent?: boolean;
consent_method: string;
consent_version?: string;
}): Promise<any> {
}): Promise<Record<string, unknown>> {
return apiClient.post(`${this.baseUrl}/me/consent`, consentData);
}
async getCurrentConsent(): Promise<any> {
async getCurrentConsent(): Promise<Record<string, unknown>> {
return apiClient.get(`${this.baseUrl}/me/consent/current`);
}
async getConsentHistory(): Promise<any[]> {
async getConsentHistory(): Promise<Record<string, unknown>[]> {
return apiClient.get(`${this.baseUrl}/me/consent/history`);
}
@@ -171,7 +175,7 @@ export class AuthService {
analytics_consent?: boolean;
consent_method: string;
consent_version?: string;
}): Promise<any> {
}): Promise<Record<string, unknown>> {
return apiClient.put(`${this.baseUrl}/me/consent`, consentData);
}
@@ -184,11 +188,11 @@ export class AuthService {
// Backend: services/auth/app/api/data_export.py
// ===================================================================
async exportMyData(): Promise<any> {
async exportMyData(): Promise<Record<string, unknown>> {
return apiClient.get(`${this.baseUrl}/me/export`);
}
async getExportSummary(): Promise<any> {
async getExportSummary(): Promise<Record<string, unknown>> {
return apiClient.get(`${this.baseUrl}/me/export/summary`);
}
@@ -197,11 +201,11 @@ export class AuthService {
// Backend: services/auth/app/api/onboarding_progress.py
// ===================================================================
async getOnboardingProgress(): Promise<any> {
async getOnboardingProgress(): Promise<Record<string, unknown>> {
return apiClient.get(`${this.baseUrl}/me/onboarding/progress`);
}
async updateOnboardingStep(stepName: string, completed: boolean, data?: any): Promise<any> {
async updateOnboardingStep(stepName: string, completed: boolean, data?: Record<string, unknown>): Promise<Record<string, unknown>> {
return apiClient.put(`${this.baseUrl}/me/onboarding/step`, {
step_name: stepName,
completed: completed,
@@ -230,4 +234,4 @@ export class AuthService {
}
}
export const authService = new AuthService();
export const authService = new AuthService();

View File

@@ -1,16 +1,3 @@
// frontend/src/api/types/auth.ts
// ================================================================
/**
* Authentication Type Definitions
*
* Aligned with backend schemas:
* - services/auth/app/schemas/auth.py
* - services/auth/app/schemas/users.py
*
* Last Updated: 2025-10-05
* Status: ✅ Complete - Zero drift with backend
*/
=======
// ================================================================
// frontend/src/api/types/auth.ts
// ================================================================
@@ -21,21 +8,8 @@
* - services/auth/app/schemas/auth.py
* - services/auth/app/schemas/users.py
*
* Last Updated: 2025-10-13
* Status: Complete - Zero drift with backend
* Changes: Removed use_trial, added payment_customer_id and default_payment_method_id
*/================================================================
// frontend/src/api/types/auth.ts
// ================================================================
/**
* Authentication Type Definitions
*
* Aligned with backend schemas:
* - services/auth/app/schemas/auth.py
* - services/auth/app/schemas/users.py
*
* Last Updated: 2025-10-05
* Status: ✅ Complete - Zero drift with backend
* Last Updated: 2025-01-14
* Status: Complete - Atomic registration flow with 3DS support
*/
// ================================================================
@@ -45,6 +19,7 @@
/**
* User registration request
* Backend: services/auth/app/schemas/auth.py:15-29 (UserRegistration)
* Updated for new atomic registration flow with 3DS support
*/
export interface UserRegistration {
email: string; // EmailStr - validated email format
@@ -61,7 +36,7 @@ export interface UserRegistration {
privacy_accepted?: boolean; // Default: true - Accept privacy policy
marketing_consent?: boolean; // Default: false - Consent to marketing communications
analytics_consent?: boolean; // Default: false - Consent to analytics cookies
// NEW: Billing address fields for subscription creation
// Billing address fields for subscription creation
address?: string | null; // Billing address
postal_code?: string | null; // Billing postal code
city?: string | null; // Billing city
@@ -69,24 +44,52 @@ export interface UserRegistration {
}
/**
* User registration with subscription response
* Extended token response for registration with subscription
* Backend: services/auth/app/schemas/auth.py:70-80 (TokenResponse with subscription_id)
* Registration verification data for 3DS completion
* Used in the second step of the atomic registration flow
*/
export interface UserRegistrationWithSubscriptionResponse extends TokenResponse {
subscription_id?: string | null; // ID of the created subscription (returned if subscription was created during registration)
requires_action?: boolean | null; // Whether 3DS/SetupIntent authentication is required
action_type?: string | null; // Type of action required (e.g., 'setup_intent_confirmation')
client_secret?: string | null; // Client secret for SetupIntent/PaymentIntent authentication
payment_intent_id?: string | null; // Payment intent ID (deprecated, use setup_intent_id)
setup_intent_id?: string | null; // SetupIntent ID for 3DS authentication
customer_id?: string | null; // Stripe customer ID (needed for completion)
plan_id?: string | null; // Plan ID (needed for completion)
payment_method_id?: string | null; // Payment method ID (needed for completion)
trial_period_days?: number | null; // Trial period days (needed for completion)
user_id?: string | null; // User ID (needed for completion)
billing_interval?: string | null; // Billing interval (needed for completion)
message?: string | null; // Message explaining what needs to be done
export interface RegistrationVerification {
setup_intent_id: string; // SetupIntent ID from first step
user_data: UserRegistration; // Original user registration data
state_id?: string | null; // Optional registration state ID for tracking
}
/**
* Registration start response (first step)
* Response from /start-registration endpoint
* Backend: services/auth/app/api/auth_operations.py:start_registration()
*/
export interface RegistrationStartResponse {
requires_action: boolean; // Whether 3DS/SetupIntent authentication is required
action_type?: string | null; // Type of action required (e.g., 'setup_intent_confirmation')
client_secret?: string | null; // Client secret for SetupIntent authentication
setup_intent_id?: string | null; // SetupIntent ID for 3DS authentication
customer_id?: string | null; // Stripe customer ID
payment_customer_id?: string | null; // Payment customer ID
plan_id?: string | null; // Plan ID
payment_method_id?: string | null; // Payment method ID
billing_cycle?: string | null; // Billing cycle
email?: string | null; // User email
state_id?: string | null; // Registration state ID for tracking
message?: string | null; // Message explaining what needs to be done
user?: UserData | null; // User data (only if no 3DS required)
subscription_id?: string | null; // Subscription ID (only if no 3DS required)
status?: string | null; // Status (only if no 3DS required)
}
/**
* Registration completion response (second step)
* Response from /complete-registration endpoint
* Backend: services/auth/app/api/auth_operations.py:complete_registration()
*/
export interface RegistrationCompletionResponse {
success: boolean; // Whether registration was successful
user?: UserData | null; // Created user data
subscription_id?: string | null; // Created subscription ID
payment_customer_id?: string | null; // Payment customer ID
status?: string | null; // Subscription status
message?: string | null; // Success/error message
access_token?: string | null; // JWT access token
refresh_token?: string | null; // JWT refresh token
}
/**
@@ -250,6 +253,11 @@ export interface TokenVerification {
message?: string | null;
}
/**
* Token verification response (alias for hooks)
*/
export type TokenVerificationResponse = TokenVerification;
/**
* Password reset response
* Backend: services/auth/app/schemas/auth.py:131-134 (PasswordResetResponse)
@@ -268,6 +276,16 @@ export interface LogoutResponse {
success: boolean; // Default: true
}
/**
* Auth health response
*/
export interface AuthHealthResponse {
status: string;
service: string;
version?: string;
features?: string[];
}
// ================================================================
// ERROR TYPES
// ================================================================
@@ -323,3 +341,24 @@ export interface TokenClaims {
exp: number; // expires at timestamp
iss: string; // issuer - Default: "bakery-auth"
}
// ================================================================
// 3DS Authentication Types
// ================================================================
/**
* Exception thrown when 3DS authentication is required
* This is used in the SetupIntent-first registration flow
*/
export class ThreeDSAuthenticationRequired extends Error {
constructor(
public setup_intent_id: string,
public client_secret: string,
public action_type: string,
message: string = "3DS authentication required",
public extra_data: Record<string, unknown> = {}
) {
super(message);
this.name = "ThreeDSAuthenticationRequired";
}
}