diff --git a/backend/src/packages/auth/auth.controller.ts b/backend/src/packages/auth/auth.controller.ts index 8e074357d..9cf37f8a4 100644 --- a/backend/src/packages/auth/auth.controller.ts +++ b/backend/src/packages/auth/auth.controller.ts @@ -83,6 +83,18 @@ class AuthController extends Controller { }>, ), }); + + this.addRoute({ + path: AuthApiPath.LOGOUT, + method: 'GET', + authStrategy: AuthStrategy.VERIFY_JWT, + handler: (options) => + this.logOut( + options as ApiHandlerOptions<{ + user: UserEntityObjectWithGroupT; + }>, + ), + }); } private async signUpCustomer( @@ -131,6 +143,17 @@ class AuthController extends Controller { payload: await this.authService.getCurrent(options.user), }; } + + private async logOut( + options: ApiHandlerOptions<{ + user: UserEntityObjectWithGroupT; + }>, + ): Promise { + return { + status: HttpCode.NO_CONTENT, + payload: await this.authService.logOut(options.user), + }; + } } export { AuthController }; diff --git a/backend/src/packages/auth/auth.service.ts b/backend/src/packages/auth/auth.service.ts index 1b891ed10..88892629c 100644 --- a/backend/src/packages/auth/auth.service.ts +++ b/backend/src/packages/auth/auth.service.ts @@ -223,6 +223,10 @@ class AuthService { return user; } + public async logOut(user: UserEntityObjectWithGroupT): Promise { + return await this.userService.removeAccessToken(user.id); + } + public async generateAccessTokenAndUpdateUser( userId: UserEntityT['id'], ): Promise { diff --git a/backend/src/packages/users/user.service.ts b/backend/src/packages/users/user.service.ts index 88fe7c2cb..2ba647770 100644 --- a/backend/src/packages/users/user.service.ts +++ b/backend/src/packages/users/user.service.ts @@ -146,6 +146,10 @@ class UserService implements IService { return UserEntity.initialize(result).toObject(); } + + public async removeAccessToken(id: UserEntityT['id']): Promise { + await this.userRepository.update(id, { accessToken: null }); + } } export { UserService }; diff --git a/frontend/src/libs/components/burger-menu/burger-menu.tsx b/frontend/src/libs/components/burger-menu/burger-menu.tsx index 492a9e889..984ab0ae4 100644 --- a/frontend/src/libs/components/burger-menu/burger-menu.tsx +++ b/frontend/src/libs/components/burger-menu/burger-menu.tsx @@ -1,6 +1,7 @@ -import { IconName } from '~/libs/enums/enums.js'; +import { BurgerMenuItemsName, IconName } from '~/libs/enums/enums.js'; import { getValidClassNames } from '~/libs/helpers/helpers.js'; import { + useAppDispatch, useCallback, useEffect, useLocation, @@ -9,6 +10,7 @@ import { useState, } from '~/libs/hooks/hooks.js'; import { type BurgerMenuItem } from '~/libs/types/types.js'; +import { actions as authActions } from '~/slices/auth/auth.js'; import { Button, Icon } from '../components.js'; import styles from './styles.module.scss'; @@ -21,6 +23,7 @@ const BurgerMenu: React.FC = ({ burgerItems }: Properties) => { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); const navigate = useNavigate(); + const dispatch = useAppDispatch(); const menuReference = useRef(null); @@ -29,10 +32,17 @@ const BurgerMenu: React.FC = ({ burgerItems }: Properties) => { }, [isOpen]); const handleNavigate = useCallback( - (navigateTo: string) => () => navigate(navigateTo), + (navigateTo: string) => () => { + navigate(navigateTo); + }, [navigate], ); + const handleLogOutClick = useCallback( + () => dispatch(authActions.logOut()), + [dispatch], + ); + useEffect(() => { setIsOpen(false); }, [location]); @@ -67,22 +77,28 @@ const BurgerMenu: React.FC = ({ burgerItems }: Properties) => { {isOpen && (
    - {burgerItems.map((item, index) => ( -
  • - -
  • - ))} + {burgerItems.map((item, index) => { + const clickHandler = + item.name === BurgerMenuItemsName.LOG_OUT + ? handleLogOutClick + : handleNavigate(item.navigateTo); + + return ( +
  • +
  • + ); + })}
)} diff --git a/frontend/src/libs/components/burger-menu/styles.module.scss b/frontend/src/libs/components/burger-menu/styles.module.scss index f18b4ef88..417a691cc 100644 --- a/frontend/src/libs/components/burger-menu/styles.module.scss +++ b/frontend/src/libs/components/burger-menu/styles.module.scss @@ -27,18 +27,29 @@ .btn { justify-content: start; + width: 100%; background-color: $blue-dark; border-radius: 0; - &:hover { + &:hover, + &:active, + &:focus { background-color: $blue; } } .menuIcon { width: 100%; + padding: 10px 20px; color: $white; font-size: 24px; + background-color: $blue-dark; + + &:hover, + &:active, + &:focus { + background-color: $blue; + } } } @@ -50,23 +61,17 @@ .btn { display: none; } - - li { - padding: 10px 20px; - - &:hover { - background-color: $blue; - } - } } } @media (width > $screen-size-lg) { - .btn { - display: inline-flex; - } + .menu { + .btn { + display: inline-flex; + } - .menuIcon { - display: none; + .menuIcon { + display: none; + } } } diff --git a/frontend/src/libs/components/button/button.tsx b/frontend/src/libs/components/button/button.tsx index 8f4ffc3dc..66edea8d0 100644 --- a/frontend/src/libs/components/button/button.tsx +++ b/frontend/src/libs/components/button/button.tsx @@ -7,7 +7,7 @@ import styles from './styles.module.scss'; type Properties = { className?: Parameters[0]; - label: string | number; + label?: string | number; type?: 'button' | 'submit'; size?: 'sm' | 'md'; variant?: 'contained' | 'outlined' | 'text'; @@ -24,7 +24,7 @@ const Button: React.FC = ({ type = 'button', size = 'md', variant = 'contained', - label, + label = '', isDisabled = false, isFullWidth = false, frontIcon, diff --git a/frontend/src/libs/components/input/styles.module.scss b/frontend/src/libs/components/input/styles.module.scss index e4fe2c19e..5c64bcd2d 100644 --- a/frontend/src/libs/components/input/styles.module.scss +++ b/frontend/src/libs/components/input/styles.module.scss @@ -87,7 +87,7 @@ $error-message-height: calc($error-font-size * $line-height-coefficient); top: 8px; right: 10px; color: $grey-dark; - background-color: $white; + background-color: transparent; border: 0; cursor: pointer; user-select: none; diff --git a/frontend/src/libs/enums/breakpoint.ts b/frontend/src/libs/enums/breakpoint.ts deleted file mode 100644 index 036ce614a..000000000 --- a/frontend/src/libs/enums/breakpoint.ts +++ /dev/null @@ -1,7 +0,0 @@ -const Breakpoint = { - MOBILE: 768, - TABLET: 1024, - DESKTOP: 1280, -} as const; - -export { Breakpoint }; diff --git a/frontend/src/libs/enums/enums.ts b/frontend/src/libs/enums/enums.ts index af3cd75a8..21ebafa7d 100644 --- a/frontend/src/libs/enums/enums.ts +++ b/frontend/src/libs/enums/enums.ts @@ -1,5 +1,4 @@ export { AppRoute } from './app-route.enum.js'; -export { Breakpoint } from './breakpoint.js'; export { BurgerMenuItemsName } from './burger-menu-items-name.enum.js'; export { DataStatus } from './data-status.enum.js'; export { FormLabel, FormName } from './form.enum.js'; diff --git a/frontend/src/packages/auth/auth-api.ts b/frontend/src/packages/auth/auth-api.ts index a5d300cce..e95367545 100644 --- a/frontend/src/packages/auth/auth-api.ts +++ b/frontend/src/packages/auth/auth-api.ts @@ -77,6 +77,14 @@ class AuthApi extends HttpApi { CustomerSignUpResponseDto | BusinessSignUpResponseDto >(); } + + public async logOut(): Promise { + return await this.load(this.getFullEndpoint(AuthApiPath.LOGOUT, {}), { + method: 'GET', + contentType: ContentType.JSON, + hasAuth: true, + }); + } } export { AuthApi }; diff --git a/frontend/src/slices/auth/actions.ts b/frontend/src/slices/auth/actions.ts index 1595d9818..8ff80593e 100644 --- a/frontend/src/slices/auth/actions.ts +++ b/frontend/src/slices/auth/actions.ts @@ -76,4 +76,21 @@ const getCurrent = createAsyncThunk< } }); -export { getCurrent, signIn, signUp }; +const logOut = createAsyncThunk( + `${sliceName}/logout`, + async (_, { extra, rejectWithValue }) => { + const { authApi, notification, localStorage } = extra; + + try { + await authApi.logOut(); + await localStorage.drop(StorageKey.TOKEN); + } catch (error_: unknown) { + const error = error_ as HttpError; + notification.warning(getErrorMessage(error)); + + return rejectWithValue({ ...error, message: error.message }); + } + }, +); + +export { getCurrent, logOut, signIn, signUp }; diff --git a/frontend/src/slices/auth/auth.slice.ts b/frontend/src/slices/auth/auth.slice.ts index 2a0e3bb58..5a0fc97e5 100644 --- a/frontend/src/slices/auth/auth.slice.ts +++ b/frontend/src/slices/auth/auth.slice.ts @@ -9,7 +9,7 @@ import { type ValueOf, } from '~/libs/types/types.js'; -import { getCurrent, signIn, signUp } from './actions.js'; +import { getCurrent, logOut, signIn, signUp } from './actions.js'; type State = { error: HttpError | null; @@ -70,6 +70,17 @@ const { reducer, actions, name } = createSlice({ builder.addCase(getCurrent.rejected, (state) => { state.dataStatus = DataStatus.REJECTED; }); + builder.addCase(logOut.pending, (state) => { + state.dataStatus = DataStatus.PENDING; + }); + builder.addCase(logOut.fulfilled, (state) => { + state.user = initialState.user; + state.dataStatus = DataStatus.FULFILLED; + }); + builder.addCase(logOut.rejected, (state, { payload }) => { + state.dataStatus = DataStatus.REJECTED; + state.error = payload ?? null; + }); }, }); diff --git a/frontend/src/slices/auth/auth.ts b/frontend/src/slices/auth/auth.ts index b99f56e2d..213e9cdfc 100644 --- a/frontend/src/slices/auth/auth.ts +++ b/frontend/src/slices/auth/auth.ts @@ -1,4 +1,4 @@ -import { getCurrent, signIn, signUp } from './actions.js'; +import { getCurrent, logOut, signIn, signUp } from './actions.js'; import { actions } from './auth.slice.js'; const allActions = { @@ -6,6 +6,7 @@ const allActions = { signIn, signUp, getCurrent, + logOut, }; export { allActions as actions }; diff --git a/shared/src/packages/auth/libs/enums/auth-api-path.enum.ts b/shared/src/packages/auth/libs/enums/auth-api-path.enum.ts index e23ae4f0e..c5470d8ba 100644 --- a/shared/src/packages/auth/libs/enums/auth-api-path.enum.ts +++ b/shared/src/packages/auth/libs/enums/auth-api-path.enum.ts @@ -4,6 +4,7 @@ const AuthApiPath = { SIGN_UP_BUSINESS: '/sign-up/business', SIGN_IN: '/sign-in', CURRENT: '/current', + LOGOUT: '/logout', } as const; export { AuthApiPath };