Skip to content

Commit

Permalink
moved auth related stuff to feature
Browse files Browse the repository at this point in the history
  • Loading branch information
azakharo committed Jan 2, 2025
1 parent 7c01bf8 commit 37d454b
Show file tree
Hide file tree
Showing 16 changed files with 84 additions and 187 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_URL=https://reqres.in
105 changes: 21 additions & 84 deletions src/api/axiosSetup.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,30 @@
import axios, {AxiosError, CancelTokenSource} from 'axios';
import axios, {isAxiosError} from 'axios';

export let axi = axios.create();
import {ROUTE__LOGIN, ROUTE__MAIN} from "@/shared/constants";

let requestInterceptor: number | null = null;
const redirectToLogin = (): void => {
const { pathname, search } = window.location;

let responseInterceptor: number | null = null;

export const init = (unauthCallback: () => void): void => {
// Add a request interceptor
requestInterceptor = axi.interceptors.request.use(
/* eslint-disable-next-line arrow-body-style */
config => {
// const url = config.url;
//
// if (something) {
// // Disable the following rule because need to update request data
// /* eslint-disable-next-line no-param-reassign */
// config.data = {
// ...config.data,
// ...something,
// };
// }

return config;
},
);

// Add a response interceptor
responseInterceptor = axi.interceptors.response.use(
/* eslint-disable-next-line arrow-body-style */
response => {
// if (not authorized here or in the error handler) {
// const {data} = response;
// const {
// extract some data
// } = data;
//
// if (INVALID_SESSION) {
// unauthCallback();
// }
//
// throw normalizedError;
// }

return response;
},
(error: AxiosError) => {
if (error?.response?.status === 401) {
unauthCallback();
}

return Promise.reject(error);
},
);
};

export const uninit = (): void => {
if (requestInterceptor) {
axi.interceptors.request.eject(requestInterceptor);
requestInterceptor = null;
if (pathname === ROUTE__LOGIN) {
return;
}

if (responseInterceptor) {
axi.interceptors.response.eject(responseInterceptor);
responseInterceptor = null;
}

// AZA:
// Looks like the ejects above do not work.
// So, recreate axios instance from scratch.
axi = axios.create();
window.location.href = `${ROUTE__LOGIN}${pathname === ROUTE__MAIN ? '' : `?redirect=${pathname}${search}`}`;
};

// Initialization
//---------------------------------------------------------------------------

//= =======================================
// Request cancellation

export const createCancelToken = (): CancelTokenSource => {
const {CancelToken} = axios;
export const axi = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});

return CancelToken.source();
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export const isRequestCancelled = (error: any): boolean => {
return axios.isCancel(error);
};
axi.interceptors.response.use(
(response) => response,
(error: unknown) => {
if (isAxiosError(error)) {
if (error.response?.status === 401) {
redirectToLogin();
}
}

// Request cancellation
//= =======================================
return Promise.reject(error);
},
);
2 changes: 1 addition & 1 deletion src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const BASE_URL = 'https://reqres.in';
export const BASE_URL = '';
1 change: 0 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export {login, logout} from './auth';
export {
createCancelToken,
init,
Expand Down
2 changes: 1 addition & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {createTheme, ThemeProvider} from '@mui/material/styles';

import AppGlobalStyles from './GlobalStyles';
import AppRoutes from '@/app/Routes';
import {AuthProvider} from '@/contexts/AuthContext';
import {AuthProvider} from '@/features/auth';
import {isProduction} from '@/shared/utils';

const theme = createTheme();
Expand Down
2 changes: 1 addition & 1 deletion src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {FC, memo} from 'react';
import {Navigate, Route, Routes as ReactRoutes} from 'react-router-dom';

import {ROUTE__LOGIN, ROUTE__MAIN} from '@/shared/constants';
import useAuth from '@/hooks/useAuth';
import {useAuth} from '@/features/auth';
import Login from '@/pages/Login';
import Main from '@/pages/Main';

Expand Down
121 changes: 36 additions & 85 deletions src/contexts/AuthContext.tsx → src/features/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {
createContext,
FC,
ReactNode,
FC, PropsWithChildren,
useCallback,
useEffect,
useMemo,
useReducer,
} from 'react';
import {useLocation, useNavigate} from 'react-router-dom';
import {useNavigate} from 'react-router-dom';

import {init as apiInit, login as apiLogin, uninit as apiUninit} from '@/api';
import {ROUTE__LOGIN, ROUTE__MAIN} from '@/shared/constants';
import {ROUTE__LOGIN} from '@/shared/constants';
import {
getAuthToken as getAuthTokenFromLocalStorage,
getUserId as getUserIdFromLocalStorage,
Expand All @@ -19,12 +17,13 @@ import {
setAuthToken as putAuthTokenToLocalStorage,
setUserId as putUserIdToLocalStorage,
} from '@/shared/helpers';
import UserLoggedIn from '@/types/UserLoggedIn';
import {UserSignedIn} from './types';
import {login as loginMethod} from './api';

export interface AuthState {
isAuthenticated: boolean;
isInitialised: boolean;
user: UserLoggedIn | null;
user: UserSignedIn | null;
}

const initialAuthState: AuthState = {
Expand All @@ -49,12 +48,12 @@ const ACTION__LOGOUT = 'ACTION__LOGOUT';

interface AppInitAction {
type: typeof ACTION__APP_INIT;
payload: UserLoggedIn | null;
payload: UserSignedIn | null;
}

interface LoginAction {
type: typeof ACTION__LOGIN;
payload: UserLoggedIn;
payload: UserSignedIn;
}

interface LogoutAction {
Expand Down Expand Up @@ -109,22 +108,13 @@ const AuthContext = createContext<AuthContextType>({
logout: () => {},
});

interface LocationState {
returnUrl?: string;
}

interface Props {
children: ReactNode;
}

export const AuthProvider: FC<Props> = ({children}) => {
export const AuthProvider: FC<PropsWithChildren> = ({children}) => {
const [state, dispatch] = useReducer(reducer, initialAuthState);
const navigate = useNavigate();
const location = useLocation();

const login = useCallback(
async (username: string, password: string, redirect = true) => {
const {id, name, token} = await apiLogin(username, password);
async (username: string, password: string) => {
const {id, name, token} = await loginMethod(username, password);

setSession(token, id);

Expand All @@ -137,21 +127,8 @@ export const AuthProvider: FC<Props> = ({children}) => {
type: ACTION__LOGIN,
payload: user,
});

if (redirect) {
const locationState = location.state as LocationState;
const returnUrl = locationState?.returnUrl;
navigate(
returnUrl && !returnUrl.endsWith(ROUTE__LOGIN)
? returnUrl
: ROUTE__MAIN,
{
replace: true,
},
);
}
},
[navigate, location.state],
[dispatch],
);

const logout = useCallback(() => {
Expand All @@ -172,64 +149,38 @@ export const AuthProvider: FC<Props> = ({children}) => {
);

useEffect(() => {
const initApp = () => {
apiInit(logout);

const redirectToLogin = () => {
navigate(ROUTE__LOGIN, {
state: {
returnUrl: `${location.pathname}${location.search}${location.hash}`,
},
});
};
try {
const accessToken = getAuthTokenFromLocalStorage();
const userId = getUserIdFromLocalStorage();

try {
const accessToken = getAuthTokenFromLocalStorage();
const userId = getUserIdFromLocalStorage();

if (accessToken && userId) {
// TODO Request current user info - check whether auth-ed or not
const user = {
id: userId,
name: 'Alexey',
};

setSession(accessToken, userId);

dispatch({
type: ACTION__APP_INIT,
payload: user,
});

if (location.pathname === ROUTE__LOGIN) {
navigate(ROUTE__MAIN);
}
} else {
dispatch({
type: ACTION__APP_INIT,
payload: null,
});
}
} catch (err) {
/* eslint-disable-next-line no-console */
console.error(err);

redirectToLogin();
if (accessToken && userId) {
// TODO Request current user info - check whether auth-ed or not
const user = {
id: userId,
name: 'Alexey',
};

setSession(accessToken, userId);

dispatch({
type: ACTION__APP_INIT,
payload: user,
});
} else {
dispatch({
type: ACTION__APP_INIT,
payload: null,
});
}
};
} catch (err) {
/* eslint-disable-next-line no-console */
console.error(err);

initApp();

// Call Api.uninit on unmount
// eslint-disable-next-line @typescript-eslint/unbound-method
return apiUninit;
// Have to run this effect only once on the app startup
// eslint-disable-next-line react-hooks/exhaustive-deps
dispatch({
type: ACTION__APP_INIT,
payload: null,
});
}
}, []);

if (!state.isInitialised) {
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions src/features/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {AuthProvider} from './AuthContext';
export {useAuth} from './useAuth';
4 changes: 4 additions & 0 deletions src/features/auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface UserSignedIn {
id: number;
name: string;
}
5 changes: 5 additions & 0 deletions src/features/auth/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {useContext} from 'react';

import AuthContext, {AuthContextType} from './AuthContext';

export const useAuth = (): AuthContextType => useContext(AuthContext);
7 changes: 0 additions & 7 deletions src/hooks/useAuth.ts

This file was deleted.

11 changes: 10 additions & 1 deletion src/pages/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {memo, useCallback, useState} from 'react';
import {Box, Button, FormHelperText, Typography} from '@mui/material';
import * as Yup from 'yup';

import useAuth from '@/hooks/useAuth';
import {useAuth} from '@/features/auth';
import {InferType} from 'yup';
import {FormProvider, useForm} from 'react-hook-form';
import {yupResolver} from '@hookform/resolvers/yup';
import {TextFieldElement} from 'react-hook-form-mui';
import {useNavigate, useSearchParams} from 'react-router-dom';
import {ROUTE__MAIN} from '@/shared/constants';

const v8nSchema = Yup.object().shape({
username: Yup.string().required('required'),
Expand All @@ -16,6 +18,8 @@ const v8nSchema = Yup.object().shape({
type FormValues = InferType<typeof v8nSchema>;

const Login = () => {
const [urlParams] = useSearchParams();
const navigate = useNavigate();
const {login} = useAuth();
const [authError, setAuthError] = useState('');

Expand All @@ -35,7 +39,12 @@ const Login = () => {
await login(username, password);
} catch (e) {
setAuthError((e as Error).message);
return;
}

const url = urlParams.get('redirect') ?? ROUTE__MAIN;

navigate(url);
},
[login],
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {memo, useEffect, useState} from 'react';
import {Box, Button} from '@mui/material';

import {getUsers} from '@/api/users';
import useAuth from '@/hooks/useAuth';
import {useAuth} from '@/features/auth';
import User from '@/types/users/User';
import errorImg from 'IMAGES/sad-cloud.png';

Expand Down
Loading

0 comments on commit 37d454b

Please sign in to comment.