Add new frontend - fix 6

This commit is contained in:
Urtzi Alfaro
2025-07-22 12:58:32 +02:00
parent 915be54349
commit 5230915bc2
4 changed files with 75 additions and 54 deletions

View File

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

View File

@@ -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<void> {
this.accessToken = response.access_token;
this.refreshToken = response.refresh_token;
async storeTokens(response: TokenResponse | any): Promise<void> {
// 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<string | null> {
// 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)

View File

@@ -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'
});

View File

@@ -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<UserProfile> {
// 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<UserProfile> {
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<void> {
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<UserProfile> {
return apiClient.get('/auth/me');
return apiClient.get('/api/v1/auth/me');
}
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> {
return apiClient.patch('/auth/profile', updates);
return apiClient.patch('/api/v1/auth/profile', updates);
}
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
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();