diff --git a/.env.example b/.env.example index 66936a88..507a9663 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ -API_URL="http://localhost:8000/v1/" \ No newline at end of file +API_URL=http://localhost:8000 +AUTH_PROVIDER=dummyAuthProvider \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index b171c906..7089926e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,8 +23,10 @@ COPY --from=build /app/dist /usr/share/nginx/html COPY ./docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -ARG API_URL=http://localhost:8000/v1 +ARG API_URL=http://localhost:8000 +ARG AUTH_PROVIDER=dummyAuthProvider ENV SYNCMASTER__UI__API_BROWSER_URL=${API_URL} +ENV SYNCMASTER__UI__AUTH_PROVIDER=${AUTH_PROVIDER} ENTRYPOINT ["/entrypoint.sh"] CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 174cd138..250cb2c0 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -3,6 +3,7 @@ cat < /usr/share/nginx/html/env-config.js window.env = { API_URL: "${SYNCMASTER__UI__API_BROWSER_URL}", + AUTH_PROVIDER: "${SYNCMASTER__UI__AUTH_PROVIDER}", }; EOF diff --git a/src/app/config/errorBoundary/Fallback.tsx b/src/app/config/errorBoundary/Fallback.tsx index aef7e78e..acd7ba92 100644 --- a/src/app/config/errorBoundary/Fallback.tsx +++ b/src/app/config/errorBoundary/Fallback.tsx @@ -1,20 +1,24 @@ import React from 'react'; import { FallbackProps } from 'react-error-boundary'; import { ErrorLayout } from '@app/layouts'; -import { Error } from '@shared/config'; +import { ErrorStatusCode } from '@shared/config'; +import { AUTH_PROVIDER, AuthProviderType } from '@shared/constants'; -import { AccessError, AuthError, NotFoundError, ServerError } from './errors'; +import { AccessError, AuthError, KeycloakAuthError, NotFoundError, ServerError } from './errors'; import { ErrorBoundaryContext } from './constants'; export const Fallback = ({ error, resetErrorBoundary }: FallbackProps) => { const renderError = () => { if ('status' in error) { switch (error.status) { - case Error.AUTH: - return ; - case Error.ACCESS: + case ErrorStatusCode.AUTH: + if (AUTH_PROVIDER === AuthProviderType.DUMMY) { + return ; + } + return ; + case ErrorStatusCode.ACCESS: return ; - case Error.NOT_FOUND: + case ErrorStatusCode.NOT_FOUND: return ; default: return ; diff --git a/src/app/config/errorBoundary/errors/AuthError.tsx b/src/app/config/errorBoundary/errors/AuthError.tsx index 98c8b9a8..d0902e2d 100644 --- a/src/app/config/errorBoundary/errors/AuthError.tsx +++ b/src/app/config/errorBoundary/errors/AuthError.tsx @@ -1,6 +1,7 @@ -import { useEffect } from 'react'; +import React, { useLayoutEffect } from 'react'; import { useLogout } from '@entities/auth'; import { useSelectedGroup } from '@entities/group'; +import { SpinOverlay } from '@shared/ui'; import { useErrorBoundaryContext } from '../hooks'; @@ -9,11 +10,11 @@ export const AuthError = () => { const { cleanGroup } = useSelectedGroup(); const { resetErrorBoundary } = useErrorBoundaryContext(); - useEffect(() => { + useLayoutEffect(() => { resetErrorBoundary(); cleanGroup(); logout(); }, [logout, cleanGroup, resetErrorBoundary]); - return null; + return ; }; diff --git a/src/app/config/errorBoundary/errors/KeycloakAuthError.tsx b/src/app/config/errorBoundary/errors/KeycloakAuthError.tsx new file mode 100644 index 00000000..53fb8e78 --- /dev/null +++ b/src/app/config/errorBoundary/errors/KeycloakAuthError.tsx @@ -0,0 +1,19 @@ +import React, { useEffect } from 'react'; +import { useKeycloakLogout } from '@entities/auth'; +import { SpinOverlay } from '@shared/ui'; + +import { AuthError } from './AuthError'; + +export const KeycloakAuthError = () => { + const { mutate: logout, isSuccess } = useKeycloakLogout(); + + useEffect(() => { + logout(null); + }, [logout]); + + if (!isSuccess) { + return ; + } + + return ; +}; diff --git a/src/app/config/errorBoundary/errors/index.ts b/src/app/config/errorBoundary/errors/index.ts index 258f3604..4af7b240 100644 --- a/src/app/config/errorBoundary/errors/index.ts +++ b/src/app/config/errorBoundary/errors/index.ts @@ -2,3 +2,4 @@ export * from './NotFoundError'; export * from './ServerError'; export * from './AuthError'; export * from './AccessError'; +export * from './KeycloakAuthError'; diff --git a/src/app/config/router/instance.tsx b/src/app/config/router/instance.tsx index d7492e03..d013826c 100644 --- a/src/app/config/router/instance.tsx +++ b/src/app/config/router/instance.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { createBrowserRouter, Navigate } from 'react-router-dom'; -import { LoginPage } from '@pages/auth'; +import { KeycloakCallbackPage, LoginPage } from '@pages/auth'; import { AuthLayout, ErrorLayout, PrivateLayout } from '@app/layouts'; import { CreateGroupPage, GroupDetailPage, GroupListPage, UpdateGroupPage } from '@pages/group'; import { AuthProvider } from '@entities/auth'; @@ -33,6 +33,10 @@ export const router = createBrowserRouter([ path: '/login', element: , }, + { + path: '/auth/callback', + element: , + }, ], }, { diff --git a/src/app/config/router/permissions/utils.ts b/src/app/config/router/permissions/utils.ts index 0754fdd6..de92b373 100644 --- a/src/app/config/router/permissions/utils.ts +++ b/src/app/config/router/permissions/utils.ts @@ -1,3 +1,9 @@ -import { Storage } from '@shared/constants'; +import { AUTH_PROVIDER, AuthProviderType, Storage } from '@shared/constants'; -export const isAuthenticated = () => !!localStorage.getItem(Storage.ACCESS_TOKEN); +export const isAuthenticated = () => { + if (AUTH_PROVIDER === AuthProviderType.DUMMY) { + return !!localStorage.getItem(Storage.ACCESS_TOKEN); + } + + return !!localStorage.getItem(Storage.IS_KEYCLOAK_AUTH); +}; diff --git a/src/app/layouts/PrivateLayout/index.tsx b/src/app/layouts/PrivateLayout/index.tsx index 76ec3af6..52a4b6a1 100644 --- a/src/app/layouts/PrivateLayout/index.tsx +++ b/src/app/layouts/PrivateLayout/index.tsx @@ -1,8 +1,8 @@ -import React, { Suspense } from 'react'; +import React from 'react'; import { Layout } from 'antd'; import { Outlet } from 'react-router-dom'; import { Header, Sidebar } from '@widgets/layout'; -import { SpinOverlay } from '@shared/ui'; +import { SpinOverlay, Suspense } from '@shared/ui'; import { useSelectedGroup } from '@entities/group'; import classes from './styles.module.less'; @@ -23,7 +23,7 @@ export const PrivateLayout = () => { - }> + diff --git a/src/entities/auth/api/authService.ts b/src/entities/auth/api/authService.ts index 7f711224..62c95a58 100644 --- a/src/entities/auth/api/authService.ts +++ b/src/entities/auth/api/authService.ts @@ -1,6 +1,6 @@ import { axiosInstance } from '@shared/config'; -import { AuthUser, LoginRequest, LoginResponse } from './types'; +import { AuthUser, KeycloakCallbackRequest, LoginRequest, LoginResponse } from './types'; export const authService = { login: (data: LoginRequest): Promise => { @@ -9,4 +9,10 @@ export const authService = { getCurrentUserInfo: (): Promise => { return axiosInstance.get('v1/users/me'); }, + keycloakCallback: (params: KeycloakCallbackRequest): Promise => { + return axiosInstance.get('v1/auth/callback', { params }); + }, + keycloakLogout: (): Promise => { + return axiosInstance.get('v1/auth/logout'); + }, }; diff --git a/src/entities/auth/api/hooks/index.ts b/src/entities/auth/api/hooks/index.ts index 70ac9d75..25b23177 100644 --- a/src/entities/auth/api/hooks/index.ts +++ b/src/entities/auth/api/hooks/index.ts @@ -1 +1,3 @@ export * from './useCurrentUserInfo'; +export * from './useKeycloakCallback'; +export * from './useKeycloakLogout'; diff --git a/src/entities/auth/api/hooks/useCurrentUserInfo/index.ts b/src/entities/auth/api/hooks/useCurrentUserInfo/index.ts index 9803da2f..a6dc5e43 100644 --- a/src/entities/auth/api/hooks/useCurrentUserInfo/index.ts +++ b/src/entities/auth/api/hooks/useCurrentUserInfo/index.ts @@ -1,17 +1,16 @@ -import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { Storage } from '@shared/constants'; +import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; import { authService } from '../../authService'; import { AuthUser } from '../../types'; import { AuthQueryKey } from '../../keys'; /** Hook for getting current user info from backend */ -export const useCurrentUserInfo = (): UseQueryResult => { - const accessToken = localStorage.getItem(Storage.ACCESS_TOKEN); - +export const useCurrentUserInfo = ( + options?: Pick, 'enabled' | 'throwOnError'>, +): UseQueryResult => { return useQuery({ queryKey: [AuthQueryKey.GET_CURRENT_USER_INFO], queryFn: authService.getCurrentUserInfo, - enabled: !!accessToken, + ...options, }); }; diff --git a/src/entities/auth/api/hooks/useKeycloakCallback/index.ts b/src/entities/auth/api/hooks/useKeycloakCallback/index.ts new file mode 100644 index 00000000..6a7b5a97 --- /dev/null +++ b/src/entities/auth/api/hooks/useKeycloakCallback/index.ts @@ -0,0 +1,14 @@ +import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query'; + +import { authService } from '../../authService'; +import { KeycloakCallbackRequest } from '../../types'; +import { AuthQueryKey } from '../../keys'; + +/** Hook for getting auth tokens from keycloak callback from backend */ +export const useKeycloakCallback = (params: KeycloakCallbackRequest): UseSuspenseQueryResult => { + return useSuspenseQuery({ + queryKey: [AuthQueryKey.KEYCLOAK_CALLBACK], + queryFn: () => authService.keycloakCallback(params), + gcTime: 0, + }); +}; diff --git a/src/entities/auth/api/hooks/useKeycloakLogout/index.ts b/src/entities/auth/api/hooks/useKeycloakLogout/index.ts new file mode 100644 index 00000000..0cdfde43 --- /dev/null +++ b/src/entities/auth/api/hooks/useKeycloakLogout/index.ts @@ -0,0 +1,11 @@ +import { UseMutationResult, useMutation } from '@tanstack/react-query'; + +import { authService } from '../../authService'; + +/** Hook for logout from keycloak */ +export const useKeycloakLogout = (): UseMutationResult => { + return useMutation({ + mutationFn: authService.keycloakLogout, + throwOnError: true, + }); +}; diff --git a/src/entities/auth/api/keys/index.ts b/src/entities/auth/api/keys/index.ts index 10b35723..271300f5 100644 --- a/src/entities/auth/api/keys/index.ts +++ b/src/entities/auth/api/keys/index.ts @@ -1,4 +1,5 @@ export const AuthQueryKey = { LOGIN: 'LOGIN', GET_CURRENT_USER_INFO: 'GET_CURRENT_USER_INFO', + KEYCLOAK_CALLBACK: 'KEYCLOAK_CALLBACK', } as const; diff --git a/src/entities/auth/api/types.ts b/src/entities/auth/api/types.ts index 27c01598..b8431e06 100644 --- a/src/entities/auth/api/types.ts +++ b/src/entities/auth/api/types.ts @@ -13,3 +13,7 @@ export interface LoginResponse { access_token: string; token_type: string; } + +export interface KeycloakCallbackRequest { + code: string; +} diff --git a/src/entities/auth/hooks/index.ts b/src/entities/auth/hooks/index.ts index ac55f7d3..fb176a9c 100644 --- a/src/entities/auth/hooks/index.ts +++ b/src/entities/auth/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useAuth'; export * from './useLogout'; export * from './useLogin'; +export * from './useKeycloakLogin'; diff --git a/src/entities/auth/hooks/useKeycloakLogin/index.ts b/src/entities/auth/hooks/useKeycloakLogin/index.ts new file mode 100644 index 00000000..9f5929ba --- /dev/null +++ b/src/entities/auth/hooks/useKeycloakLogin/index.ts @@ -0,0 +1,13 @@ +import { Storage } from '@shared/constants'; +import { useNavigate } from 'react-router-dom'; + +export const useKeycloakLogin = () => { + const navigate = useNavigate(); + + const login = () => { + localStorage.setItem(Storage.IS_KEYCLOAK_AUTH, 'true'); + navigate('/connections'); + }; + + return login; +}; diff --git a/src/features/auth/KeycloakCallback/index.tsx b/src/features/auth/KeycloakCallback/index.tsx new file mode 100644 index 00000000..88117809 --- /dev/null +++ b/src/features/auth/KeycloakCallback/index.tsx @@ -0,0 +1,13 @@ +import { useKeycloakCallback, useKeycloakLogin } from '@entities/auth'; +import { useLayoutEffect } from 'react'; + +import { KeycloakCallbackProps } from './types'; + +export const KeycloakCallback = (props: KeycloakCallbackProps) => { + useKeycloakCallback(props); + const login = useKeycloakLogin(); + + useLayoutEffect(login, [login]); + + return null; +}; diff --git a/src/features/auth/KeycloakCallback/types.ts b/src/features/auth/KeycloakCallback/types.ts new file mode 100644 index 00000000..0a178071 --- /dev/null +++ b/src/features/auth/KeycloakCallback/types.ts @@ -0,0 +1,3 @@ +export interface KeycloakCallbackProps { + code: string; +} diff --git a/src/features/auth/KeycloakLogin/index.tsx b/src/features/auth/KeycloakLogin/index.tsx new file mode 100644 index 00000000..2c6d31c9 --- /dev/null +++ b/src/features/auth/KeycloakLogin/index.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import { useCurrentUserInfo, useKeycloakLogin } from '@entities/auth'; +import { Button, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import classes from './styles.module.less'; + +const { Title } = Typography; + +export const KeycloakLogin = () => { + const { t } = useTranslation('auth'); + const login = useKeycloakLogin(); + const [isEnabledRequest, setIsEnabledRequest] = useState(false); + const { isSuccess, isEnabled, isFetching } = useCurrentUserInfo({ enabled: isEnabledRequest, throwOnError: false }); + + useEffect(() => { + if (isSuccess && isEnabled) { + login(); + } + }, [login, isSuccess, isEnabled]); + + const handleLogin = () => { + setIsEnabledRequest(true); + }; + + return ( +
+ {t('auth')} + +
+ ); +}; diff --git a/src/features/auth/KeycloakLogin/styles.module.less b/src/features/auth/KeycloakLogin/styles.module.less new file mode 100644 index 00000000..b8a476b7 --- /dev/null +++ b/src/features/auth/KeycloakLogin/styles.module.less @@ -0,0 +1,10 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: 32px; + width: 420px; + padding: 32px; + background-color: @white; + border-radius: 16px; + box-shadow: @box-shadow-base; +} diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index a10c3a83..cc1f46bd 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1 +1,3 @@ export * from './Login'; +export * from './KeycloakLogin'; +export * from './KeycloakCallback'; diff --git a/src/pages/auth/KeycloakCallbackPage/index.tsx b/src/pages/auth/KeycloakCallbackPage/index.tsx new file mode 100644 index 00000000..01757c02 --- /dev/null +++ b/src/pages/auth/KeycloakCallbackPage/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { KeycloakCallback } from '@features/auth'; +import { useSearchParams } from 'react-router-dom'; +import { Suspense } from '@shared/ui'; + +export const KeycloakCallbackPage = () => { + const [searchParams] = useSearchParams(); + const code = searchParams.get('code')!; + + return ( + + + + ); +}; diff --git a/src/pages/auth/LoginPage/index.tsx b/src/pages/auth/LoginPage/index.tsx index 33460b29..ea100ec1 100644 --- a/src/pages/auth/LoginPage/index.tsx +++ b/src/pages/auth/LoginPage/index.tsx @@ -1,4 +1,11 @@ -import { Login } from '@features/auth'; +import { KeycloakLogin, Login } from '@features/auth'; +import { AUTH_PROVIDER, AuthProviderType } from '@shared/constants'; import React from 'react'; -export const LoginPage = () => ; +export const LoginPage = () => { + if (AUTH_PROVIDER === AuthProviderType.DUMMY) { + return ; + } + + return ; +}; diff --git a/src/pages/auth/index.ts b/src/pages/auth/index.ts index f772190e..64cbe1dc 100644 --- a/src/pages/auth/index.ts +++ b/src/pages/auth/index.ts @@ -1 +1,2 @@ export * from './LoginPage'; +export * from './KeycloakCallbackPage'; diff --git a/src/shared/config/axios/instance.ts b/src/shared/config/axios/instance.ts index 9e12b1e5..4a7087df 100644 --- a/src/shared/config/axios/instance.ts +++ b/src/shared/config/axios/instance.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import { requestInterceptor, responseSuccessInterceptor } from './interceptors'; +import { requestInterceptor, responseErrorInterceptor, responseSuccessInterceptor } from './interceptors'; export const axiosInstance = axios.create({ baseURL: window.env?.API_URL || process.env.API_URL || 'http://localhost:8000', @@ -8,8 +8,9 @@ export const axiosInstance = axios.create({ Accept: 'application/json', 'Content-Type': 'application/json', }, + withCredentials: true, }); axiosInstance.interceptors.request.use(requestInterceptor); -axiosInstance.interceptors.response.use(responseSuccessInterceptor); +axiosInstance.interceptors.response.use(responseSuccessInterceptor, responseErrorInterceptor); diff --git a/src/shared/config/axios/interceptors/request.ts b/src/shared/config/axios/interceptors/request.ts index 18d3a54f..896b585c 100644 --- a/src/shared/config/axios/interceptors/request.ts +++ b/src/shared/config/axios/interceptors/request.ts @@ -1,13 +1,10 @@ -import { Storage } from '@shared/constants'; +import { AuthProviderType, AUTH_PROVIDER, Storage } from '@shared/constants'; import { InternalAxiosRequestConfig } from 'axios'; export const requestInterceptor = (config: InternalAxiosRequestConfig) => { - const accessToken = localStorage.getItem(Storage.ACCESS_TOKEN); - - if (!accessToken) { - return config; + if (AUTH_PROVIDER === AuthProviderType.DUMMY) { + config.headers.Authorization = `Bearer ${localStorage.getItem(Storage.ACCESS_TOKEN)}`; } - config.headers.Authorization = `Bearer ${accessToken}`; return config; }; diff --git a/src/shared/config/axios/interceptors/response.ts b/src/shared/config/axios/interceptors/response.ts index cc17a3b1..833c487a 100644 --- a/src/shared/config/axios/interceptors/response.ts +++ b/src/shared/config/axios/interceptors/response.ts @@ -1,3 +1,14 @@ -import { AxiosResponse } from 'axios'; +import { checkIsKeycloakUnauthorizedRedirectError } from '@shared/config'; +import { AxiosError, AxiosResponse } from 'axios'; export const responseSuccessInterceptor = (response: AxiosResponse) => response.data; + +export const responseErrorInterceptor = (error: AxiosError) => { + if (checkIsKeycloakUnauthorizedRedirectError(error)) { + localStorage.clear(); + window.location.href = error.response!.data.error.details; + return Promise.resolve(); + } + + return Promise.reject(error); +}; diff --git a/src/shared/config/errors/classes/accessError.ts b/src/shared/config/errors/classes/accessError.ts index 6253cc0f..856e4b65 100644 --- a/src/shared/config/errors/classes/accessError.ts +++ b/src/shared/config/errors/classes/accessError.ts @@ -1,7 +1,7 @@ -import { Error as ErrorType } from '../constants'; +import { ErrorStatusCode } from '../constants'; export class AccessError extends Error { - status = ErrorType.ACCESS; + status = ErrorStatusCode.ACCESS; constructor(message = 'Access error') { super(message); diff --git a/src/shared/config/errors/constants.ts b/src/shared/config/errors/constants.ts index f9b3abcb..148ee4f1 100644 --- a/src/shared/config/errors/constants.ts +++ b/src/shared/config/errors/constants.ts @@ -1,4 +1,4 @@ -export const Error = { +export const ErrorStatusCode = { AUTH: 401, ACCESS: 403, NOT_FOUND: 404, diff --git a/src/shared/config/errors/guards/checkIsKeycloakUnauthorizedError/index.ts b/src/shared/config/errors/guards/checkIsKeycloakUnauthorizedError/index.ts new file mode 100644 index 00000000..0db35977 --- /dev/null +++ b/src/shared/config/errors/guards/checkIsKeycloakUnauthorizedError/index.ts @@ -0,0 +1,25 @@ +import { AxiosError } from 'axios'; +import { AUTH_PROVIDER, AuthProviderType } from '@shared/constants'; + +import { KeycloakUnauthorizedRedirectError } from '../../types'; +import { ErrorStatusCode } from '../../constants'; + +export const checkIsKeycloakUnauthorizedRedirectError = ( + error: unknown, +): error is AxiosError => { + if (AUTH_PROVIDER !== AuthProviderType.KEYCLOAK) { + return false; + } + + const keycloakUnauthorizedError = error as AxiosError; + + if (keycloakUnauthorizedError.response?.status !== ErrorStatusCode.AUTH) { + return false; + } + + if (keycloakUnauthorizedError.response?.data?.error.code !== 'unauthorized') { + return false; + } + + return !!keycloakUnauthorizedError.response?.data.error.details; +}; diff --git a/src/shared/config/errors/guards/index.ts b/src/shared/config/errors/guards/index.ts index f6ccaa1d..c2cc1028 100644 --- a/src/shared/config/errors/guards/index.ts +++ b/src/shared/config/errors/guards/index.ts @@ -1,2 +1,3 @@ export * from './checkIsFormFieldsError'; export * from './checkIsMessageError'; +export * from './checkIsKeycloakUnauthorizedError'; diff --git a/src/shared/config/errors/types.ts b/src/shared/config/errors/types.ts index fa83e0a7..f8dad7ce 100644 --- a/src/shared/config/errors/types.ts +++ b/src/shared/config/errors/types.ts @@ -18,3 +18,11 @@ export interface FormFieldsError { details: FormFieldError[]; }; } + +export interface KeycloakUnauthorizedRedirectError { + error: { + code: 'unauthorized'; + message: string; + details: string; + }; +} diff --git a/src/shared/config/tanstackQuery/instance.ts b/src/shared/config/tanstackQuery/instance.ts index b9d4a7ff..78b4fb6a 100644 --- a/src/shared/config/tanstackQuery/instance.ts +++ b/src/shared/config/tanstackQuery/instance.ts @@ -1,7 +1,7 @@ import { QueryClient } from '@tanstack/react-query'; import axios from 'axios'; -import { Error } from '../errors'; +import { ErrorStatusCode } from '../errors'; export const queryClient = new QueryClient({ defaultOptions: { @@ -14,7 +14,7 @@ export const queryClient = new QueryClient({ throwOnError: true, }, mutations: { - throwOnError: (error) => axios.isAxiosError(error) && error.status === Error.AUTH, + throwOnError: (error) => axios.isAxiosError(error) && error.status === ErrorStatusCode.AUTH, }, }, }); diff --git a/src/shared/constants/auth.ts b/src/shared/constants/auth.ts new file mode 100644 index 00000000..b15b81db --- /dev/null +++ b/src/shared/constants/auth.ts @@ -0,0 +1,9 @@ +/** Types of auth providers for users' authentication */ +export const AuthProviderType = { + /** Default auth provider */ + DUMMY: 'dummyAuthProvider', + /** Keycloak auth provider */ + KEYCLOAK: 'keycloakAuthProvider', +}; + +export const AUTH_PROVIDER = window.env?.AUTH_PROVIDER || process.env.AUTH_PROVIDER || AuthProviderType.DUMMY; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 345ffeb2..54477203 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -3,3 +3,4 @@ export * from './antd'; export * from './regexp'; export * from './role'; export * from './errorMessage'; +export * from './auth'; diff --git a/src/shared/constants/storage.ts b/src/shared/constants/storage.ts index 83403c38..d22c877d 100644 --- a/src/shared/constants/storage.ts +++ b/src/shared/constants/storage.ts @@ -1,3 +1,4 @@ export const Storage = { ACCESS_TOKEN: 'ACCESS_TOKEN', + IS_KEYCLOAK_AUTH: 'IS_KEYCLOAK_AUTH', } as const; diff --git a/src/shared/ui/Suspense/index.tsx b/src/shared/ui/Suspense/index.tsx new file mode 100644 index 00000000..9059e9d5 --- /dev/null +++ b/src/shared/ui/Suspense/index.tsx @@ -0,0 +1,7 @@ +import React, { PropsWithChildren } from 'react'; + +import { SpinOverlay } from '../SpinOverlay'; + +export const Suspense = ({ children }: PropsWithChildren) => { + return }>{children}; +}; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index e36e18d4..24048984 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -16,3 +16,4 @@ export * from './Fieldset'; export * from './CanvasNode'; export * from './Select'; export * from './ActionButton'; +export * from './Suspense'; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 06aabe44..9f0f9e72 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -11,5 +11,6 @@ declare module "*.svg" { interface Window { env?: { API_URL: string; + AUTH_PROVIDER: "dummyAuthProvider" | "keycloakAuthProvider"; }; } \ No newline at end of file diff --git a/src/widgets/layout/Header/components/LogoutButton/hooks/index.ts b/src/widgets/layout/Header/components/LogoutButton/hooks/index.ts new file mode 100644 index 00000000..b9006ead --- /dev/null +++ b/src/widgets/layout/Header/components/LogoutButton/hooks/index.ts @@ -0,0 +1 @@ +export * from './useLogoutButton'; diff --git a/src/widgets/layout/Header/components/LogoutButton/hooks/useLogoutButton/index.ts b/src/widgets/layout/Header/components/LogoutButton/hooks/useLogoutButton/index.ts new file mode 100644 index 00000000..3a5cda36 --- /dev/null +++ b/src/widgets/layout/Header/components/LogoutButton/hooks/useLogoutButton/index.ts @@ -0,0 +1,30 @@ +import { useLogout, useKeycloakLogout } from '@entities/auth'; +import { useSelectedGroup } from '@entities/group'; +import { AUTH_PROVIDER, AuthProviderType } from '@shared/constants'; + +export const useLogoutButton = () => { + const logout = useLogout(); + const { mutate: logoutKeycloak, isPending } = useKeycloakLogout(); + const { cleanGroup } = useSelectedGroup(); + + const handleLogout = () => { + cleanGroup(); + logout(); + }; + + const handleKeycloakLogout = () => { + logoutKeycloak(null, { + onSuccess: handleLogout, + }); + }; + + const handleClick = () => { + if (AUTH_PROVIDER === AuthProviderType.DUMMY) { + handleLogout(); + } else { + handleKeycloakLogout(); + } + }; + + return { handleClick, isPending }; +}; diff --git a/src/widgets/layout/Header/components/LogoutButton/index.tsx b/src/widgets/layout/Header/components/LogoutButton/index.tsx index aeb4eff2..34fad5e5 100644 --- a/src/widgets/layout/Header/components/LogoutButton/index.tsx +++ b/src/widgets/layout/Header/components/LogoutButton/index.tsx @@ -1,19 +1,19 @@ import React from 'react'; import { LogoutOutlined } from '@ant-design/icons'; -import { useLogout } from '@entities/auth'; -import { useSelectedGroup } from '@entities/group'; import { Button } from 'antd'; import { useTranslation } from 'react-i18next'; +import { SpinOverlay } from '@shared/ui'; + +import { useLogoutButton } from './hooks'; export const LogoutButton = () => { const { t } = useTranslation('auth'); - const logout = useLogout(); - const { cleanGroup } = useSelectedGroup(); - - const handleClickLogout = () => { - cleanGroup(); - logout(); - }; + const { handleClick, isPending } = useLogoutButton(); - return