diff --git a/docker-compose.yml b/docker-compose.yml index 20914ae8..871e87fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -522,10 +522,6 @@ services: build: context: ./frontend dockerfile: Dockerfile.${ENVIRONMENT} - args: - - REACT_APP_API_URL=${FRONTEND_API_URL} - - REACT_APP_WS_URL=${FRONTEND_WS_URL} - - REACT_APP_ENVIRONMENT=${ENVIRONMENT} image: bakery/dashboard:${IMAGE_TAG} container_name: bakery-dashboard restart: unless-stopped diff --git a/frontend/src/api/auth/tokenManager.ts b/frontend/src/api/auth/tokenManager.ts index b1f6a20b..4429f4ad 100644 --- a/frontend/src/api/auth/tokenManager.ts +++ b/frontend/src/api/auth/tokenManager.ts @@ -1,4 +1,5 @@ -// src/api/auth/tokenManager.ts +// File: frontend/src/api/auth/tokenManager.ts + import { jwtDecode } from 'jwt-decode'; interface TokenPayload { @@ -13,7 +14,7 @@ interface TokenResponse { access_token: string; refresh_token: string; token_type: string; - expires_in: number; + expires_in?: number; } class TokenManager { @@ -42,20 +43,32 @@ class TokenManager { // Check if token needs refresh if (this.isTokenExpired()) { - await this.refreshAccessToken(); + try { + await this.refreshAccessToken(); + } catch (error) { + // If refresh fails on init, clear tokens + this.clearTokens(); + } } } } - async storeTokens(response: TokenResponse): Promise { - this.accessToken = response.access_token; - this.refreshToken = response.refresh_token; + async storeTokens(response: TokenResponse | any): Promise { + // Handle both direct TokenResponse and login response with nested tokens + if (response.access_token) { + this.accessToken = response.access_token; + this.refreshToken = response.refresh_token; + } else { + // Handle login response format + this.accessToken = response.access_token; + this.refreshToken = response.refresh_token; + } // Calculate expiry time const expiresIn = response.expires_in || 3600; // Default 1 hour this.tokenExpiry = new Date(Date.now() + expiresIn * 1000); - // Store securely (not in localStorage for security) + // Store securely this.secureStore({ accessToken: this.accessToken, refreshToken: this.refreshToken, @@ -66,7 +79,12 @@ class TokenManager { async getAccessToken(): Promise { // Check if token is expired or will expire soon (5 min buffer) if (this.shouldRefreshToken()) { - await this.refreshAccessToken(); + try { + await this.refreshAccessToken(); + } catch (error) { + console.error('Token refresh failed:', error); + return null; + } } return this.accessToken; } @@ -92,7 +110,8 @@ class TokenManager { } try { - const response = await fetch('/api/auth/refresh', { + // FIXED: Use correct refresh endpoint + const response = await fetch('/api/v1/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -103,12 +122,14 @@ class TokenManager { }); if (!response.ok) { - throw new Error('Token refresh failed'); + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || 'Token refresh failed'); } const data: TokenResponse = await response.json(); await this.storeTokens(data); } catch (error) { + console.error('Token refresh error:', error); // Clear tokens on refresh failure this.clearTokens(); throw error; @@ -140,26 +161,33 @@ class TokenManager { // Secure storage implementation private secureStore(data: any): void { - // In production, use httpOnly cookies or secure session storage - // For now, using sessionStorage with encryption - const encrypted = this.encrypt(JSON.stringify(data)); - sessionStorage.setItem('auth_tokens', encrypted); + try { + const encrypted = this.encrypt(JSON.stringify(data)); + sessionStorage.setItem('auth_tokens', encrypted); + } catch (error) { + console.error('Failed to store tokens:', error); + } } private getStoredTokens(): any { - const stored = sessionStorage.getItem('auth_tokens'); - if (!stored) return null; - try { + const stored = sessionStorage.getItem('auth_tokens'); + if (!stored) return null; + const decrypted = this.decrypt(stored); return JSON.parse(decrypted); - } catch { + } catch (error) { + console.error('Failed to retrieve stored tokens:', error); return null; } } private clearSecureStore(): void { - sessionStorage.removeItem('auth_tokens'); + try { + sessionStorage.removeItem('auth_tokens'); + } catch (error) { + console.error('Failed to clear stored tokens:', error); + } } // Simple encryption for demo (use proper encryption in production) diff --git a/frontend/src/api/base/apiClient.ts b/frontend/src/api/base/apiClient.ts index 6dc2007b..518b30bc 100644 --- a/frontend/src/api/base/apiClient.ts +++ b/frontend/src/api/base/apiClient.ts @@ -320,5 +320,5 @@ class ApiClient { // Create default instance export const apiClient = new ApiClient({ - baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1' + baseURL: process.env.FRONTEND_API_URL || 'http://localhost:8000' }); diff --git a/frontend/src/api/services/authService.ts b/frontend/src/api/services/authService.ts index e0bf776b..fc66ccd7 100644 --- a/frontend/src/api/services/authService.ts +++ b/frontend/src/api/services/authService.ts @@ -1,4 +1,5 @@ -// src/api/auth/authService.ts +// File: frontend/src/api/services/authService.ts + import { tokenManager } from '../auth/tokenManager'; import { apiClient } from '../base/apiClient'; @@ -26,41 +27,37 @@ export interface UserProfile { class AuthService { async login(credentials: LoginCredentials): Promise { - // OAuth2 password flow - const formData = new URLSearchParams(); - formData.append('username', credentials.email); - formData.append('password', credentials.password); - formData.append('grant_type', 'password'); + // FIXED: Use correct endpoint and method + const response = await apiClient.post('/api/v1/auth/login', credentials); + + // Store tokens from login response + await tokenManager.storeTokens(response); - const response = await fetch('/auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: formData - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.detail || 'Login failed'); + // Get user profile from the response or make separate call + if (response.user) { + return response.user; + } else { + return this.getCurrentUser(); } - - const tokenResponse = await response.json(); - await tokenManager.storeTokens(tokenResponse); - - // Get user profile - return this.getCurrentUser(); } async register(data: RegisterData): Promise { - const response = await apiClient.post('/auth/register', data); + // FIXED: Use correct endpoint path + const response = await apiClient.post('/api/v1/auth/register', data); - return response; + // Registration only returns user data, NOT tokens + // So we need to login separately to get tokens + await this.login({ + email: data.email, + password: data.password + }); + + return response; // This is the user profile from registration } async logout(): Promise { try { - await apiClient.post('/auth/logout'); + await apiClient.post('/api/v1/auth/logout'); } finally { tokenManager.clearTokens(); window.location.href = '/login'; @@ -68,15 +65,15 @@ class AuthService { } async getCurrentUser(): Promise { - return apiClient.get('/auth/me'); + return apiClient.get('/api/v1/auth/me'); } async updateProfile(updates: Partial): Promise { - return apiClient.patch('/auth/profile', updates); + return apiClient.patch('/api/v1/auth/profile', updates); } async changePassword(currentPassword: string, newPassword: string): Promise { - await apiClient.post('/auth/change-password', { + await apiClient.post('/api/v1/auth/change-password', { current_password: currentPassword, new_password: newPassword }); @@ -87,4 +84,4 @@ class AuthService { } } -export const authService = new AuthService(); +export const authService = new AuthService(); \ No newline at end of file