Skip to content

Commit 8a867ac

Browse files
authored
Merge pull request #69 from ojasvagr123/main
Phase2_Day1
2 parents 5e75dc9 + 51b954e commit 8a867ac

4 files changed

Lines changed: 81 additions & 49 deletions

File tree

frontend/src/api/auth.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import apiClient, { ApiResponse, handleApiError } from './index';
22

3-
// Types for auth requests/responses
3+
// Types for login request payload
44
export interface LoginRequest {
55
email: string;
66
password: string;
77
}
88

9+
// Types for registration request payload
910
export interface RegisterRequest {
1011
user_name: string;
1112
user_email: string;
1213
user_password: string;
13-
organizationId?: string;
14+
organizationId?: string; // Optional field
1415
}
1516

17+
// Types for authentication response from backend
1618
export interface AuthResponse {
1719
user: {
1820
id: string;
@@ -24,67 +26,86 @@ export interface AuthResponse {
2426
}
2527

2628
/**
27-
* User login
29+
* User login function
30+
* @param credentials - LoginRequest containing email and password
31+
* @returns Promise resolving to AuthResponse containing user info and token
32+
* Stores token in localStorage for subsequent requests
2833
*/
2934
export const login = async (credentials: LoginRequest): Promise<AuthResponse> => {
3035
try {
36+
// Call login API endpoint with user credentials
3137
const response = await apiClient.post<ApiResponse<AuthResponse>>('/auth/login', credentials);
3238
const { token, user } = response.data.data;
3339

34-
// Store token for future requests
40+
// Save token in localStorage for auth persistence
3541
localStorage.setItem('accessToken', token);
3642

3743
return { token, user };
3844
} catch (error) {
45+
// Handle errors gracefully with helper function
3946
throw new Error(handleApiError(error));
4047
}
4148
};
4249

4350
/**
44-
* User registration
51+
* User registration function
52+
* @param userData - RegisterRequest containing registration details
53+
* @returns Promise resolving to AuthResponse containing user info and token
54+
* Stores token in localStorage for subsequent requests
4555
*/
4656
export const register = async (userData: RegisterRequest): Promise<AuthResponse> => {
4757
try {
58+
// Call register API endpoint with user data
4859
const response = await apiClient.post<ApiResponse<AuthResponse>>('/auth/register', userData);
4960
const { token, user } = response.data.data;
5061

51-
// Store token for future requests
62+
// Save token in localStorage for auth persistence
5263
localStorage.setItem('accessToken', token);
5364

5465
return { token, user };
5566
} catch (error) {
67+
// Handle errors gracefully with helper function
5668
throw new Error(handleApiError(error));
5769
}
5870
};
5971

6072
/**
61-
* User logout
73+
* User logout function
74+
* Calls logout API endpoint and removes token from localStorage
6275
*/
6376
export const logout = async (): Promise<void> => {
6477
try {
78+
// Notify backend about logout
6579
await apiClient.post('/auth/logout');
80+
81+
// Remove token to clear authentication state on client
6682
localStorage.removeItem('accessToken');
6783
} catch (error) {
6884
throw new Error(handleApiError(error));
6985
}
7086
};
7187

7288
/**
73-
* Request password reset
89+
* Request password reset email
90+
* @param email - User email to send reset link
7491
*/
7592
export const forgotPassword = async (email: string): Promise<void> => {
7693
try {
94+
// Call forgot-password API with user email
7795
await apiClient.post('/auth/forgot-password', { email });
7896
} catch (error) {
7997
throw new Error(handleApiError(error));
8098
}
8199
};
82100

83101
/**
84-
* Reset password with token
102+
* Reset password with reset token and new password
103+
* @param token - Password reset token received via email
104+
* @param newPassword - New password string
85105
*/
86106
export const resetPassword = async (token: string, newPassword: string): Promise<void> => {
87107
try {
108+
// Call reset-password API with token and new password
88109
await apiClient.post('/auth/reset-password', { token, newPassword });
89110
} catch (error) {
90111
throw new Error(handleApiError(error));

frontend/src/api/index.ts

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,11 @@
1-
/**
2-
* Ensures all Promise rejections use proper Error objects
3-
*
4-
* This fix addresses a TypeScript error where Promise rejections were not
5-
* guaranteed to be Error objects. The code now checks if the rejection reason
6-
* is an Error instance and, if not, converts it to an Error object.
7-
*
8-
* Fixed in:
9-
* - Request interceptor error handler
10-
* - Response interceptor error handler
11-
*
12-
* This ensures type safety and better error handling throughout the application.
13-
*/
14-
/**
15-
* TypeScript type declarations for Vite's import.meta.env
16-
*
17-
* These declarations extend the ImportMeta interface to include Vite-specific
18-
* environment variables, resolving the TypeScript error:
19-
* "Property 'env' does not exist on type 'ImportMeta'"
20-
*
21-
* @see https://vitejs.dev/guide/env-and-mode.html#env-files
22-
*/
231
import axios, {
242
AxiosError,
253
AxiosInstance,
26-
AxiosRequestConfig, // Import AxiosRequestConfig
4+
AxiosRequestConfig,
275
AxiosResponse,
286
isAxiosError as axiosIsAxiosError,
297
} from 'axios';
8+
import { useAuthStore } from '../state/authStore'; // Adjust path if needed
309

3110
// TypeScript declaration for Vite env
3211
declare global {
@@ -42,11 +21,11 @@ declare global {
4221

4322
// API base URL - should match your backend
4423
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
24+
4525
// Default timeout in milliseconds
4626
const DEFAULT_TIMEOUT = 15000;
4727

48-
// Define an interface for the request config with retry flag
49-
// by extending the original AxiosRequestConfig
28+
// Extend AxiosRequestConfig to add _retry flag for token refresh logic
5029
interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
5130
_retry?: boolean;
5231
}
@@ -66,7 +45,7 @@ const createApiClient = (): AxiosInstance => {
6645

6746
// Request interceptor - adds auth token to requests
6847
apiClient.interceptors.request.use(
69-
config => {
48+
(config) => {
7049
const token = localStorage.getItem('accessToken');
7150
if (token && config.headers) {
7251
config.headers.Authorization = `Bearer ${token}`;
@@ -76,60 +55,65 @@ const createApiClient = (): AxiosInstance => {
7655
(error: AxiosError) => Promise.reject(error)
7756
);
7857

79-
// Response interceptor - handle common errors
58+
// Response interceptor - handle 401 errors and token refresh
8059
apiClient.interceptors.response.use(
8160
(response: AxiosResponse) => response,
8261
async (error: AxiosError) => {
83-
// Cast error.config to the extended interface
8462
const originalRequest = error.config as ExtendedAxiosRequestConfig;
8563

86-
// Handle 401 Unauthorized errors (expired token)
8764
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
8865
originalRequest._retry = true;
8966

9067
try {
91-
// Call refresh token endpoint
68+
// Attempt to refresh token
9269
const refreshResponse = await axios.post(
9370
`${API_BASE_URL}/auth/refresh-token`,
9471
{},
95-
{
96-
withCredentials: true, // Needed for cookies
97-
}
72+
{ withCredentials: true }
9873
);
9974

10075
const newToken = refreshResponse.data.token;
10176
localStorage.setItem('accessToken', newToken);
10277

103-
// Retry the original request with new token
10478
if (originalRequest.headers) {
10579
originalRequest.headers.Authorization = `Bearer ${newToken}`;
10680
}
81+
10782
return apiClient(originalRequest);
10883
} catch (refreshError) {
109-
// If refresh token fails, redirect to login
84+
// Refresh token failed - logout user
11085
localStorage.removeItem('accessToken');
111-
// Ensure window is defined (for SSR or test environments)
86+
const store = useAuthStore.getState();
87+
store.logout();
88+
11289
if (typeof window !== 'undefined') {
11390
window.location.href = '/login';
11491
}
11592

116-
// Ensure we reject with an Error object
11793
if (refreshError instanceof Error) {
11894
return Promise.reject(refreshError);
11995
} else if (typeof refreshError === 'string') {
12096
return Promise.reject(new Error(refreshError));
12197
} else {
122-
// For other types, you might want to serialize or provide a generic message
12398
return Promise.reject(
12499
new Error(
125100
`An unknown error occurred during token refresh: ${JSON.stringify(refreshError)}`
126101
)
127102
);
128103
}
129104
}
105+
} else if (error.response?.status === 401) {
106+
// Other 401 errors after retry or no retry - logout user
107+
localStorage.removeItem('accessToken');
108+
const store = useAuthStore.getState();
109+
store.logout();
110+
111+
if (typeof window !== 'undefined') {
112+
window.location.href = '/login';
113+
}
130114
}
131115

132-
return Promise.reject(error); // error here is AxiosError, which is an Error instance
116+
return Promise.reject(error);
133117
}
134118
);
135119

@@ -148,7 +132,7 @@ export interface ApiResponse<T> {
148132
status: number;
149133
}
150134

151-
// Common error handler
135+
// Common error handler function
152136
export const handleApiError = (error: unknown): string => {
153137
if (axiosIsAxiosError(error)) {
154138
const response = error.response;

frontend/src/api/labs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,5 @@ export const deleteLab = async (labId: string): Promise<void> => {
6464
throw new Error(handleApiError(error));
6565
}
6666
};
67+
68+

frontend/src/state/authStore.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { create } from 'zustand';
2+
3+
type User = {
4+
id: string;
5+
name: string;
6+
email: string;
7+
};
8+
9+
type AuthState = {
10+
user: User | null;
11+
token: string | null;
12+
isLoggedIn: boolean;
13+
login: (token: string, user: User) => void;
14+
logout: () => void;
15+
};
16+
17+
export const useAuthStore = create<AuthState>((set) => ({
18+
user: null,
19+
token: null,
20+
isLoggedIn: false,
21+
login: (token, user) =>
22+
set({ token, user, isLoggedIn: true }),
23+
logout: () =>
24+
set({ token: null, user: null, isLoggedIn: false }),
25+
}));

0 commit comments

Comments
 (0)