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: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile.${ENVIRONMENT} 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} image: bakery/dashboard:${IMAGE_TAG}
container_name: bakery-dashboard container_name: bakery-dashboard
restart: unless-stopped 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'; import { jwtDecode } from 'jwt-decode';
interface TokenPayload { interface TokenPayload {
@@ -13,7 +14,7 @@ interface TokenResponse {
access_token: string; access_token: string;
refresh_token: string; refresh_token: string;
token_type: string; token_type: string;
expires_in: number; expires_in?: number;
} }
class TokenManager { class TokenManager {
@@ -42,20 +43,32 @@ class TokenManager {
// Check if token needs refresh // Check if token needs refresh
if (this.isTokenExpired()) { 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> { async storeTokens(response: TokenResponse | any): Promise<void> {
this.accessToken = response.access_token; // Handle both direct TokenResponse and login response with nested tokens
this.refreshToken = response.refresh_token; 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 // Calculate expiry time
const expiresIn = response.expires_in || 3600; // Default 1 hour const expiresIn = response.expires_in || 3600; // Default 1 hour
this.tokenExpiry = new Date(Date.now() + expiresIn * 1000); this.tokenExpiry = new Date(Date.now() + expiresIn * 1000);
// Store securely (not in localStorage for security) // Store securely
this.secureStore({ this.secureStore({
accessToken: this.accessToken, accessToken: this.accessToken,
refreshToken: this.refreshToken, refreshToken: this.refreshToken,
@@ -66,7 +79,12 @@ class TokenManager {
async getAccessToken(): Promise<string | null> { async getAccessToken(): Promise<string | null> {
// Check if token is expired or will expire soon (5 min buffer) // Check if token is expired or will expire soon (5 min buffer)
if (this.shouldRefreshToken()) { if (this.shouldRefreshToken()) {
await this.refreshAccessToken(); try {
await this.refreshAccessToken();
} catch (error) {
console.error('Token refresh failed:', error);
return null;
}
} }
return this.accessToken; return this.accessToken;
} }
@@ -92,7 +110,8 @@ class TokenManager {
} }
try { try {
const response = await fetch('/api/auth/refresh', { // FIXED: Use correct refresh endpoint
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -103,12 +122,14 @@ class TokenManager {
}); });
if (!response.ok) { 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(); const data: TokenResponse = await response.json();
await this.storeTokens(data); await this.storeTokens(data);
} catch (error) { } catch (error) {
console.error('Token refresh error:', error);
// Clear tokens on refresh failure // Clear tokens on refresh failure
this.clearTokens(); this.clearTokens();
throw error; throw error;
@@ -140,26 +161,33 @@ class TokenManager {
// Secure storage implementation // Secure storage implementation
private secureStore(data: any): void { private secureStore(data: any): void {
// In production, use httpOnly cookies or secure session storage try {
// For now, using sessionStorage with encryption const encrypted = this.encrypt(JSON.stringify(data));
const encrypted = this.encrypt(JSON.stringify(data)); sessionStorage.setItem('auth_tokens', encrypted);
sessionStorage.setItem('auth_tokens', encrypted); } catch (error) {
console.error('Failed to store tokens:', error);
}
} }
private getStoredTokens(): any { private getStoredTokens(): any {
const stored = sessionStorage.getItem('auth_tokens');
if (!stored) return null;
try { try {
const stored = sessionStorage.getItem('auth_tokens');
if (!stored) return null;
const decrypted = this.decrypt(stored); const decrypted = this.decrypt(stored);
return JSON.parse(decrypted); return JSON.parse(decrypted);
} catch { } catch (error) {
console.error('Failed to retrieve stored tokens:', error);
return null; return null;
} }
} }
private clearSecureStore(): void { 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) // Simple encryption for demo (use proper encryption in production)

View File

@@ -320,5 +320,5 @@ class ApiClient {
// Create default instance // Create default instance
export const apiClient = new ApiClient({ 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 { tokenManager } from '../auth/tokenManager';
import { apiClient } from '../base/apiClient'; import { apiClient } from '../base/apiClient';
@@ -26,41 +27,37 @@ export interface UserProfile {
class AuthService { class AuthService {
async login(credentials: LoginCredentials): Promise<UserProfile> { async login(credentials: LoginCredentials): Promise<UserProfile> {
// OAuth2 password flow // FIXED: Use correct endpoint and method
const formData = new URLSearchParams(); const response = await apiClient.post('/api/v1/auth/login', credentials);
formData.append('username', credentials.email);
formData.append('password', credentials.password); // Store tokens from login response
formData.append('grant_type', 'password'); await tokenManager.storeTokens(response);
const response = await fetch('/auth/login', { // Get user profile from the response or make separate call
method: 'POST', if (response.user) {
headers: { return response.user;
'Content-Type': 'application/x-www-form-urlencoded', } else {
}, return this.getCurrentUser();
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Login failed');
} }
const tokenResponse = await response.json();
await tokenManager.storeTokens(tokenResponse);
// Get user profile
return this.getCurrentUser();
} }
async register(data: RegisterData): Promise<UserProfile> { 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> { async logout(): Promise<void> {
try { try {
await apiClient.post('/auth/logout'); await apiClient.post('/api/v1/auth/logout');
} finally { } finally {
tokenManager.clearTokens(); tokenManager.clearTokens();
window.location.href = '/login'; window.location.href = '/login';
@@ -68,15 +65,15 @@ class AuthService {
} }
async getCurrentUser(): Promise<UserProfile> { async getCurrentUser(): Promise<UserProfile> {
return apiClient.get('/auth/me'); return apiClient.get('/api/v1/auth/me');
} }
async updateProfile(updates: Partial<UserProfile>): Promise<UserProfile> { 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> { 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, current_password: currentPassword,
new_password: newPassword new_password: newPassword
}); });
@@ -87,4 +84,4 @@ class AuthService {
} }
} }
export const authService = new AuthService(); export const authService = new AuthService();