Add subcription feature 3
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
) => {
|
||||
|
||||
@@ -436,7 +436,6 @@ export {
|
||||
useAuthProfile,
|
||||
useAuthHealth,
|
||||
useVerifyToken,
|
||||
useRegister,
|
||||
useLogin,
|
||||
useRefreshToken,
|
||||
useLogout,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user