Add new frontend - fix 6
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user