Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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