diff --git a/package.json b/package.json index 0139002..84da454 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App/App.tsx b/src/App/App.tsx index 049aeee..8c4267e 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -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"; @@ -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({ @@ -45,7 +50,7 @@ export const App: FC = (): ReactElement => { } as ToastType); return ( - + <> 404} /> @@ -72,7 +77,7 @@ export const App: FC = (): ReactElement => { - + ); }; diff --git a/src/components/GoogleApi/GoogleAuth.ts b/src/components/GoogleApi/GoogleAuth.ts new file mode 100644 index 0000000..52a949b --- /dev/null +++ b/src/components/GoogleApi/GoogleAuth.ts @@ -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); + } +}; + diff --git a/src/components/GoogleApi/GoogleAuthButton.tsx b/src/components/GoogleApi/GoogleAuthButton.tsx new file mode 100644 index 0000000..57b2849 --- /dev/null +++ b/src/components/GoogleApi/GoogleAuthButton.tsx @@ -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 = ({ + 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 ( + + ); +}; + + diff --git a/src/components/GoogleApi/GoogleDrive.ts b/src/components/GoogleApi/GoogleDrive.ts new file mode 100644 index 0000000..d1705af --- /dev/null +++ b/src/components/GoogleApi/GoogleDrive.ts @@ -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."); + } +} \ No newline at end of file diff --git a/src/components/GoogleApi/index.ts b/src/components/GoogleApi/index.ts new file mode 100644 index 0000000..3560b63 --- /dev/null +++ b/src/components/GoogleApi/index.ts @@ -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'; diff --git a/src/components/GoogleAuth/GoogleAuthC.tsx b/src/components/GoogleAuth/GoogleAuthC.tsx deleted file mode 100644 index 6f0bd44..0000000 --- a/src/components/GoogleAuth/GoogleAuthC.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { FC, ReactElement } from "react"; -import { - googleLogout, - useGoogleLogin, - TokenResponse, -} from "@react-oauth/google"; -import { Button } from "react-bootstrap"; - -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; -} - -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 = ({ - authenticated, - onLogin, - onLogout, - loginVariant = DEFAULT_LOGIN_VARIANT, - logoutVariant = DEFAULT_LOGOUT_VARIANT, - loginText = DEFAULT_LOGIN_TEXT, - logoutText = DEFAULT_LOGOUT_TEXT, -}): ReactElement => { - const login = useGoogleLogin({ - onSuccess: (tokenResponse: TokenResponse) => { - onLogin(tokenResponse); - }, - onError: (error) => { - console.log(error); - }, - }); - - const logout = () => { - onLogout(); - googleLogout(); - }; - - return ( - - ); -}; - -// todo: update this to use the new auth system once it is published -export const fetchRefreshToken = async (refreshToken: string) => { - const response = await fetch( - `https://securetoken.googleapis.com/v1/token?key=${process.env.REACT_APP_G_CLIENT_ID as string}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: `grant_type=refresh_token&refresh_token=${refreshToken}`, - } - ); - return response.json(); -}; - -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) => { - if (autoRefresh) { - logoutTimeout = setTimeout(async () => { - clearLogoutTimer(); - const refreshResponse = await fetchRefreshToken(sessionData.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); - } -}; - - diff --git a/src/components/GoogleAuth/index.ts b/src/components/GoogleAuth/index.ts deleted file mode 100644 index a2747c9..0000000 --- a/src/components/GoogleAuth/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { - GoogleAuthButton, - DEFAULT_LOGIN_TEXT, - DEFAULT_LOGOUT_TEXT, - DEFAULT_LOGIN_VARIANT, - DEFAULT_LOGOUT_VARIANT, - setLogoutTimer, - fetchRefreshToken, -} from "./GoogleAuthC"; -export type { - GoogleAuthProps, - EmptyFunction, - GoogleLoginSuccess, -} from "./GoogleAuthC"; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index d33195f..3250332 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; import { FC, ReactElement } from "react"; import { Button, Offcanvas } from "react-bootstrap"; -import { ABOUT_APP_HEADER, END, LINK_SECONDARY } from "../../strings"; +import { ABOUT_APP_HEADER, END, LINK_SECONDARY, PRIMARY } from "../../strings"; import "./sidebar.scss"; -import { GoogleAuthButton, setLogoutTimer } from "../GoogleAuth"; +import { GoogleAuthButton, listFiles, setLogoutTimer } from "../GoogleApi"; import { AboutModal } from "../AboutModal"; import store from "../../store/store"; import { @@ -11,8 +11,7 @@ import { deauthenticate, useSession, } from "../../store/Session"; -import { TokenResponse } from "@react-oauth/google"; -import { clearLogoutTimer } from "../GoogleAuth/GoogleAuthC"; +import { clearLogoutTimer, TokenResponse } from "../GoogleApi"; /** * Sidebar Component @@ -33,7 +32,7 @@ export const Sidebar: FC = ({ toggleSidebar, }): ReactElement => { const session = useSession(); - const { authenticated: isAuthenticated } = session; + const { authenticated: isAuthenticated, data } = session; const [authenticated, setAuthenticated] = useState(isAuthenticated); // const [rememberMe, setRememberMe] = useState(autoRefresh); @@ -78,6 +77,7 @@ export const Sidebar: FC = ({ authenticated={authenticated} onLogin={handleLogin} onLogout={handleLogout} + tokenData={data.access_token && data} /> {/* = ({ + + + setShowAbout(false)} />

diff --git a/yarn.lock b/yarn.lock index 6c8aa68..4d32c7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,11 +1584,6 @@ dependencies: "@babel/runtime" "^7.6.2" -"@react-oauth/google@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.5.1.tgz#fc5cf33bff5d59583c5b0ed387431350216dd907" - integrity sha512-XCMMke24klAHIVnrZAMibodyjSUsxBOJ+vO5yvRptWC2Vnq02uLUnydjtIdCzCUIAxbvbFbQWZxG0xF0Y8GtHA== - "@reduxjs/toolkit@^1.8.2": version "1.9.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e"