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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"homepage": "https://accessitech.github.io/tracker/",
"license": "MIT",
"dependencies": {
"@react-oauth/google": "^0.5.1",
"@reduxjs/toolkit": "^1.8.2",
"formik": "^2.2.9",
"react": "^18.2.0",
Expand Down
39 changes: 22 additions & 17 deletions src/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { FC, ReactElement, useEffect } from "react";
import { HashRouter, Routes, Route } from "react-router-dom";
import { googleLogout, GoogleOAuthProvider } from '@react-oauth/google';
import { Home } from "../containers/Home";
import { Edit } from "../containers/Edit";
import { Log } from "../containers/Log";
Expand All @@ -10,31 +9,37 @@ import { EDIT_URL, EMPTY, ENTRY_EDIT_URL, ENTRY_URL, FIELD_URL, HOME_URL, LOG_ID
import { Toaster, ToastType } from "../components/Toaster";
import { deauthenticate, useSession } from "../store/Session/reducer";
import store from "../store/store";
import { setLogoutTimer } from "../components/GoogleAuth";
import { authenticateUser, deauthenticateUser, initGoogleAuth, setLogoutTimer } from "../components/GoogleApi";

export const App: FC = (): ReactElement => {
const apiKey = process.env.REACT_APP_G_API_KEY as string;
const clientId = process.env.REACT_APP_G_CLIENT_ID as string;
const session = useSession();
const { authenticated, expiresAt, data } = session;

const handleLogout = (): void => {
googleLogout();
store.dispatch(deauthenticate(''));
deauthenticateUser(() => {
store.dispatch(deauthenticate(''));
})
};

useEffect(() => {
if (authenticated) {
if (expiresAt && expiresAt < Date.now()) {
handleLogout();
} else {
setLogoutTimer({
logoutCallback: handleLogout,
timeout: expiresAt - Date.now(),
// autoRefresh,
sessionData: data,
});
initGoogleAuth({ apiKey, clientId }, () => {
if (authenticated) {
if (expiresAt && expiresAt < Date.now()) {
handleLogout();
} else {
authenticateUser(() => {
setLogoutTimer({
logoutCallback: handleLogout,
timeout: expiresAt - Date.now(),
// autoRefresh,
sessionData: data,
});
}, data.access_token && data);
}
}
}
});
}, []);

const [toast, setToast] = React.useState({
Expand All @@ -45,7 +50,7 @@ export const App: FC = (): ReactElement => {
} as ToastType);

return (
<GoogleOAuthProvider clientId={clientId}>
<>
<HashRouter>
<Routes>
<Route path={WILDCARD} element={<h1>404</h1>} />
Expand All @@ -72,7 +77,7 @@ export const App: FC = (): ReactElement => {
</Routes>
</HashRouter>
<Toaster toast={toast} setToast={setToast} />
</GoogleOAuthProvider>
</>
);
};

Expand Down
164 changes: 164 additions & 0 deletions src/components/GoogleApi/GoogleAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { EmptyFunction } from "./GoogleAuthButton";

export const DISCOVERY_DOC =
"https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";
export const SCOPES = ["https://www.googleapis.com/auth/drive"];

let apiInitialized = false;
let googleInitialized = false;
let tokenClient: any;
let gapi: any;
let google: any;

export type ErrorCode =
| "invalid_request"
| "access_denied"
| "unauthorized_client"
| "unsupported_response_type"
| "invalid_scope"
| "server_error"
| "temporarily_unavailable";

export interface TokenResponse {
/** The access token of a successful token response. */
access_token: string;
/** The lifetime in seconds of the access token. */
expires_in: number;
/** The hosted domain the signed-in user belongs to. */
hd?: string;
/** The prompt value that was used from the possible list of values specified by TokenClientConfig or OverridableTokenClientConfig */
prompt: string;
/** The type of the token issued. */
token_type: string;
/** A space-delimited list of scopes that are approved by the user. */
scope: string;
/** The string value that your application uses to maintain state between your authorization request and the response. */
state?: string;
/** A single ASCII error code. */
error?: ErrorCode;
/** Human-readable ASCII text providing additional information, used to assist the client developer in understanding the error that occurred. */
error_description?: string;
/** A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. */
error_uri?: string;
}

export interface InitGoogleAuthParams {
apiKey: string;
clientId: string;
discoveryDocs?: string[];
}

export const initGoogleAuth = ({
apiKey,
clientId,
discoveryDocs = [DISCOVERY_DOC],
}: InitGoogleAuthParams, callback?:EmptyFunction): void => {
if (!apiInitialized) {
const gapiScript = document.createElement("script");
gapiScript.src = "https://apis.google.com/js/api.js";
gapiScript.onload = () => {
gapi = (window as any).gapi;
gapi.load("client", async () => {
await gapi.client.init({
apiKey,
discoveryDocs,
});
apiInitialized = true;

if (!googleInitialized) {
const googleScript = document.createElement("script");
googleScript.src = "https://accounts.google.com/gsi/client";
googleScript.onload = () => {
google = (window as any).google;
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: clientId,
scope: SCOPES.join(" "),
callback: "",
});
googleInitialized = true;
if (callback) {
callback();
}
};
document.body.appendChild(googleScript);
}
});
};
document.body.appendChild(gapiScript);
}
};

export const authenticateUser = (
tokenCallback: (token: TokenResponse) => void,
tokenData?: TokenResponse
) => {
tokenClient.callback = tokenCallback;
const prompt = tokenData ? "none" : "consent";
tokenClient.requestAccessToken({ prompt });
};

export const deauthenticateUser = (
signOutCallback: () => void,
tokenData?: TokenResponse,
) => {
const token = tokenData || gapi.client.getToken();
google.accounts.oauth2.revoke(token.access_token);
signOutCallback();
};

export const getTokenClient = () => {
return tokenClient;
};

export const getApiClient = () => {
return gapi && gapi.client;
};

export const getGoogle = () => {
return google;
};

let logoutTimeout: any;

export const clearLogoutTimer = () => {
clearTimeout(logoutTimeout);
};

export interface LogoutTimerProps {
logoutCallback: EmptyFunction;
autoRefresh?: boolean;
sessionData: { [key: string]: any };
timeout: number;
}

export const setLogoutTimer = ({
logoutCallback,
autoRefresh = false,
timeout,
}: // sessionData,
LogoutTimerProps) => {
// auto refresh doesn't work yet!
if (autoRefresh) {
logoutTimeout = setTimeout(async () => {
clearLogoutTimer();
// const refreshResponse = await fetchRefreshToken(sessionData.refresh_token);
const refreshResponse = {} as any; // todo: refresh token
if (refreshResponse.error) {
logoutCallback();
} else {
setLogoutTimer({
logoutCallback,
autoRefresh,
timeout: refreshResponse.expires_in * 1000 - 1000 * 60 * 5,
sessionData: refreshResponse,
});
}
}, timeout - 1000 * 60 * 5);
} else {
logoutTimeout = setTimeout(() => {
logoutCallback();
clearLogoutTimer();
}, timeout);
}
};

48 changes: 48 additions & 0 deletions src/components/GoogleApi/GoogleAuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { FC, ReactElement } from "react";
import { Button } from "react-bootstrap";
import { authenticateUser, deauthenticateUser, TokenResponse } from "./GoogleAuth";

export type EmptyFunction = () => void;
export type GoogleLoginSuccess = (tokenResponse: TokenResponse) => void;

export interface GoogleAuthProps {
authenticated: boolean;
onLogout: EmptyFunction;
onLogin: GoogleLoginSuccess;
loginVariant?: string;
logoutVariant?: string;
loginText?: string;
logoutText?: string;
tokenData?: TokenResponse;
}

export const DEFAULT_LOGIN_VARIANT = "outline-primary";
export const DEFAULT_LOGOUT_VARIANT = "outline-danger";
export const DEFAULT_LOGIN_TEXT = "Log In";
export const DEFAULT_LOGOUT_TEXT = "Log Out";

export const GoogleAuthButton: FC<GoogleAuthProps> = ({
authenticated,
onLogin,
onLogout,
loginVariant = DEFAULT_LOGIN_VARIANT,
logoutVariant = DEFAULT_LOGOUT_VARIANT,
loginText = DEFAULT_LOGIN_TEXT,
logoutText = DEFAULT_LOGOUT_TEXT,
tokenData,
}): ReactElement => {

const login = () => authenticateUser(onLogin);
const logout = () => deauthenticateUser(onLogout, tokenData);

return (
<Button
onClick={() => (authenticated ? logout() : login())}
variant={authenticated ? logoutVariant : loginVariant}
>
{authenticated ? logoutText : loginText}
</Button>
);
};


18 changes: 18 additions & 0 deletions src/components/GoogleApi/GoogleDrive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getApiClient } from "./GoogleAuth";

export const listFiles = async () => {
const { drive } = getApiClient();
const response = await drive.files.list({
pageSize: 10,
fields: "nextPageToken, files(id, name)",
});
const files = response.result.files;
if (files && files.length > 0) {
console.log("Files:");
files.map((file: any) => {
console.log(`${file.name} (${file.id})`);
});
} else {
console.log("No files found.");
}
}
38 changes: 38 additions & 0 deletions src/components/GoogleApi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export {
GoogleAuthButton,
DEFAULT_LOGIN_TEXT,
DEFAULT_LOGOUT_TEXT,
DEFAULT_LOGIN_VARIANT,
DEFAULT_LOGOUT_VARIANT,

} from "./GoogleAuthButton";
export type {
GoogleAuthProps,
EmptyFunction,
GoogleLoginSuccess,
} from "./GoogleAuthButton";

export {
DISCOVERY_DOC,
SCOPES,

initGoogleAuth,
authenticateUser,
deauthenticateUser,
getTokenClient,
getApiClient,
getGoogle,

setLogoutTimer,
clearLogoutTimer,
} from './GoogleAuth';

export type {
ErrorCode,
TokenResponse,
InitGoogleAuthParams,
} from './GoogleAuth';

export {
listFiles,
} from './GoogleDrive';
Loading