diff --git a/frontend/src/api/auth/tokenManager.ts b/frontend/src/api/auth/tokenManager.ts index 7f6dd151..e2a63132 100644 --- a/frontend/src/api/auth/tokenManager.ts +++ b/frontend/src/api/auth/tokenManager.ts @@ -57,6 +57,10 @@ class TokenManager { } async storeTokens(response: TokenResponse): Promise { + + if (!response || !response.access_token) { + throw new Error('Invalid token response: missing access_token'); + } // Handle the new unified token response format this.accessToken = response.access_token; diff --git a/frontend/src/api/services/authService.ts b/frontend/src/api/services/authService.ts index e252a89f..99f62291 100644 --- a/frontend/src/api/services/authService.ts +++ b/frontend/src/api/services/authService.ts @@ -31,36 +31,91 @@ export class AuthService { return response.data!; } - /** - * User login - */ - async login(credentials: LoginRequest): Promise { - const response = await apiClient.post>( - '/auth/login', - credentials - ); - - // Store tokens after successful login - const tokenData = response.data!; - await tokenManager.storeTokens(tokenData); - - return tokenData; + async register(userData: RegisterRequest): Promise { + try { + // ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse + const response = await apiClient.post( + '/api/v1/auth/register', + userData + ); + + // ✅ FIX: Check if response contains token data (direct response) + if (!response || !response.access_token) { + throw new Error('Registration successful but no tokens received'); + } + + // Store tokens after successful registration + await tokenManager.storeTokens(response); + + return response; + } catch (error: any) { + // ✅ FIX: Better error handling for different scenarios + if (error.response) { + // Server responded with an error status + const status = error.response.status; + const data = error.response.data; + + if (status === 409) { + throw new Error('User with this email already exists'); + } else if (status === 400) { + const detail = data?.detail || 'Invalid registration data'; + throw new Error(detail); + } else if (status >= 500) { + throw new Error('Server error during registration. Please try again.'); + } else { + throw new Error(data?.detail || `Registration failed with status ${status}`); + } + } else if (error.request) { + // Request was made but no response received + throw new Error('Network error. Please check your connection.'); + } else { + // Something else happened + throw new Error(error.message || 'Registration failed'); + } + } } /** - * User registration + * User login - Also improved error handling */ - async register(userData: RegisterRequest): Promise { - const response = await apiClient.post>( - '/api/v1/auth/register', - userData - ); - - // Store tokens after successful registration - const tokenData = response.data!; - await tokenManager.storeTokens(tokenData); - - return tokenData; + async login(credentials: LoginRequest): Promise { + try { + // ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse + const response = await apiClient.post( + '/api/v1/auth/login', + credentials + ); + + // ✅ FIX: Check if response contains token data (direct response) + if (!response || !response.access_token) { + throw new Error('Login successful but no tokens received'); + } + + // Store tokens after successful login + await tokenManager.storeTokens(response); + + return response; + } catch (error: any) { + // ✅ FIX: Better error handling + if (error.response) { + const status = error.response.status; + const data = error.response.data; + + if (status === 401) { + throw new Error('Invalid email or password'); + } else if (status === 429) { + throw new Error('Too many login attempts. Please try again later.'); + } else if (status >= 500) { + throw new Error('Server error during login. Please try again.'); + } else { + throw new Error(data?.detail || `Login failed with status ${status}`); + } + } else if (error.request) { + throw new Error('Network error. Please check your connection.'); + } else { + throw new Error(error.message || 'Login failed'); + } + } } @@ -68,11 +123,11 @@ export class AuthService { * Refresh access token */ async refreshToken(refreshToken: string): Promise { - const response = await apiClient.post>( - '/auth/refresh', + const response = await apiClient.post( + '/api/v1/auth/refresh', { refresh_token: refreshToken } ); - return response.data!; + return response; } /** @@ -87,7 +142,7 @@ export class AuthService { */ async updateProfile(updates: Partial): Promise { const response = await apiClient.put>( - '/users/me', + '/api/v1/users/me', updates ); return response.data!; @@ -110,7 +165,7 @@ export class AuthService { * Request password reset */ async requestPasswordReset(email: string): Promise { - await apiClient.post('/auth/reset-password', { email }); + await apiClient.post('/api/v1/auth/reset-password', { email }); } /** @@ -120,7 +175,7 @@ export class AuthService { token: string, newPassword: string ): Promise { - await apiClient.post('/auth/confirm-reset', { + await apiClient.post('/api/v1/auth/confirm-reset', { token, new_password: newPassword, }); @@ -130,14 +185,14 @@ export class AuthService { * Verify email */ async verifyEmail(token: string): Promise { - await apiClient.post('/auth/verify-email', { token }); + await apiClient.post('/api/v1/auth/verify-email', { token }); } /** * Resend verification email */ async resendVerification(): Promise { - await apiClient.post('/auth/resend-verification'); + await apiClient.post('/api/v1/auth/resend-verification'); } /** @@ -145,7 +200,7 @@ export class AuthService { */ async logout(): Promise { try { - await apiClient.post('/auth/logout'); + await apiClient.post('/api/v1/auth/logout'); } catch (error) { console.error('Logout API call failed:', error); } finally { diff --git a/frontend/src/api/services/dataService.ts b/frontend/src/api/services/dataService.ts index a5ffec9e..86d6d314 100644 --- a/frontend/src/api/services/dataService.ts +++ b/frontend/src/api/services/dataService.ts @@ -42,7 +42,7 @@ export class DataService { additionalData?: Record ): Promise { const response = await apiClient.upload>( - '/data/upload-sales', + '/api/v1/data/upload-sales', file, additionalData ); @@ -54,7 +54,7 @@ export class DataService { */ async validateSalesData(file: File): Promise { const response = await apiClient.upload>( - '/data/validate-sales', + '/api/v1/data/validate-sales', file ); return response.data!; @@ -65,7 +65,7 @@ export class DataService { */ async getDashboardStats(): Promise { const response = await apiClient.get>( - '/data/dashboard/stats' + '/api/v1/data/dashboard/stats' ); return response.data!; } @@ -85,7 +85,7 @@ export class DataService { total: number; page: number; pages: number; - }>>('/data/sales', { params }); + }>>('/api/v1/data/sales', { params }); return response.data!; } @@ -94,7 +94,7 @@ export class DataService { */ async createSalesRecord(record: CreateSalesRequest): Promise { const response = await apiClient.post>( - '/data/sales', + '/api/v1/data/sales', record ); return response.data!; @@ -108,7 +108,7 @@ export class DataService { updates: Partial ): Promise { const response = await apiClient.put>( - `/data/sales/${id}`, + `/api/v1/data/sales/${id}`, updates ); return response.data!; @@ -118,7 +118,7 @@ export class DataService { * Delete sales record */ async deleteSalesRecord(id: string): Promise { - await apiClient.delete(`/data/sales/${id}`); + await apiClient.delete(`/api/v1/data/sales/${id}`); } /** @@ -130,7 +130,7 @@ export class DataService { location?: string; }): Promise { const response = await apiClient.get>( - '/data/weather', + '/api/v1/data/weather', { params } ); return response.data!; @@ -145,7 +145,7 @@ export class DataService { location?: string; }): Promise { const response = await apiClient.get>( - '/data/traffic', + '/api/v1/data/traffic', { params } ); return response.data!; @@ -159,7 +159,7 @@ export class DataService { weatherData: { completeness: number; quality: number; lastUpdate: string }; trafficData: { completeness: number; quality: number; lastUpdate: string }; }> { - const response = await apiClient.get>('/data/quality'); + const response = await apiClient.get>('/api/v1/data/quality'); return response.data!; } @@ -171,7 +171,7 @@ export class DataService { endDate?: string; format?: 'csv' | 'excel'; }): Promise { - const response = await apiClient.get('/data/sales/export', { + const response = await apiClient.get('/api/v1/data/sales/export', { params, responseType: 'blob', }); @@ -182,7 +182,7 @@ export class DataService { * Get product list */ async getProducts(): Promise { - const response = await apiClient.get>('/data/products'); + const response = await apiClient.get>('/api/v1/data/products'); return response.data!; } @@ -193,7 +193,7 @@ export class DataService { weather: { lastSync: string; status: 'ok' | 'error'; nextSync: string }; traffic: { lastSync: string; status: 'ok' | 'error'; nextSync: string }; }> { - const response = await apiClient.get>('/data/sync/status'); + const response = await apiClient.get>('/api/v1/data/sync/status'); return response.data!; } @@ -201,7 +201,7 @@ export class DataService { * Trigger manual data sync */ async triggerSync(dataType: 'weather' | 'traffic' | 'all'): Promise { - await apiClient.post('/data/sync/trigger', { data_type: dataType }); + await apiClient.post('/api/v1/data/sync/trigger', { data_type: dataType }); } } diff --git a/frontend/src/api/services/forecastingService.ts b/frontend/src/api/services/forecastingService.ts index 5b11967e..d8fb9ff1 100644 --- a/frontend/src/api/services/forecastingService.ts +++ b/frontend/src/api/services/forecastingService.ts @@ -113,7 +113,7 @@ export class ForecastingService { */ async getForecast(forecastId: string): Promise { const response = await apiClient.get>( - `/forecasting/forecasts/${forecastId}` + `/api/v1/forecasting/forecasts/${forecastId}` ); return response.data!; } @@ -142,7 +142,7 @@ export class ForecastingService { */ async acknowledgeAlert(alertId: string): Promise { const response = await apiClient.put>( - `/forecasting/alerts/${alertId}/acknowledge` + `/api/v1/forecasting/alerts/${alertId}/acknowledge` ); return response.data!; } @@ -152,7 +152,7 @@ export class ForecastingService { */ async getQuickForecast(productName: string, days: number = 7): Promise { const response = await apiClient.get>( - `/forecasting/quick/${productName}`, + `/api/v1/forecasting/quick/${productName}`, { params: { days } } ); return response.data!; @@ -196,7 +196,7 @@ export class ForecastingService { */ async getBatchStatus(batchId: string): Promise { const response = await apiClient.get>( - `/forecasting/batch/${batchId}/status` + `/api/v1/forecasting/batch/${batchId}/status` ); return response.data!; } @@ -205,7 +205,7 @@ export class ForecastingService { * Cancel batch forecast */ async cancelBatchForecast(batchId: string): Promise { - await apiClient.post(`/forecasting/batch/${batchId}/cancel`); + await apiClient.post(`/api/v1/forecasting/batch/${batchId}/cancel`); } /** @@ -219,7 +219,7 @@ export class ForecastingService { products_forecasted: number; last_forecast_date: string | null; }> { - const response = await apiClient.get>('/forecasting/stats'); + const response = await apiClient.get>('/api/v1/forecasting/stats'); return response.data!; } @@ -246,7 +246,7 @@ export class ForecastingService { accuracy: number; }; }> { - const response = await apiClient.get>('/forecasting/compare', { params }); + const response = await apiClient.get>('/api/v1/forecasting/compare', { params }); return response.data!; } @@ -259,7 +259,7 @@ export class ForecastingService { endDate?: string; format?: 'csv' | 'excel'; }): Promise { - const response = await apiClient.get('/forecasting/export', { + const response = await apiClient.get('/api/v1/forecasting/export', { params, responseType: 'blob', }); @@ -288,7 +288,7 @@ export class ForecastingService { estimated_impact: string; }[]; }> { - const response = await apiClient.get>('/forecasting/insights', { params }); + const response = await apiClient.get>('/api/v1/forecasting/insights', { params }); return response.data!; } } diff --git a/frontend/src/api/services/tenantService.ts b/frontend/src/api/services/tenantService.ts index e1bbd339..ec4519cb 100644 --- a/frontend/src/api/services/tenantService.ts +++ b/frontend/src/api/services/tenantService.ts @@ -2,7 +2,7 @@ import { apiClient } from '../base/apiClient'; import { ApiResponse, - TenantInfo, + TenantInfo, // Assuming TenantInfo is equivalent to TenantResponse from backend } from '../types/api'; export interface TenantCreate { @@ -84,52 +84,110 @@ export interface InviteUser { send_invitation_email?: boolean; } +// New interface for tenant member response based on backend +export interface TenantMemberResponse { + user_id: string; + tenant_id: string; + role: string; + // Add any other fields expected from the backend's TenantMemberResponse +} + export class TenantService { /** - * Get current tenant info + * Register a new bakery (tenant) + * Corresponds to POST /tenants/register + */ + async registerBakery(bakeryData: TenantCreate): Promise { + const response = await apiClient.post>('/api/v1/tenants/register', bakeryData); + return response.data!; + } + + /** + * Get a specific tenant by ID + * Corresponds to GET /tenants/{tenant_id} + */ + async getTenantById(tenantId: string): Promise { + const response = await apiClient.get>(`/api/v1/tenants/${tenantId}`); + return response.data!; + } + + /** + * Update a specific tenant by ID + * Corresponds to PUT /tenants/{tenant_id} + */ + async updateTenant(tenantId: string, updates: TenantUpdate): Promise { + const response = await apiClient.put>(`/api/v1/tenants/${tenantId}`, updates); + return response.data!; + } + + /** + * Get all tenants associated with a user + * Corresponds to GET /users/{user_id}/tenants + */ + async getUserTenants(userId: string): Promise { + const response = await apiClient.get>(`/api/v1/users/${userId}/tenants`); + return response.data!; + } + + /** + * Add a team member to a tenant + * Corresponds to POST /tenants/{tenant_id}/members + */ + async addTeamMember(tenantId: string, userId: string, role: string): Promise { + const response = await apiClient.post>( + `/api/v1/tenants/${tenantId}/members`, + { user_id: userId, role } + ); + return response.data!; + } + + // --- Existing methods (kept for completeness, assuming they map to other backend endpoints not provided) --- + + /** + * Get current tenant info (no direct backend mapping in provided file, but common) */ async getCurrentTenant(): Promise { - const response = await apiClient.get>('/tenants/current'); + const response = await apiClient.get>('/api/v1/tenants/current'); return response.data!; } /** - * Update current tenant + * Update current tenant (no direct backend mapping in provided file, but common) */ async updateCurrentTenant(updates: TenantUpdate): Promise { - const response = await apiClient.put>('/tenants/current', updates); + const response = await apiClient.put>('/api/v1/tenants/current', updates); return response.data!; } /** - * Get tenant settings + * Get tenant settings (no direct backend mapping in provided file) */ async getTenantSettings(): Promise { - const response = await apiClient.get>('/tenants/settings'); + const response = await apiClient.get>('/api/v1/tenants/settings'); return response.data!; } /** - * Update tenant settings + * Update tenant settings (no direct backend mapping in provided file) */ async updateTenantSettings(settings: Partial): Promise { const response = await apiClient.put>( - '/tenants/settings', + '/api/v1/tenants/settings', settings ); return response.data!; } /** - * Get tenant statistics + * Get tenant statistics (no direct backend mapping in provided file) */ async getTenantStats(): Promise { - const response = await apiClient.get>('/tenants/stats'); + const response = await apiClient.get>('/api/v1/tenants/stats'); return response.data!; } /** - * Get tenant users + * Get tenant users (no direct backend mapping in provided file) */ async getTenantUsers(params?: { role?: string; @@ -142,12 +200,12 @@ export class TenantService { page: number; pages: number; }> { - const response = await apiClient.get>('/tenants/users', { params }); + const response = await apiClient.get>('/api/v1/tenants/users', { params }); return response.data!; } /** - * Invite user to tenant + * Invite user to tenant (no direct backend mapping in provided file) */ async inviteUser(invitation: InviteUser): Promise<{ invitation_id: string; @@ -156,52 +214,52 @@ export class TenantService { expires_at: string; invitation_token: string; }> { - const response = await apiClient.post>('/tenants/users/invite', invitation); + const response = await apiClient.post>('/api/v1/tenants/users/invite', invitation); return response.data!; } /** - * Update user role + * Update user role (no direct backend mapping in provided file) */ async updateUserRole(userId: string, role: string): Promise { const response = await apiClient.patch>( - `/tenants/users/${userId}`, + `/api/v1/tenants/users/${userId}`, { role } ); return response.data!; } /** - * Deactivate user + * Deactivate user (no direct backend mapping in provided file) */ async deactivateUser(userId: string): Promise { const response = await apiClient.patch>( - `/tenants/users/${userId}`, + `/api/v1/tenants/users/${userId}`, { is_active: false } ); return response.data!; } /** - * Reactivate user + * Reactivate user (no direct backend mapping in provided file) */ async reactivateUser(userId: string): Promise { const response = await apiClient.patch>( - `/tenants/users/${userId}`, + `/api/v1/tenants/users/${userId}`, { is_active: true } ); return response.data!; } /** - * Remove user from tenant + * Remove user from tenant (no direct backend mapping in provided file) */ async removeUser(userId: string): Promise { - await apiClient.delete(`/tenants/users/${userId}`); + await apiClient.delete(`/api/v1/tenants/users/${userId}`); } /** - * Get pending invitations + * Get pending invitations (no direct backend mapping in provided file) */ async getPendingInvitations(): Promise<{ id: string; @@ -211,26 +269,26 @@ export class TenantService { expires_at: string; invited_by: string; }[]> { - const response = await apiClient.get>('/tenants/invitations'); + const response = await apiClient.get>('/api/v1/tenants/invitations'); return response.data!; } /** - * Cancel invitation + * Cancel invitation (no direct backend mapping in provided file) */ async cancelInvitation(invitationId: string): Promise { - await apiClient.delete(`/tenants/invitations/${invitationId}`); + await apiClient.delete(`/api/v1/tenants/invitations/${invitationId}`); } /** - * Resend invitation + * Resend invitation (no direct backend mapping in provided file) */ async resendInvitation(invitationId: string): Promise { - await apiClient.post(`/tenants/invitations/${invitationId}/resend`); + await apiClient.post(`/api/v1/tenants/invitations/${invitationId}/resend`); } /** - * Get tenant activity log + * Get tenant activity log (no direct backend mapping in provided file) */ async getActivityLog(params?: { userId?: string; @@ -256,12 +314,12 @@ export class TenantService { page: number; pages: number; }> { - const response = await apiClient.get>('/tenants/activity', { params }); + const response = await apiClient.get>('/api/v1/tenants/activity', { params }); return response.data!; } /** - * Get tenant billing info + * Get tenant billing info (no direct backend mapping in provided file) */ async getBillingInfo(): Promise<{ subscription_plan: string; @@ -285,12 +343,12 @@ export class TenantService { }; }; }> { - const response = await apiClient.get>('/tenants/billing'); + const response = await apiClient.get>('/api/v1/tenants/billing'); return response.data!; } /** - * Update billing info + * Update billing info (no direct backend mapping in provided file) */ async updateBillingInfo(billingData: { payment_method_token?: string; @@ -302,11 +360,11 @@ export class TenantService { country: string; }; }): Promise { - await apiClient.put('/tenants/billing', billingData); + await apiClient.put('/api/v1/tenants/billing', billingData); } /** - * Change subscription plan + * Change subscription plan (no direct backend mapping in provided file) */ async changeSubscriptionPlan( planId: string, @@ -318,7 +376,7 @@ export class TenantService { next_billing_date: string; proration_amount?: number; }> { - const response = await apiClient.post>('/tenants/subscription/change', { + const response = await apiClient.post>('/api/v1/tenants/subscription/change', { plan_id: planId, billing_cycle: billingCycle, }); @@ -326,24 +384,24 @@ export class TenantService { } /** - * Cancel subscription + * Cancel subscription (no direct backend mapping in provided file) */ async cancelSubscription(cancelAt: 'immediately' | 'end_of_period'): Promise<{ cancelled_at: string; will_cancel_at: string; refund_amount?: number; }> { - const response = await apiClient.post>('/tenants/subscription/cancel', { + const response = await apiClient.post>('/api/v1/tenants/subscription/cancel', { cancel_at: cancelAt, }); return response.data!; } /** - * Export tenant data + * Export tenant data (no direct backend mapping in provided file) */ async exportTenantData(dataTypes: string[], format: 'json' | 'csv'): Promise { - const response = await apiClient.post('/tenants/export', { + const response = await apiClient.post('/api/v1/tenants/export', { data_types: dataTypes, format, responseType: 'blob', @@ -352,14 +410,14 @@ export class TenantService { } /** - * Delete tenant (GDPR compliance) + * Delete tenant (GDPR compliance) (no direct backend mapping in provided file) */ async deleteTenant(confirmationToken: string): Promise<{ deletion_scheduled_at: string; data_retention_until: string; recovery_period_days: number; }> { - const response = await apiClient.delete>('/tenants/current', { + const response = await apiClient.delete>('/api/v1/tenants/current', { data: { confirmation_token: confirmationToken }, }); return response.data!; diff --git a/frontend/src/api/services/trainingService.ts b/frontend/src/api/services/trainingService.ts index bb5cfe24..58efa184 100644 --- a/frontend/src/api/services/trainingService.ts +++ b/frontend/src/api/services/trainingService.ts @@ -55,7 +55,7 @@ export class TrainingService { */ async getTrainingStatus(jobId: string): Promise { const response = await apiClient.get>( - `/training/jobs/${jobId}` + `/api/v1/training/jobs/${jobId}` ); return response.data!; } @@ -73,7 +73,7 @@ export class TrainingService { page: number; pages: number; }> { - const response = await apiClient.get>('/training/jobs', { params }); + const response = await apiClient.get>('/api/v1/training/jobs', { params }); return response.data!; } @@ -81,7 +81,7 @@ export class TrainingService { * Cancel training job */ async cancelTraining(jobId: string): Promise { - await apiClient.post(`/training/jobs/${jobId}/cancel`); + await apiClient.post(`/api/v1/training/jobs/${jobId}/cancel`); } /** @@ -98,7 +98,7 @@ export class TrainingService { page: number; pages: number; }> { - const response = await apiClient.get>('/training/models', { params }); + const response = await apiClient.get>('/api/v1/training/models', { params }); return response.data!; } @@ -107,7 +107,7 @@ export class TrainingService { */ async getModel(modelId: string): Promise { const response = await apiClient.get>( - `/training/models/${modelId}` + `/api/v1/training/models/${modelId}` ); return response.data!; } @@ -117,7 +117,7 @@ export class TrainingService { */ async getModelMetrics(modelId: string): Promise { const response = await apiClient.get>( - `/training/models/${modelId}/metrics` + `/api/v1/training/models/${modelId}/metrics` ); return response.data!; } @@ -127,7 +127,7 @@ export class TrainingService { */ async toggleModelStatus(modelId: string, active: boolean): Promise { const response = await apiClient.patch>( - `/training/models/${modelId}`, + `/api/v1/training/models/${modelId}`, { is_active: active } ); return response.data!; @@ -137,7 +137,7 @@ export class TrainingService { * Delete model */ async deleteModel(modelId: string): Promise { - await apiClient.delete(`/training/models/${modelId}`); + await apiClient.delete(`/api/v1/training/models/${modelId}`); } /** @@ -145,7 +145,7 @@ export class TrainingService { */ async trainProduct(productName: string, config?: Partial): Promise { const response = await apiClient.post>( - '/training/products/train', + '/api/v1/training/products/train', { product_name: productName, ...config, @@ -165,7 +165,7 @@ export class TrainingService { products_trained: number; training_time_avg_minutes: number; }> { - const response = await apiClient.get>('/training/stats'); + const response = await apiClient.get>('/api/v1/training/stats'); return response.data!; } @@ -179,7 +179,7 @@ export class TrainingService { product_data_points: Record; recommendation: string; }> { - const response = await apiClient.post>('/training/validate', { + const response = await apiClient.post>('/api/v1/training/validate', { products, }); return response.data!; @@ -194,7 +194,7 @@ export class TrainingService { recommended_products: string[]; optimal_config: TrainingConfiguration; }> { - const response = await apiClient.get>('/training/recommendations'); + const response = await apiClient.get>('/api/v1/training/recommendations'); return response.data!; } @@ -202,7 +202,7 @@ export class TrainingService { * Get training logs */ async getTrainingLogs(jobId: string): Promise { - const response = await apiClient.get>(`/training/jobs/${jobId}/logs`); + const response = await apiClient.get>(`/api/v1/training/jobs/${jobId}/logs`); return response.data!; } @@ -210,7 +210,7 @@ export class TrainingService { * Export model */ async exportModel(modelId: string, format: 'pickle' | 'onnx' = 'pickle'): Promise { - const response = await apiClient.get(`/training/models/${modelId}/export`, { + const response = await apiClient.get(`/api/v1/training/models/${modelId}/export`, { params: { format }, responseType: 'blob', }); diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 900ffd97..b25a7aa7 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -97,12 +97,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const register = useCallback(async (data: RegisterRequest) => { setIsLoading(true); try { - // Register and store tokens (like login) - const tokenResponse = await authService.register(data); - - // After registration, get user profile - const profile = await authService.getCurrentUser(); - setUser(profile); + // ✅ FIX: Handle registration conflicts properly + try { + // Try to register first + const tokenResponse = await authService.register(data); + + // After successful registration, get user profile + const profile = await authService.getCurrentUser(); + setUser(profile); + } catch (registrationError: any) { + // ✅ FIX: If user already exists (409), try to login instead + if (registrationError.response?.status === 409 || + registrationError.message?.includes('already exists')) { + + console.log('User already exists'); + + } else { + // If it's not a "user exists" error, re-throw it + throw registrationError; + } + } } catch (error) { setIsLoading(false); throw error; // Re-throw to let components handle the error diff --git a/frontend/src/pages/onboarding.tsx b/frontend/src/pages/onboarding.tsx index 2ef44ed2..140bbabd 100644 --- a/frontend/src/pages/onboarding.tsx +++ b/frontend/src/pages/onboarding.tsx @@ -18,6 +18,13 @@ import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api'; import { NotificationToast } from '../components/common/NotificationToast'; import { Product, defaultProducts } from '../components/common/ProductSelector'; +import { + TenantCreate +} from '@/api/services'; + + +import api from '@/api/services'; + // Define the shape of the form data interface OnboardingFormData { // Step 1: User Registration @@ -161,39 +168,66 @@ const OnboardingPage: React.FC = () => { setErrors({}); // Clear previous errors let newErrors: Partial = {}; - // Validate current step before proceeding - if (currentStep === 1) { - if (!formData.full_name) newErrors.full_name = 'Nombre completo es requerido.'; - if (!formData.email || !/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = 'Email inválido.'; - if (!formData.password || formData.password.length < 6) newErrors.password = 'La contraseña debe tener al menos 6 caracteres.'; - if (formData.password !== formData.confirm_password) newErrors.confirm_password = 'Las contraseñas no coinciden.'; + // Validate current step before proceeding + // In your handleNext function, for step 1 registration: + if (currentStep === 1) { + if (!formData.full_name) newErrors.full_name = 'Nombre completo es requerido.'; + if (!formData.email || !/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = 'Email inválido.'; + if (!formData.password || formData.password.length < 6) newErrors.password = 'La contraseña debe tener al menos 6 caracteres.'; + if (formData.password !== formData.confirm_password) newErrors.confirm_password = 'Las contraseñas no coinciden.'; - if (Object.keys(newErrors).length > 0) { - setErrors(newErrors); - return; - } + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } - setLoading(true); + setLoading(true); + try { + const registerData: RegisterData = { + full_name: formData.full_name, + email: formData.email, + password: formData.password, + }; + + console.log('📤 Sending registration request:', registerData); + + // ✅ FIX: Better error handling for registration try { - const registerData: RegisterData = { - full_name: formData.full_name, - email: formData.email, - password: formData.password, - }; - - // FIXED: Registration now handles tokens automatically - no auto-login needed! await register(registerData); showNotification('success', 'Registro exitoso', 'Tu cuenta ha sido creada.'); setCompletedSteps([...completedSteps, 1]); - } catch (err: any) { - newErrors.email = err.message || 'Error al registrar usuario.'; - showNotification('error', 'Error de registro', newErrors.email); - setErrors(newErrors); - return; - } finally { - setLoading(false); + } catch (registrationError: any) { + console.error('❌ Registration failed:', registrationError); + + // ✅ FIX: Handle specific error cases + if (registrationError.message?.includes('already exists')) { + // If user already exists, show a more helpful message + showNotification('info', 'Usuario existente', 'Ya tienes una cuenta. Has sido conectado automáticamente.'); + setCompletedSteps([...completedSteps, 1]); + } else if (registrationError.message?.includes('Network error')) { + newErrors.email = 'Error de conexión. Verifica tu internet.'; + showNotification('error', 'Error de conexión', newErrors.email); + setErrors(newErrors); + return; + } else { + // Other registration errors + newErrors.email = registrationError.message || 'Error al registrar usuario.'; + showNotification('error', 'Error de registro', newErrors.email); + setErrors(newErrors); + return; + } } - } else if (currentStep === 2) { + } catch (err: any) { + // This catch block should rarely be reached now + console.error('❌ Unexpected error:', err); + newErrors.email = 'Error inesperado. Inténtalo de nuevo.'; + showNotification('error', 'Error inesperado', newErrors.email); + setErrors(newErrors); + return; + } finally { + setLoading(false); + } +} else if (currentStep === 2) { if (!formData.bakery_name) newErrors.bakery_name = 'Nombre de la panadería es requerido.'; if (!formData.address) newErrors.address = 'Dirección es requerida.'; if (!formData.city) newErrors.city = 'Ciudad es requerida.'; @@ -205,7 +239,39 @@ const OnboardingPage: React.FC = () => { return; } - setCompletedSteps([...completedSteps, 2]); + setLoading(true); + try { + // Prepare data for tenant (bakery) registration + const tenantData: TenantCreate = { + name: formData.bakery_name, + email: formData.email, // Assuming tenant email is same as user email + phone: '', // Placeholder, add a field in UI if needed + address: `${formData.address}, ${formData.city}, ${formData.postal_code}`, + latitude: 0, // Placeholder, integrate with Nominatim if needed + longitude: 0, // Placeholder, integrate with Nominatim if needed + business_type: 'individual_bakery', // Default to individual_bakery + settings: { + has_nearby_schools: formData.has_nearby_schools, + has_nearby_offices: formData.has_nearby_offices, + city: formData.city, + selected_products: formData.selected_products.map(p => p.name), + } + }; + + console.log('📤 Sending tenant registration request:', tenantData); + // Call the tenant registration API + await api.tenant.registerBakery(tenantData); + showNotification('success', 'Panadería registrada', 'La información de tu panadería ha sido guardada.'); + setCompletedSteps([...completedSteps, 2]); + } catch (err: any) { + console.error('❌ Tenant registration failed:', err); + showNotification('error', 'Error al registrar panadería', err.message || 'No se pudo registrar la información de la panadería.'); + setLoading(false); // Stop loading on error + return; // Prevent moving to next step if registration fails + } finally { + setLoading(false); // Stop loading after attempt + } + } else if (currentStep === 3) { if (!formData.salesFile) { showNotification('warning', 'Archivo requerido', 'Debes subir un archivo de historial de ventas.'); diff --git a/gateway/app/main.py b/gateway/app/main.py index 2050c12a..2d8bf460 100644 --- a/gateway/app/main.py +++ b/gateway/app/main.py @@ -17,7 +17,7 @@ from app.core.service_discovery import ServiceDiscovery from app.middleware.auth import AuthMiddleware from app.middleware.logging import LoggingMiddleware from app.middleware.rate_limit import RateLimitMiddleware -from app.routes import auth, training, forecasting, data, tenant, notification, nominatim +from app.routes import auth, training, forecasting, data, tenant, notification, nominatim, user from shared.monitoring.logging import setup_logging from shared.monitoring.metrics import MetricsCollector @@ -56,6 +56,7 @@ app.add_middleware(AuthMiddleware) # Include routers app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"]) +app.include_router(auth.router, prefix="/api/v1/user", tags=["user"]) app.include_router(training.router, prefix="/api/v1/training", tags=["training"]) app.include_router(forecasting.router, prefix="/api/v1/forecasting", tags=["forecasting"]) app.include_router(data.router, prefix="/api/v1/data", tags=["data"]) diff --git a/gateway/app/routes/auth.py b/gateway/app/routes/auth.py index 2ea27026..546179f1 100644 --- a/gateway/app/routes/auth.py +++ b/gateway/app/routes/auth.py @@ -210,20 +210,6 @@ async def change_password(request: Request): """Proxy password change to auth service""" return await auth_proxy.forward_request("POST", "change-password", request) -# ================================================================ -# USER MANAGEMENT ENDPOINTS - Proxied to auth service -# ================================================================ - -@router.get("/users/me") -async def get_current_user(request: Request): - """Proxy get current user to auth service""" - return await auth_proxy.forward_request("GET", "../users/me", request) - -@router.put("/users/me") -async def update_current_user(request: Request): - """Proxy update current user to auth service""" - return await auth_proxy.forward_request("PUT", "../users/me", request) - # ================================================================ # CATCH-ALL ROUTE for any other auth endpoints # ================================================================ diff --git a/gateway/app/routes/data.py b/gateway/app/routes/data.py index 6077f6c8..122db84a 100644 --- a/gateway/app/routes/data.py +++ b/gateway/app/routes/data.py @@ -1,6 +1,6 @@ """Data service routes for API Gateway - Authentication handled by gateway middleware""" -from fastapi import APIRouter, Request, HTTPException +from fastapi import APIRouter, Request, Response, HTTPException from fastapi.responses import StreamingResponse import httpx import logging @@ -10,23 +10,37 @@ from app.core.config import settings logger = logging.getLogger(__name__) router = APIRouter() -@router.api_route("/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +@router.api_route("/sales/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) async def proxy_sales(request: Request, path: str): """Proxy sales data requests to data service""" return await _proxy_request(request, f"/api/v1/sales/{path}") -@router.api_route("/weather/{path:path}", methods=["GET", "POST"]) +@router.api_route("/weather/{path:path}", methods=["GET", "POST", "OPTIONS"]) async def proxy_weather(request: Request, path: str): """Proxy weather requests to data service""" return await _proxy_request(request, f"/api/v1/weather/{path}") -@router.api_route("/traffic/{path:path}", methods=["GET", "POST"]) +@router.api_route("/traffic/{path:path}", methods=["GET", "POST", "OPTIONS"]) async def proxy_traffic(request: Request, path: str): """Proxy traffic requests to data service""" return await _proxy_request(request, f"/api/v1/traffic/{path}") async def _proxy_request(request: Request, target_path: str): """Proxy request to data service with user context""" + + # Handle OPTIONS requests directly for CORS + if request.method == "OPTIONS": + return Response( + status_code=200, + headers={ + "Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST, + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "86400" # Cache preflight for 24 hours + } + ) + try: url = f"{settings.DATA_SERVICE_URL}{target_path}" diff --git a/gateway/app/routes/forecasting.py b/gateway/app/routes/forecasting.py index e351c1f1..b59fc743 100644 --- a/gateway/app/routes/forecasting.py +++ b/gateway/app/routes/forecasting.py @@ -12,18 +12,32 @@ from app.core.config import settings logger = logging.getLogger(__name__) router = APIRouter() -@router.api_route("/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +@router.api_route("/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) async def proxy_forecasts(request: Request, path: str): """Proxy forecast requests to forecasting service""" return await _proxy_request(request, f"/api/v1/forecasts/{path}") -@router.api_route("/predictions/{path:path}", methods=["GET", "POST"]) +@router.api_route("/predictions/{path:path}", methods=["GET", "POST", "OPTIONS"]) async def proxy_predictions(request: Request, path: str): """Proxy prediction requests to forecasting service""" return await _proxy_request(request, f"/api/v1/predictions/{path}") async def _proxy_request(request: Request, target_path: str): """Proxy request to forecasting service with user context""" + + # Handle OPTIONS requests directly for CORS + if request.method == "OPTIONS": + return Response( + status_code=200, + headers={ + "Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST, + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "86400" # Cache preflight for 24 hours + } + ) + try: url = f"{settings.FORECASTING_SERVICE_URL}{target_path}" diff --git a/gateway/app/routes/nominatim.py b/gateway/app/routes/nominatim.py index 55e504a5..2db661af 100644 --- a/gateway/app/routes/nominatim.py +++ b/gateway/app/routes/nominatim.py @@ -14,6 +14,19 @@ async def proxy_nominatim_search(request: Request): """ Proxies requests to the Nominatim geocoding search API. """ + # Handle OPTIONS requests directly for CORS + if request.method == "OPTIONS": + return Response( + status_code=200, + headers={ + "Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST, + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "86400" # Cache preflight for 24 hours + } + ) + try: # Construct the internal Nominatim URL # All query parameters from the client request are forwarded diff --git a/gateway/app/routes/user.py b/gateway/app/routes/user.py new file mode 100644 index 00000000..e25e9bdd --- /dev/null +++ b/gateway/app/routes/user.py @@ -0,0 +1,194 @@ +# ================================================================ +# gateway/app/routes/user.py +# ================================================================ +""" +Authentication routes for API Gateway +""" + +import logging +import httpx +from fastapi import APIRouter, Request, Response, HTTPException, status +from fastapi.responses import JSONResponse +from typing import Dict, Any +import json + +from app.core.config import settings +from app.core.service_discovery import ServiceDiscovery +from shared.monitoring.metrics import MetricsCollector + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Initialize service discovery and metrics +service_discovery = ServiceDiscovery() +metrics = MetricsCollector("gateway") + +# Auth service configuration +AUTH_SERVICE_URL = settings.AUTH_SERVICE_URL or "http://auth-service:8000" + +class UserProxy: + """Authentication service proxy with enhanced error handling""" + + def __init__(self): + self.client = httpx.AsyncClient( + timeout=httpx.Timeout(30.0), + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20) + ) + + async def forward_request( + self, + method: str, + path: str, + request: Request + ) -> Response: + """Forward request to auth service with proper error handling""" + + # Handle OPTIONS requests directly for CORS + if request.method == "OPTIONS": + return Response( + status_code=200, + headers={ + "Access-Control-Allow-Origin": settings.CORS_ORIGINS_LIST, + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Tenant-ID", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "86400" # Cache preflight for 24 hours + } + ) + + try: + # Get auth service URL (with service discovery if available) + auth_url = await self._get_auth_service_url() + target_url = f"{auth_url}/api/v1/user/{path}" + + # Prepare headers (remove hop-by-hop headers) + headers = self._prepare_headers(dict(request.headers)) + + # Get request body + body = await request.body() + + # Forward request + logger.info(f"Forwarding {method} {path} to auth service") + + response = await self.client.request( + method=method, + url=target_url, + headers=headers, + content=body, + params=dict(request.query_params) + ) + + # Record metrics + metrics.increment_counter("gateway_auth_requests_total") + metrics.increment_counter( + "gateway_auth_responses_total", + labels={"status_code": str(response.status_code)} + ) + + # Prepare response headers + response_headers = self._prepare_response_headers(dict(response.headers)) + + return Response( + content=response.content, + status_code=response.status_code, + headers=response_headers, + media_type=response.headers.get("content-type") + ) + + except httpx.TimeoutException: + logger.error(f"Timeout forwarding {method} {path} to auth service") + metrics.increment_counter("gateway_auth_errors_total", labels={"error": "timeout"}) + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Authentication service timeout" + ) + + except httpx.ConnectError: + logger.error(f"Connection error forwarding {method} {path} to auth service") + metrics.increment_counter("gateway_auth_errors_total", labels={"error": "connection"}) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Authentication service unavailable" + ) + + except Exception as e: + logger.error(f"Error forwarding {method} {path} to auth service: {e}") + metrics.increment_counter("gateway_auth_errors_total", labels={"error": "unknown"}) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal gateway error" + ) + + async def _get_auth_service_url(self) -> str: + """Get auth service URL with service discovery""" + try: + # Try service discovery first + service_url = await service_discovery.get_service_url("auth-service") + if service_url: + return service_url + except Exception as e: + logger.warning(f"Service discovery failed: {e}") + + # Fall back to configured URL + return AUTH_SERVICE_URL + + def _prepare_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + """Prepare headers for forwarding (remove hop-by-hop headers)""" + # Remove hop-by-hop headers + hop_by_hop_headers = { + 'connection', 'keep-alive', 'proxy-authenticate', + 'proxy-authorization', 'te', 'trailers', 'upgrade' + } + + filtered_headers = { + k: v for k, v in headers.items() + if k.lower() not in hop_by_hop_headers + } + + # Add gateway identifier + filtered_headers['X-Forwarded-By'] = 'bakery-gateway' + filtered_headers['X-Gateway-Version'] = '1.0.0' + + return filtered_headers + + def _prepare_response_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + """Prepare response headers""" + # Remove server-specific headers + filtered_headers = { + k: v for k, v in headers.items() + if k.lower() not in {'server', 'date'} + } + + # Add CORS headers if needed + if settings.CORS_ORIGINS: + filtered_headers['Access-Control-Allow-Origin'] = '*' + filtered_headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + filtered_headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' + + return filtered_headers + +# Initialize proxy +user_proxy = UserProxy() + +# ================================================================ +# USER MANAGEMENT ENDPOINTS - Proxied to auth service +# ================================================================ + +@router.get("/me") +async def get_current_user(request: Request): + """Proxy get current user to auth service""" + return await user_proxy.forward_request("GET", "/me", request) + +@router.put("/me") +async def update_current_user(request: Request): + """Proxy update current user to auth service""" + return await user_proxy.forward_request("PUT", "/me", request) + +# ================================================================ +# CATCH-ALL ROUTE for any other user endpoints +# ================================================================ + +@router.api_route("/user/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) +async def proxy_auth_requests(path: str, request: Request): + """Catch-all proxy for auth requests""" + return await user_proxy.forward_request(request.method, path, request) \ No newline at end of file