Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
API_URL="http://localhost:8000/v1/"
API_URL=http://localhost:8000
AUTH_PROVIDER=dummyAuthProvider
4 changes: 3 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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;"]
Expand Down
1 change: 1 addition & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
cat <<EOF > /usr/share/nginx/html/env-config.js
window.env = {
API_URL: "${SYNCMASTER__UI__API_BROWSER_URL}",
AUTH_PROVIDER: "${SYNCMASTER__UI__AUTH_PROVIDER}",
};
EOF

Expand Down
16 changes: 10 additions & 6 deletions src/app/config/errorBoundary/Fallback.tsx
Original file line number Diff line number Diff line change
@@ -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 <AuthError />;
case Error.ACCESS:
case ErrorStatusCode.AUTH:
if (AUTH_PROVIDER === AuthProviderType.DUMMY) {
return <AuthError />;
}
return <KeycloakAuthError />;
case ErrorStatusCode.ACCESS:
return <AccessError />;
case Error.NOT_FOUND:
case ErrorStatusCode.NOT_FOUND:
return <NotFoundError />;
default:
return <ServerError />;
Expand Down
7 changes: 4 additions & 3 deletions src/app/config/errorBoundary/errors/AuthError.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -9,11 +10,11 @@ export const AuthError = () => {
const { cleanGroup } = useSelectedGroup();
const { resetErrorBoundary } = useErrorBoundaryContext();

useEffect(() => {
useLayoutEffect(() => {
resetErrorBoundary();
cleanGroup();
logout();
}, [logout, cleanGroup, resetErrorBoundary]);

return null;
return <SpinOverlay />;
};
19 changes: 19 additions & 0 deletions src/app/config/errorBoundary/errors/KeycloakAuthError.tsx
Original file line number Diff line number Diff line change
@@ -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 <SpinOverlay />;
}

return <AuthError />;
};
1 change: 1 addition & 0 deletions src/app/config/errorBoundary/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './NotFoundError';
export * from './ServerError';
export * from './AuthError';
export * from './AccessError';
export * from './KeycloakAuthError';
6 changes: 5 additions & 1 deletion src/app/config/router/instance.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -33,6 +33,10 @@ export const router = createBrowserRouter([
path: '/login',
element: <LoginPage />,
},
{
path: '/auth/callback',
element: <KeycloakCallbackPage />,
},
],
},
{
Expand Down
10 changes: 8 additions & 2 deletions src/app/config/router/permissions/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
};
6 changes: 3 additions & 3 deletions src/app/layouts/PrivateLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -23,7 +23,7 @@ export const PrivateLayout = () => {
<Sidebar />
<Layout>
<Content className={classes.layout__content}>
<Suspense fallback={<SpinOverlay />}>
<Suspense>
<Outlet />
</Suspense>
</Content>
Expand Down
8 changes: 7 additions & 1 deletion src/entities/auth/api/authService.ts
Original file line number Diff line number Diff line change
@@ -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<LoginResponse> => {
Expand All @@ -9,4 +9,10 @@ export const authService = {
getCurrentUserInfo: (): Promise<AuthUser> => {
return axiosInstance.get('v1/users/me');
},
keycloakCallback: (params: KeycloakCallbackRequest): Promise<null> => {
return axiosInstance.get('v1/auth/callback', { params });
},
keycloakLogout: (): Promise<null> => {
return axiosInstance.get('v1/auth/logout');
},
};
2 changes: 2 additions & 0 deletions src/entities/auth/api/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './useCurrentUserInfo';
export * from './useKeycloakCallback';
export * from './useKeycloakLogout';
11 changes: 5 additions & 6 deletions src/entities/auth/api/hooks/useCurrentUserInfo/index.ts
Original file line number Diff line number Diff line change
@@ -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<AuthUser> => {
const accessToken = localStorage.getItem(Storage.ACCESS_TOKEN);

export const useCurrentUserInfo = (
options?: Pick<UseQueryOptions<AuthUser>, 'enabled' | 'throwOnError'>,
): UseQueryResult<AuthUser> => {
return useQuery({
queryKey: [AuthQueryKey.GET_CURRENT_USER_INFO],
queryFn: authService.getCurrentUserInfo,
enabled: !!accessToken,
...options,
});
};
14 changes: 14 additions & 0 deletions src/entities/auth/api/hooks/useKeycloakCallback/index.ts
Original file line number Diff line number Diff line change
@@ -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<null> => {
return useSuspenseQuery({
queryKey: [AuthQueryKey.KEYCLOAK_CALLBACK],
queryFn: () => authService.keycloakCallback(params),
gcTime: 0,
});
};
11 changes: 11 additions & 0 deletions src/entities/auth/api/hooks/useKeycloakLogout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UseMutationResult, useMutation } from '@tanstack/react-query';

import { authService } from '../../authService';

/** Hook for logout from keycloak */
export const useKeycloakLogout = (): UseMutationResult<null> => {
return useMutation({
mutationFn: authService.keycloakLogout,
throwOnError: true,
});
};
1 change: 1 addition & 0 deletions src/entities/auth/api/keys/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const AuthQueryKey = {
LOGIN: 'LOGIN',
GET_CURRENT_USER_INFO: 'GET_CURRENT_USER_INFO',
KEYCLOAK_CALLBACK: 'KEYCLOAK_CALLBACK',
} as const;
4 changes: 4 additions & 0 deletions src/entities/auth/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export interface LoginResponse {
access_token: string;
token_type: string;
}

export interface KeycloakCallbackRequest {
code: string;
}
1 change: 1 addition & 0 deletions src/entities/auth/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useAuth';
export * from './useLogout';
export * from './useLogin';
export * from './useKeycloakLogin';
13 changes: 13 additions & 0 deletions src/entities/auth/hooks/useKeycloakLogin/index.ts
Original file line number Diff line number Diff line change
@@ -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;
};
13 changes: 13 additions & 0 deletions src/features/auth/KeycloakCallback/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
3 changes: 3 additions & 0 deletions src/features/auth/KeycloakCallback/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface KeycloakCallbackProps {
code: string;
}
41 changes: 41 additions & 0 deletions src/features/auth/KeycloakLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes.wrapper}>
<Title>{t('auth')}</Title>
<Button
className={classes.button}
type="primary"
size="large"
htmlType="submit"
onClick={handleLogin}
loading={isFetching}
>
{t('signIn')}
</Button>
</div>
);
};
10 changes: 10 additions & 0 deletions src/features/auth/KeycloakLogin/styles.module.less
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/features/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './Login';
export * from './KeycloakLogin';
export * from './KeycloakCallback';
15 changes: 15 additions & 0 deletions src/pages/auth/KeycloakCallbackPage/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense>
<KeycloakCallback code={code} />
</Suspense>
);
};
11 changes: 9 additions & 2 deletions src/pages/auth/LoginPage/index.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => <Login />;
export const LoginPage = () => {
if (AUTH_PROVIDER === AuthProviderType.DUMMY) {
return <Login />;
}

return <KeycloakLogin />;
};
1 change: 1 addition & 0 deletions src/pages/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './LoginPage';
export * from './KeycloakCallbackPage';
5 changes: 3 additions & 2 deletions src/shared/config/axios/instance.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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',
headers: {
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);
Loading