Add new frontend - fix 16

This commit is contained in:
Urtzi Alfaro
2025-07-23 07:26:04 +02:00
parent e6b0be0c95
commit 1d35912459
14 changed files with 588 additions and 169 deletions

View File

@@ -57,6 +57,10 @@ class TokenManager {
}
async storeTokens(response: TokenResponse): Promise<void> {
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;

View File

@@ -31,36 +31,91 @@ export class AuthService {
return response.data!;
}
/**
* User login
*/
async login(credentials: LoginRequest): Promise<TokenResponse> {
const response = await apiClient.post<ApiResponse<TokenResponse>>(
'/auth/login',
credentials
);
async register(userData: RegisterRequest): Promise<TokenResponse> {
try {
// ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/register',
userData
);
// Store tokens after successful login
const tokenData = response.data!;
await tokenManager.storeTokens(tokenData);
// ✅ FIX: Check if response contains token data (direct response)
if (!response || !response.access_token) {
throw new Error('Registration successful but no tokens received');
}
return tokenData;
// 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<TokenResponse> {
const response = await apiClient.post<ApiResponse<TokenResponse>>(
'/api/v1/auth/register',
userData
);
async login(credentials: LoginRequest): Promise<TokenResponse> {
try {
// ✅ FIX: apiClient returns direct response, not wrapped in ApiResponse
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/login',
credentials
);
// Store tokens after successful registration
const tokenData = response.data!;
await tokenManager.storeTokens(tokenData);
// ✅ FIX: Check if response contains token data (direct response)
if (!response || !response.access_token) {
throw new Error('Login successful but no tokens received');
}
return tokenData;
// 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<TokenResponse> {
const response = await apiClient.post<ApiResponse<TokenResponse>>(
'/auth/refresh',
const response = await apiClient.post<TokenResponse>(
'/api/v1/auth/refresh',
{ refresh_token: refreshToken }
);
return response.data!;
return response;
}
/**
@@ -87,7 +142,7 @@ export class AuthService {
*/
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
const response = await apiClient.put<ApiResponse<UserProfile>>(
'/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<void> {
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<void> {
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<void> {
await apiClient.post('/auth/verify-email', { token });
await apiClient.post('/api/v1/auth/verify-email', { token });
}
/**
* Resend verification email
*/
async resendVerification(): Promise<void> {
await apiClient.post('/auth/resend-verification');
await apiClient.post('/api/v1/auth/resend-verification');
}
/**
@@ -145,7 +200,7 @@ export class AuthService {
*/
async logout(): Promise<void> {
try {
await apiClient.post('/auth/logout');
await apiClient.post('/api/v1/auth/logout');
} catch (error) {
console.error('Logout API call failed:', error);
} finally {

View File

@@ -42,7 +42,7 @@ export class DataService {
additionalData?: Record<string, any>
): Promise<UploadResponse> {
const response = await apiClient.upload<ApiResponse<UploadResponse>>(
'/data/upload-sales',
'/api/v1/data/upload-sales',
file,
additionalData
);
@@ -54,7 +54,7 @@ export class DataService {
*/
async validateSalesData(file: File): Promise<DataValidation> {
const response = await apiClient.upload<ApiResponse<DataValidation>>(
'/data/validate-sales',
'/api/v1/data/validate-sales',
file
);
return response.data!;
@@ -65,7 +65,7 @@ export class DataService {
*/
async getDashboardStats(): Promise<DashboardStats> {
const response = await apiClient.get<ApiResponse<DashboardStats>>(
'/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<SalesRecord> {
const response = await apiClient.post<ApiResponse<SalesRecord>>(
'/data/sales',
'/api/v1/data/sales',
record
);
return response.data!;
@@ -108,7 +108,7 @@ export class DataService {
updates: Partial<CreateSalesRequest>
): Promise<SalesRecord> {
const response = await apiClient.put<ApiResponse<SalesRecord>>(
`/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<void> {
await apiClient.delete(`/data/sales/${id}`);
await apiClient.delete(`/api/v1/data/sales/${id}`);
}
/**
@@ -130,7 +130,7 @@ export class DataService {
location?: string;
}): Promise<WeatherData[]> {
const response = await apiClient.get<ApiResponse<WeatherData[]>>(
'/data/weather',
'/api/v1/data/weather',
{ params }
);
return response.data!;
@@ -145,7 +145,7 @@ export class DataService {
location?: string;
}): Promise<TrafficData[]> {
const response = await apiClient.get<ApiResponse<TrafficData[]>>(
'/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<ApiResponse<any>>('/data/quality');
const response = await apiClient.get<ApiResponse<any>>('/api/v1/data/quality');
return response.data!;
}
@@ -171,7 +171,7 @@ export class DataService {
endDate?: string;
format?: 'csv' | 'excel';
}): Promise<Blob> {
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<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>('/data/products');
const response = await apiClient.get<ApiResponse<string[]>>('/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<ApiResponse<any>>('/data/sync/status');
const response = await apiClient.get<ApiResponse<any>>('/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<void> {
await apiClient.post('/data/sync/trigger', { data_type: dataType });
await apiClient.post('/api/v1/data/sync/trigger', { data_type: dataType });
}
}

View File

@@ -113,7 +113,7 @@ export class ForecastingService {
*/
async getForecast(forecastId: string): Promise<ForecastRecord> {
const response = await apiClient.get<ApiResponse<ForecastRecord>>(
`/forecasting/forecasts/${forecastId}`
`/api/v1/forecasting/forecasts/${forecastId}`
);
return response.data!;
}
@@ -142,7 +142,7 @@ export class ForecastingService {
*/
async acknowledgeAlert(alertId: string): Promise<ForecastAlert> {
const response = await apiClient.put<ApiResponse<ForecastAlert>>(
`/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<QuickForecast> {
const response = await apiClient.get<ApiResponse<QuickForecast>>(
`/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<BatchForecastStatus> {
const response = await apiClient.get<ApiResponse<BatchForecastStatus>>(
`/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<void> {
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<ApiResponse<any>>('/forecasting/stats');
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/stats');
return response.data!;
}
@@ -246,7 +246,7 @@ export class ForecastingService {
accuracy: number;
};
}> {
const response = await apiClient.get<ApiResponse<any>>('/forecasting/compare', { params });
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/compare', { params });
return response.data!;
}
@@ -259,7 +259,7 @@ export class ForecastingService {
endDate?: string;
format?: 'csv' | 'excel';
}): Promise<Blob> {
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<ApiResponse<any>>('/forecasting/insights', { params });
const response = await apiClient.get<ApiResponse<any>>('/api/v1/forecasting/insights', { params });
return response.data!;
}
}

View File

@@ -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<TenantInfo> {
const response = await apiClient.post<ApiResponse<TenantInfo>>('/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<TenantInfo> {
const response = await apiClient.get<ApiResponse<TenantInfo>>(`/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<TenantInfo> {
const response = await apiClient.put<ApiResponse<TenantInfo>>(`/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<TenantInfo[]> {
const response = await apiClient.get<ApiResponse<TenantInfo[]>>(`/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<TenantMemberResponse> {
const response = await apiClient.post<ApiResponse<TenantMemberResponse>>(
`/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<TenantInfo> {
const response = await apiClient.get<ApiResponse<TenantInfo>>('/tenants/current');
const response = await apiClient.get<ApiResponse<TenantInfo>>('/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<TenantInfo> {
const response = await apiClient.put<ApiResponse<TenantInfo>>('/tenants/current', updates);
const response = await apiClient.put<ApiResponse<TenantInfo>>('/api/v1/tenants/current', updates);
return response.data!;
}
/**
* Get tenant settings
* Get tenant settings (no direct backend mapping in provided file)
*/
async getTenantSettings(): Promise<TenantSettings> {
const response = await apiClient.get<ApiResponse<TenantSettings>>('/tenants/settings');
const response = await apiClient.get<ApiResponse<TenantSettings>>('/api/v1/tenants/settings');
return response.data!;
}
/**
* Update tenant settings
* Update tenant settings (no direct backend mapping in provided file)
*/
async updateTenantSettings(settings: Partial<TenantSettings>): Promise<TenantSettings> {
const response = await apiClient.put<ApiResponse<TenantSettings>>(
'/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<TenantStats> {
const response = await apiClient.get<ApiResponse<TenantStats>>('/tenants/stats');
const response = await apiClient.get<ApiResponse<TenantStats>>('/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<ApiResponse<any>>('/tenants/users', { params });
const response = await apiClient.get<ApiResponse<any>>('/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<ApiResponse<any>>('/tenants/users/invite', invitation);
const response = await apiClient.post<ApiResponse<any>>('/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<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/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<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/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<TenantUser> {
const response = await apiClient.patch<ApiResponse<TenantUser>>(
`/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<void> {
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<ApiResponse<any>>('/tenants/invitations');
const response = await apiClient.get<ApiResponse<any>>('/api/v1/tenants/invitations');
return response.data!;
}
/**
* Cancel invitation
* Cancel invitation (no direct backend mapping in provided file)
*/
async cancelInvitation(invitationId: string): Promise<void> {
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<void> {
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<ApiResponse<any>>('/tenants/activity', { params });
const response = await apiClient.get<ApiResponse<any>>('/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<ApiResponse<any>>('/tenants/billing');
const response = await apiClient.get<ApiResponse<any>>('/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<void> {
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<ApiResponse<any>>('/tenants/subscription/change', {
const response = await apiClient.post<ApiResponse<any>>('/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<ApiResponse<any>>('/tenants/subscription/cancel', {
const response = await apiClient.post<ApiResponse<any>>('/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<Blob> {
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<ApiResponse<any>>('/tenants/current', {
const response = await apiClient.delete<ApiResponse<any>>('/api/v1/tenants/current', {
data: { confirmation_token: confirmationToken },
});
return response.data!;

View File

@@ -55,7 +55,7 @@ export class TrainingService {
*/
async getTrainingStatus(jobId: string): Promise<TrainingJobProgress> {
const response = await apiClient.get<ApiResponse<TrainingJobProgress>>(
`/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<ApiResponse<any>>('/training/jobs', { params });
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/jobs', { params });
return response.data!;
}
@@ -81,7 +81,7 @@ export class TrainingService {
* Cancel training job
*/
async cancelTraining(jobId: string): Promise<void> {
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<ApiResponse<any>>('/training/models', { params });
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/models', { params });
return response.data!;
}
@@ -107,7 +107,7 @@ export class TrainingService {
*/
async getModel(modelId: string): Promise<TrainedModel> {
const response = await apiClient.get<ApiResponse<TrainedModel>>(
`/training/models/${modelId}`
`/api/v1/training/models/${modelId}`
);
return response.data!;
}
@@ -117,7 +117,7 @@ export class TrainingService {
*/
async getModelMetrics(modelId: string): Promise<ModelMetrics> {
const response = await apiClient.get<ApiResponse<ModelMetrics>>(
`/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<TrainedModel> {
const response = await apiClient.patch<ApiResponse<TrainedModel>>(
`/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<void> {
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<TrainingConfiguration>): Promise<TrainingJobStatus> {
const response = await apiClient.post<ApiResponse<TrainingJobStatus>>(
'/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<ApiResponse<any>>('/training/stats');
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/stats');
return response.data!;
}
@@ -179,7 +179,7 @@ export class TrainingService {
product_data_points: Record<string, number>;
recommendation: string;
}> {
const response = await apiClient.post<ApiResponse<any>>('/training/validate', {
const response = await apiClient.post<ApiResponse<any>>('/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<ApiResponse<any>>('/training/recommendations');
const response = await apiClient.get<ApiResponse<any>>('/api/v1/training/recommendations');
return response.data!;
}
@@ -202,7 +202,7 @@ export class TrainingService {
* Get training logs
*/
async getTrainingLogs(jobId: string): Promise<string[]> {
const response = await apiClient.get<ApiResponse<string[]>>(`/training/jobs/${jobId}/logs`);
const response = await apiClient.get<ApiResponse<string[]>>(`/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<Blob> {
const response = await apiClient.get(`/training/models/${modelId}/export`, {
const response = await apiClient.get(`/api/v1/training/models/${modelId}/export`, {
params: { format },
responseType: 'blob',
});

View File

@@ -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);
// ✅ FIX: Handle registration conflicts properly
try {
// Try to register first
const tokenResponse = await authService.register(data);
// After registration, get user profile
const profile = await authService.getCurrentUser();
setUser(profile);
// 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

View File

@@ -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<OnboardingFormData> = {};
// 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.');

View File

@@ -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"])

View File

@@ -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
# ================================================================

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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

194
gateway/app/routes/user.py Normal file
View File

@@ -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)