Skip to content

Commit

Permalink
Replaced the Grand Central cookie-based login mechanism with a bearer…
Browse files Browse the repository at this point in the history
… token

Instead of relying on Grand Central to specify a cookie, we instead set one ourselves, and then use Grand Central's API to specify the token as an Authorization header.
  • Loading branch information
SStorm committed Feb 6, 2024
1 parent 48fd7e7 commit 3f09323
Show file tree
Hide file tree
Showing 19 changed files with 130 additions and 48 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [SQL Scheduler] Move "Running..." from "Last Execution" into "Next Due" column.
- [SQL Scheduler] Fix "Invalid Date" in job logs.
- Migrate to axios and configured interceptor to handle JWT Expire.
- Replaced the Grand Central cookie-based login mechanism with a bearer token.

## 2024-02-01 - 0.4.5

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"react-intl": "^6.5.5",
"react-router-dom": "^6.21.1",
"react-syntax-highlighter": "^15.5.0",
"react-use-cookie": "^1.4.0",
"recharts": "^2.10.4",
"sql-formatter": "^15.0.2",
"swr": "^2.2.4",
Expand Down
20 changes: 17 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@ import { Route, Routes } from 'react-router-dom';
import { bottomNavigation, topNavigation } from './constants/navigation';
import routes from './constants/routes';
import { useMemo, useState } from 'react';
import { ConnectionStatus, isGcConnected } from './utils/gc/connectivity';
import { GCContextProvider } from './contexts';
import { ConnectionStatus, GCContextProvider } from './contexts';
import Layout from './components/Layout';
import StatusBar from './components/StatusBar';
import NotificationHandler from './components/NotificationHandler';
import logo from './assets/logo.svg';
import StatsUpdater from './components/StatsUpdater';
import useGcApi from './hooks/useGcApi.ts';
import { apiGet } from './utils/api.ts';

function App() {
const [gcStatus, setGCStatus] = useState(ConnectionStatus.PENDING);
const gcApi = useGcApi();

useMemo(() => {
isGcConnected(process.env.REACT_APP_GRAND_CENTRAL_URL).then(setGCStatus);
apiGet(gcApi, `/api/`, {})
.then(res => {
if (res.status == 200) {
setGCStatus(ConnectionStatus.CONNECTED);
} else if (res.status == 401) {
setGCStatus(ConnectionStatus.NOT_LOGGED_IN);
} else {
setGCStatus(ConnectionStatus.ERROR);
}
})
.catch(() => {
setGCStatus(ConnectionStatus.ERROR);
});
}, []);

const gcUrl = process.env.REACT_APP_GRAND_CENTRAL_URL;
Expand Down
3 changes: 1 addition & 2 deletions src/components/EnterpriseScreen/EnterpriseScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { Result } from 'antd';
import Button from '../../components/Button';
import { useGCContext } from '../../contexts';
import { ConnectionStatus } from '../../utils/gc/connectivity';
import { ConnectionStatus, useGCContext } from '../../contexts';

type EnterpriseScreenProps = {
children: React.ReactElement;
Expand Down
3 changes: 1 addition & 2 deletions src/components/GCStatusIndicator/GCStatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ConnectionStatus } from '../../utils/gc/connectivity';
import { useGCContext } from '../../contexts';
import { ConnectionStatus, useGCContext } from '../../contexts';

function renderStatus(status: ConnectionStatus) {
const prefix = 'GC';
Expand Down
1 change: 1 addition & 0 deletions src/constants/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GRAND_CENTRAL_TOKEN_COOKIE = 'grand_central_token';
6 changes: 5 additions & 1 deletion src/constants/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Help, Overview, SQLConsole, Tables, Users } from '../routes';
import { Auth, Help, Overview, SQLConsole, Tables, Users } from '../routes';
import EnterpriseScreen from '../components/EnterpriseScreen';
import { Route } from '../types';
import ScheduledJobs from '../routes/JobScheduler';

const routes: Route[] = [
{
path: '/auth',
element: <Auth />,
},
{
path: '/',
element: (
Expand Down
9 changes: 8 additions & 1 deletion src/contexts/GrandCentral.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { PropsWithChildren, useContext } from 'react';
import { ConnectionStatus } from '../utils/gc/connectivity';

type GCContextType = {
gcStatus: ConnectionStatus;
Expand All @@ -9,6 +8,14 @@ type GCContextType = {
onGcApiJwtExpire?: () => Promise<void>;
};

export enum ConnectionStatus {
CONNECTED,
NOT_CONFIGURED,
NOT_LOGGED_IN,
ERROR,
PENDING,
}

const defaultProps: GCContextType = {
gcStatus: ConnectionStatus.PENDING,
gcUrl: process.env.REACT_APP_GRAND_CENTRAL_URL,
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/useExecuteSql.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { QueryResults } from '../types/query';
import { apiPost } from '../utils/api';
import { ConnectionStatus } from '../utils/gc/connectivity';
import useGcApi from './useGcApi';
import useCrateApi from './useCrateApi';
import { useGCContext } from '../contexts';
import { ConnectionStatus, useGCContext } from '../contexts';

export type ExecuteSqlResult = {
data: QueryResults | null;
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useGCLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import useGcApi from './useGcApi.ts';
import { apiGet } from '../utils/api.ts';
import useCookie from 'react-use-cookie';
import { GRAND_CENTRAL_TOKEN_COOKIE } from '../constants/cookie.ts';

export type GCLoginParams = {
token: string;
refresh?: string | null;
};
export default function useGCLogin() {
const gcApi = useGcApi();
const [, setToken] = useCookie(GRAND_CENTRAL_TOKEN_COOKIE);

return async ({ token, refresh }: GCLoginParams) => {
let qs = `?token=${token}`;
if (refresh) {
qs += `&refresh=${refresh}`;
}
try {
const res = await apiGet(gcApi, `/api/auth${qs}`, {});
if (res.success && res.status == 200) {
setToken(token);
return true;
}
return false;
} catch (e) {
return false;
}
};
}
8 changes: 7 additions & 1 deletion src/hooks/useGcApi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import axios from 'axios';
import useCookie from 'react-use-cookie';
import { useGCContext } from '../contexts';
import { GRAND_CENTRAL_TOKEN_COOKIE } from '../constants/cookie.ts';

export default function useGcApi() {
const { gcUrl, onGcApiJwtExpire } = useGCContext();

const [token] = useCookie(GRAND_CENTRAL_TOKEN_COOKIE);

const instance = axios.create({
baseURL: gcUrl,
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`,
},
});

if (onGcApiJwtExpire) {
Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import './index.css';
import { ConnectionStatus } from './utils/gc/connectivity';

export * from './components';
export * from './contexts';
export * from './routes';
export { ConnectionStatus };
45 changes: 45 additions & 0 deletions src/routes/Auth/Auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import useCookie from 'react-use-cookie';
import { GRAND_CENTRAL_TOKEN_COOKIE } from '../../constants/cookie.ts';
import { GCSpin, NoDataView } from '../../components';
import { useMemo, useState } from 'react';
import useGCLogin from '../../hooks/useGCLogin.ts';

function Auth() {
const [, setToken] = useCookie(GRAND_CENTRAL_TOKEN_COOKIE);

const gcLogin = useGCLogin();

const [status, setStatus] = useState<boolean | undefined>(undefined);

const specifiedToken = new URLSearchParams(location.search).get('token');
const specifiedRefreshToken = new URLSearchParams(location.search).get('refresh');

useMemo(() => {
if (!specifiedToken) {
setStatus(false);
return;
}
gcLogin({
token: specifiedToken,
refresh: specifiedRefreshToken,
}).then(success => {
if (success) {
setToken(specifiedToken);
setStatus(true);
window.location.assign('/');
} else {
setStatus(false);
}
});
}, [specifiedToken]);

return (
<GCSpin spinning={status === undefined}>
{status === false && (
<NoDataView description="Could not authenticate to Grand Central: Invalid or no token" />
)}
</GCSpin>
);
}

export default Auth;
3 changes: 3 additions & 0 deletions src/routes/Auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Auth from './Auth';

export default Auth;
1 change: 1 addition & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as Overview } from './Overview';
export { default as SQLConsole } from './SQLConsole';
export { default as Tables } from './Tables';
export { default as Users } from './Users';
export { default as Auth } from './Auth';
4 changes: 2 additions & 2 deletions src/types/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import React from 'react';
export type Route = {
path: string;
element: React.JSX.Element;
label: React.JSX.Element | string;
key: string;
label?: React.JSX.Element | string;
key?: string;
};
30 changes: 0 additions & 30 deletions src/utils/gc/connectivity.ts

This file was deleted.

3 changes: 1 addition & 2 deletions test/testUtils/renderWithTestWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { PropsWithChildren } from 'react';
import { render as rtlRender } from '@testing-library/react';
import userEvent, { UserEvent } from '@testing-library/user-event';
import { GCContextProvider } from '../../src/contexts';
import { ConnectionStatus } from '../../src/utils/gc/connectivity';
import { ConnectionStatus, GCContextProvider } from '../../src/contexts';
import { SWRConfig } from 'swr';

type RenderType = {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5813,6 +5813,11 @@ react-transition-group@2.9.0:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"

react-use-cookie@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/react-use-cookie/-/react-use-cookie-1.4.0.tgz#8f9fffeea9ad48d97dfbb6818aa0c63771946e3a"
integrity sha512-e21YFkimh8OX6Ctqq89YeeiwsIL1jr2GyCeBdkBKDWY5X5o2yonbQuqfFODK4opteJS3F7ya0OuYg06lOeqvJQ==

react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
Expand Down

0 comments on commit 3f09323

Please sign in to comment.