diff --git a/.eslintrc.js b/.eslintrc.js
index 90fbff819..5deb32bde 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -74,6 +74,7 @@ module.exports = {
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/unbound-method": "off",
+ "arrow-body-style": "off",
"no-unused-expressions": "off",
"no-unused-vars": "off",
"no-use-before-define": "off",
diff --git a/app/App.tsx b/app/App.tsx
index c505f4dc2..a79f3d8de 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -10,7 +10,7 @@ import Intercom from "react-intercom";
import { Helmet } from "react-helmet-async";
import { Redirect, Route, Switch, useHistory } from "react-router-dom";
-import { AppContext, GeoCoordinates, getLocation, whiteLabel } from "./utils";
+import { GeoCoordinates, getLocation, whiteLabel, AppProvider } from "./utils";
import {
Banner,
HamburgerMenu,
@@ -19,6 +19,7 @@ import {
PopupMessageProp,
UserWay,
} from "./components/ui";
+
import config from "./config";
import MetaImage from "./assets/img/sfsg-preview.png";
@@ -43,6 +44,9 @@ import OrganizationEditPage from "./pages/OrganizationEditPage";
import { EditBreakingNewsPage } from "./pages/EditBreakingNewsPage";
import { ServiceDiscoveryForm } from "./pages/ServiceDiscoveryForm";
import { ServiceDiscoveryResults } from "./pages/ServiceDiscoveryResults";
+import { LoginPage } from "./pages/Auth/LoginPage";
+import { SignUpPage } from "./pages/Auth/SignUpPage";
+import { LogoutPage } from "./pages/Auth/LogoutPage";
import styles from "./App.module.scss";
@@ -99,8 +103,7 @@ export const App = () => {
return (
- {/* eslint-disable-next-line react/jsx-no-constructed-context-values */}
-
+
{title}
@@ -144,7 +147,6 @@ export const App = () => {
/>
-
{/* NB: /organizations/new must be listed before /organizations/:id or else the /new
step will be interpreted as an ID and will thus break the OrganizationEditPage */}
{
path="/breaking-news/edit"
component={EditBreakingNewsPage}
/>
+
+
+
{/* UCSF white label paths */}
{
path="/find-services/:selectedResourceSlug"
component={UcsfDiscoveryForm}
/>
-
{/* Legacy redirects */}
{
path="/resource"
component={RedirectToOrganizations}
/>
-
{popUpMessage && }
-
+
);
};
diff --git a/app/components/ui/Navigation.tsx b/app/components/ui/Navigation.tsx
index 68eb96991..c9263a65f 100644
--- a/app/components/ui/Navigation.tsx
+++ b/app/components/ui/Navigation.tsx
@@ -2,8 +2,8 @@ import React, { FormEvent, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import cn from "classnames";
import qs from "qs";
+import { useAppContext, whiteLabel } from "utils";
import Translate from "./Translate";
-import whiteLabel from "../../utils/whitelabel";
import styles from "./Navigation.module.scss";
const {
@@ -89,46 +89,59 @@ const SiteLogo = () =>
);
-const SiteLinks = () => (
-
-
- About
-
-
-
- FAQ
-
-
-
-
- Contact Us
-
-
- {showReportCrisis && (
+const SiteLinks = () => {
+ const { authState } = useAppContext();
+
+ return (
+
+ {/* Todo: This will eventually be replaced by a user icon with a dropdown menu of account related options.
+ The designs are still forthcoming. For now, it serves as a basic log-out functionality for the purposes
+ of development and testing.
+ */}
+ {authState && (
+
+ Log Out
+
+ )}
+
+ About
+
+
+
+ FAQ
+
+
- Report Street Crisis
+ Contact Us
- )}
-
-
-);
+ {showReportCrisis && (
+
+
+ Report Street Crisis
+
+
+ )}
+
+
+ );
+};
const SiteSearch = ({
query,
diff --git a/app/config.ts b/app/config.ts
index d9fa5ab6c..be2ecf972 100644
--- a/app/config.ts
+++ b/app/config.ts
@@ -3,20 +3,35 @@ declare global {
}
interface Config {
+ ALGOLIA_APPLICATION_ID: string;
+ ALGOLIA_INDEX_PREFIX: string;
+ ALGOLIA_READ_ONLY_API_KEY: string;
+ AUTH0_AUDIENCE: string;
+ AUTH0_CLIENT_ID: string;
+ AUTH0_DOMAIN: string;
+ AUTH0_REDIRECT_URI: string;
GOOGLE_ANALYTICS_ID: string;
GOOGLE_ANALYTICS_GA4_ID: string;
+ GOOGLE_API_KEY: string;
+ INTERCOM_APP_ID: string;
LINKSF_DOMAIN: string;
MOHCD_DOMAIN: string;
MOHCD_SUBDOMAIN: string;
+ SENTRY_PROJECT_ID: string;
+ SENTRY_PUBLIC_KEY: string;
SFFAMILIES_DOMAIN: string;
+ SFFAMILIES_USERWAY_APP_ID: string;
UCSF_DOMAIN: string;
- [key: string]: any;
}
const config: Config = {
ALGOLIA_APPLICATION_ID: CONFIG.ALGOLIA_APPLICATION_ID,
ALGOLIA_INDEX_PREFIX: CONFIG.ALGOLIA_INDEX_PREFIX,
ALGOLIA_READ_ONLY_API_KEY: CONFIG.ALGOLIA_READ_ONLY_API_KEY,
+ AUTH0_AUDIENCE: CONFIG.AUTH0_AUDIENCE,
+ AUTH0_CLIENT_ID: CONFIG.AUTH0_CLIENT_ID,
+ AUTH0_DOMAIN: CONFIG.AUTH0_DOMAIN,
+ AUTH0_REDIRECT_URI: CONFIG.AUTH0_REDIRECT_URI,
// When GA sunsets Universal Analytics wit GA4 in July 2023, this prop can be removed
GOOGLE_ANALYTICS_ID:
process.env.NODE_ENV === "production" ? "UA-116318550-1" : "UA-116318550-2",
diff --git a/app/pages/Auth/Auth.module.scss b/app/pages/Auth/Auth.module.scss
new file mode 100644
index 000000000..e20b8c042
--- /dev/null
+++ b/app/pages/Auth/Auth.module.scss
@@ -0,0 +1,28 @@
+@import "~styles/utils/_helpers.scss";
+
+.authPage {
+ display: grid;
+ grid-column: 2 / span 10;
+ gap: 10px;
+ margin: 20px;
+}
+
+.verificationDigits {
+ display: flex;
+ width: 400px;
+}
+
+.title {
+ font-size: 24px;
+}
+
+.authForm {
+ width: 66%;
+ display: grid;
+ gap: 8px;
+}
+
+.authFormButton {
+ max-width: 300px;
+ margin-top: 8px;
+}
diff --git a/app/pages/Auth/LoginPage.tsx b/app/pages/Auth/LoginPage.tsx
new file mode 100644
index 000000000..40aad0526
--- /dev/null
+++ b/app/pages/Auth/LoginPage.tsx
@@ -0,0 +1,57 @@
+import React, { useState } from "react";
+import { Link } from "react-router-dom";
+import { Button } from "components/ui/inline/Button/Button";
+import { useAppContext, passwordlessLogin, passwordlessStart } from "utils";
+
+import { VerificationModal } from "./VerificationModal";
+
+import styles from "./Auth.module.scss";
+
+export const LoginPage = () => {
+ const [modalIsOpen, setModalIsOpen] = useState(false);
+ const [email, setEmail] = useState("");
+ const { authClient } = useAppContext();
+
+ const logIn = (evt: React.SyntheticEvent) => {
+ evt.preventDefault();
+ passwordlessStart(authClient, email).then(() => {
+ setModalIsOpen(true);
+ });
+ };
+
+ return (
+
+
For Case Managers
+
New here? Sign up!
+
+ We want to make sure that your account information is safe, so you will
+ be sent a verification code to your email each time you log in. Please
+ enter in your email address and then check your email to find a 6 digit
+ verification code.
+
+
+
+
passwordlessLogin(authClient, email, code)}
+ resendCode={() => passwordlessStart(authClient, email)}
+ buttonText="Log in"
+ />
+
+ );
+};
diff --git a/app/pages/Auth/LogoutPage.tsx b/app/pages/Auth/LogoutPage.tsx
new file mode 100644
index 000000000..a006582eb
--- /dev/null
+++ b/app/pages/Auth/LogoutPage.tsx
@@ -0,0 +1,18 @@
+import React, { useEffect } from "react";
+import { Redirect } from "react-router-dom";
+import * as AuthService from "utils/AuthService";
+import { useAppContext } from "../../utils";
+
+import Config from "../../config";
+
+export const LogoutPage = () => {
+ const context = useAppContext();
+ const { setAuthState } = context;
+ const { authClient } = context;
+
+ useEffect(() => {
+ AuthService.logout(authClient, Config.AUTH0_CLIENT_ID, setAuthState);
+ });
+
+ return ;
+};
diff --git a/app/pages/Auth/SignUpPage.tsx b/app/pages/Auth/SignUpPage.tsx
new file mode 100644
index 000000000..2c93e1f05
--- /dev/null
+++ b/app/pages/Auth/SignUpPage.tsx
@@ -0,0 +1,84 @@
+import React, { useState } from "react";
+import * as Sentry from "@sentry/browser";
+import { Link } from "react-router-dom";
+import { Button } from "components/ui/inline/Button/Button";
+import * as AuthService from "utils/AuthService";
+import { useAppContext } from "utils";
+
+import { VerificationModal } from "./VerificationModal";
+
+import styles from "./Auth.module.scss";
+
+export const SignUpPage = () => {
+ const [modalIsOpen, setModalIsOpen] = useState(false);
+ const [email, setEmail] = useState("");
+ const [name, setName] = useState("");
+ const [organization, setOrganization] = useState("");
+ const { authClient } = useAppContext();
+ const { passwordlessStart, completeUserSignup } = AuthService;
+
+ const signUp = (evt: React.SyntheticEvent) => {
+ evt.preventDefault();
+ passwordlessStart(authClient, email).then(
+ () => {
+ setModalIsOpen(true);
+ },
+ (error) => {
+ if (error) {
+ // TODO: Inform user of the error?
+ Sentry.captureException(error);
+ }
+ }
+ );
+ };
+
+ return (
+
+
For Case Managers
+ Already have an account? Log in!
+
+
+
+ completeUserSignup(authClient, code, email, name, organization)
+ }
+ resendCode={() => passwordlessStart(authClient, email)}
+ buttonText="Sign up"
+ />
+
+ );
+};
diff --git a/app/pages/Auth/VerificationModal.tsx b/app/pages/Auth/VerificationModal.tsx
new file mode 100644
index 000000000..809308119
--- /dev/null
+++ b/app/pages/Auth/VerificationModal.tsx
@@ -0,0 +1,116 @@
+import React, { useState, createRef } from "react";
+import { Modal } from "components/ui/Modal/Modal";
+import { Button } from "components/ui/inline/Button/Button";
+
+import styles from "./Auth.module.scss";
+
+export const VerificationModal = ({
+ email,
+ verifyCode,
+ modalIsOpen,
+ setModalIsOpen,
+ resendCode,
+ buttonText,
+}: {
+ email: string;
+ verifyCode: (code: string) => void;
+ modalIsOpen: boolean;
+ setModalIsOpen: (isOpen: boolean) => void;
+ resendCode: () => Promise;
+ buttonText: string;
+}) => {
+ const initialVerificationCode = ["", "", "", "", "", ""];
+ const [verificationCode, setVerificationCode] = useState(
+ initialVerificationCode
+ );
+ const inputRefs: React.RefObject[] =
+ initialVerificationCode.map(() => createRef());
+
+ const handleVerificationCodeChange = (index: number, value: string) => {
+ const updatedVerificationCode = [...verificationCode];
+ updatedVerificationCode[index] = value;
+ setVerificationCode(updatedVerificationCode);
+
+ const nextRef = inputRefs[index + 1];
+ if (value && nextRef && nextRef.current) {
+ nextRef.current.focus();
+ }
+ };
+
+ const handlePaste = (event: React.ClipboardEvent) => {
+ event.preventDefault();
+ const clipboardData = event.clipboardData || window.Clipboard;
+ const pastedData = clipboardData.getData("text");
+
+ if (pastedData.length === 6) {
+ const pastedCodeArray = pastedData.split("");
+ const updatedVerificationCode = [...verificationCode];
+
+ pastedCodeArray.forEach((digit, index) => {
+ const inputRef = inputRefs[index];
+ if (inputRef.current) {
+ updatedVerificationCode[index] = digit;
+ inputRef.current.value = digit;
+ }
+ });
+
+ setVerificationCode(updatedVerificationCode);
+ inputRefs?.[0]?.current?.focus();
+ }
+ };
+
+ const onSubmitCode = () => {
+ const codeString = verificationCode.join("");
+ verifyCode(codeString);
+ };
+
+ return (
+ setModalIsOpen(false)}
+ >
+
+
Please check your email
+
We've sent a code to {email}
+
+ {verificationCode.map((digit, index) => (
+
+ handleVerificationCodeChange(index, evt.target.value ?? "")
+ }
+ onPaste={handlePaste}
+ ref={inputRefs[index]}
+ maxLength={1}
+ />
+ ))}
+
+
+
{buttonText}
+
+
+ );
+};
+
+const ResendCode = ({ resendCode }: { resendCode: () => Promise }) => {
+ return (
+
+
+ Didn't receive a code? Please check your spam folder.
+ {
+ resendCode();
+ }}
+ >
+ Send another code.
+
+
+
+ );
+};
diff --git a/app/pages/HomePage/HomePage.tsx b/app/pages/HomePage/HomePage.tsx
index 45a889eb7..b82ab8976 100644
--- a/app/pages/HomePage/HomePage.tsx
+++ b/app/pages/HomePage/HomePage.tsx
@@ -4,11 +4,12 @@ import qs from "qs";
import { getResourceCount } from "utils/DataService";
import { Footer, NewsArticles } from "components/ui";
+import * as AuthService from "utils/AuthService";
import { Partners } from "./components/Partners/Partners";
import { SearchBar } from "./components/SearchBar/SearchBar";
import { HomePageSection } from "./components/Section/Section";
import ResourceList from "./components/ResourceList/ResourceList";
-import { whiteLabel } from "../../utils";
+import { whiteLabel, useAppContext } from "../../utils";
const { showBreakingNews } = whiteLabel;
@@ -68,6 +69,7 @@ const covidResources = [
];
export const HomePage = () => {
+ const { authClient, setAuthState } = useAppContext();
const [resourceCount, setResourceCount] = useState();
const [searchValue, setSearchValue] = useState("");
const history = useHistory();
@@ -83,6 +85,21 @@ export const HomePage = () => {
getResourceCount().then((count: number) => setResourceCount(count));
}, []);
+ useEffect(() => {
+ // TODO: This effect should be moved to the case worker UI homepage when that page is created
+ const { hash } = window.location;
+ if (!hash || !hash.includes("access_token")) return;
+
+ AuthService.initializeUserSession(
+ window.location.hash,
+ authClient,
+ setAuthState
+ );
+
+ // Remove the url query params set by Auth0
+ history.replace(window.location.pathname + window.location.search);
+ }, [history, setAuthState, authClient]);
+
return (
<>
{showBreakingNews && }
diff --git a/app/utils/AuthService.ts b/app/utils/AuthService.ts
new file mode 100644
index 000000000..93ca00c34
--- /dev/null
+++ b/app/utils/AuthService.ts
@@ -0,0 +1,155 @@
+import type { WebAuth, Auth0Result } from "auth0-js";
+import * as Sentry from "@sentry/browser";
+import { post } from "utils/DataService";
+import type { AuthState, UserSignUpData } from "utils";
+import { setUserSignUpData } from "utils";
+import config from "../config";
+
+/**
+ This file provides a set of methods that serve as an interface between our application
+ and the Auth0 servers where the user's auth state and data is stored.
+*/
+
+export const calculateSessionExpiration = (secondsUntilExpiration: number) => {
+ const currentTime = new Date();
+ const expirationTime = new Date(
+ currentTime.getTime() + secondsUntilExpiration * 1000
+ );
+
+ return expirationTime;
+};
+
+export const initializeUserSession = (
+ hash: string,
+ authClient: WebAuth,
+ setAuthState: (a: AuthState) => void
+) => {
+ authClient.parseHash({ hash }, (err, authResult) => {
+ if (err) {
+ // TODO: Inform user of the error?
+ Sentry.captureException(err);
+ }
+
+ if (authResult?.accessToken) {
+ const { accessToken, expiresIn, idTokenPayload } = authResult;
+ const authObject = {
+ user: {
+ email: idTokenPayload.email,
+ id: idTokenPayload.sub,
+ },
+ accessTokenObject: {
+ token: accessToken,
+ expiresAt: expiresIn
+ ? calculateSessionExpiration(expiresIn)
+ : new Date(1970, 1, 1),
+ },
+ };
+
+ setAuthState(authObject);
+ }
+ });
+};
+
+// Invokes the passwordlessLogin method and following that stores the new user data in sessionStorage
+export const completeUserSignup = (
+ authClient: WebAuth,
+ verificationCode: string,
+ email: string,
+ name: string,
+ organization: string | null = null
+) => {
+ passwordlessLogin(authClient, email, verificationCode);
+ // Store user sign up data, which will be saved to our backend after Auth0 success redirect
+ setUserSignUpData({ email, name, organization });
+};
+
+/** This method initiates the log-in/sign-up process by sending a code
+ to the user's inbox.
+*/
+export const passwordlessStart = (authClient: WebAuth, email: string) => {
+ return new Promise((resolve, reject) => {
+ authClient.passwordlessStart(
+ {
+ connection: "email",
+ send: "code",
+ email,
+ },
+ (err) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ resolve(true);
+ }
+ );
+ });
+};
+
+/** This method passes the user's verification code to Auth0's server, which
+ completes their sign-up/log-in action. Upon success, the Auth0 library
+ will initiate a redirect, which is why this function doesn't have a
+ meaningful return value.
+*/
+export const passwordlessLogin = (
+ authClient: WebAuth,
+ email: string,
+ verificationCode: string
+) => {
+ authClient.passwordlessLogin(
+ {
+ connection: "email",
+ email,
+ verificationCode,
+ },
+ (err) => {
+ if (err) {
+ // TODO: Inform user of the error?
+ Sentry.captureException(err);
+ }
+ }
+ );
+};
+
+export const logout = (
+ authClient: WebAuth,
+ clientId: string,
+ setAuthState: (state: AuthState) => void
+) => {
+ setAuthState(null);
+
+ authClient.logout({
+ returnTo: config.AUTH0_REDIRECT_URI,
+ clientID: clientId,
+ });
+};
+
+export const hasAccessTokenExpired = (tokenExpiration: Date) => {
+ return !tokenExpiration || tokenExpiration < new Date();
+};
+
+export const refreshAccessToken = (
+ authClient: WebAuth
+): Promise => {
+ return new Promise((resolve, reject) => {
+ authClient.checkSession({}, (err, authResult: Auth0Result) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(authResult);
+ }
+ });
+ });
+};
+
+export const saveUser = (
+ userSignUpData: UserSignUpData,
+ userExternalId: string,
+ authToken: string
+) => {
+ return post(
+ "/api/users",
+ { ...userSignUpData, user_external_id: userExternalId },
+ { Authorization: `Bearer ${authToken}` }
+ );
+};
diff --git a/app/utils/SessionCacher.ts b/app/utils/SessionCacher.ts
new file mode 100644
index 000000000..859e831f0
--- /dev/null
+++ b/app/utils/SessionCacher.ts
@@ -0,0 +1,38 @@
+import type { AuthState, UserSignUpData } from "utils";
+
+/**
+ This file provides methods to sync a user's auth state, which is managed by the AppProvider
+ component, with the browser's sessionStorage; this enables the app to get the user's auth data
+ upon refreshing the page, etc., and to then reset the auth state.
+*/
+
+export const getAuthObject = (): AuthState | null => {
+ const object = sessionStorage.getItem("authObject");
+ return object ? JSON.parse(object) : null;
+};
+
+export const setAuthObject = (authObject: AuthState) => {
+ sessionStorage.setItem("authObject", JSON.stringify(authObject));
+};
+
+export const clearSession = () => {
+ sessionStorage.removeItem("authObject");
+};
+
+export const setUserSignUpData = (userData: UserSignUpData) => {
+ sessionStorage.setItem("userSignUpData", JSON.stringify(userData));
+};
+
+export const hasUserSignupData = (): boolean => {
+ const object = sessionStorage.getItem("userSignUpData");
+ return !!object;
+};
+
+export const getUserSignUpData = (): UserSignUpData | null => {
+ const object = sessionStorage.getItem("userSignUpData");
+ return object ? JSON.parse(object) : null;
+};
+
+export const clearUserSignUpData = () => {
+ sessionStorage.removeItem("userSignUpData");
+};
diff --git a/app/utils/index.ts b/app/utils/index.ts
index d70b57e5c..650951a31 100644
--- a/app/utils/index.ts
+++ b/app/utils/index.ts
@@ -2,4 +2,6 @@ export * from "./location";
export * from "./numbers";
export * from "./time";
export * from "./useAppContext";
+export * from "./SessionCacher";
+export * from "./AuthService";
export { default as whiteLabel } from "./whitelabel";
diff --git a/app/utils/useAppContext.ts b/app/utils/useAppContext.ts
deleted file mode 100644
index 6667358b2..000000000
--- a/app/utils/useAppContext.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createContext, useContext } from "react";
-import { GeoCoordinates } from "./location";
-
-export const AppContext = createContext({
- userLocation: null,
-});
-
-export const useAppContext = () => useContext(AppContext);
diff --git a/app/utils/useAppContext.tsx b/app/utils/useAppContext.tsx
new file mode 100644
index 000000000..35418a028
--- /dev/null
+++ b/app/utils/useAppContext.tsx
@@ -0,0 +1,132 @@
+import React, {
+ useState,
+ useMemo,
+ useEffect,
+ createContext,
+ useContext,
+} from "react";
+import auth0, { Auth0Result, WebAuth } from "auth0-js";
+import * as Sentry from "@sentry/browser";
+import * as SessionCacher from "utils/SessionCacher";
+import * as AuthService from "utils/AuthService";
+import { GeoCoordinates } from "utils";
+import config from "../config";
+
+export interface UserSignUpData {
+ email: string;
+ name: string;
+ organization: string | null;
+}
+
+export type AuthState = {
+ user: {
+ id: string;
+ email: string;
+ };
+ accessTokenObject: {
+ expiresAt: Date;
+ token: string;
+ };
+} | null;
+
+interface Context {
+ userLocation: GeoCoordinates | null;
+ authState: AuthState;
+ setAuthState: (state: AuthState) => void;
+ authClient: WebAuth;
+}
+
+const authClient = new auth0.WebAuth({
+ audience: config.AUTH0_AUDIENCE,
+ clientID: config.AUTH0_CLIENT_ID,
+ domain: config.AUTH0_DOMAIN,
+ redirectUri: config.AUTH0_REDIRECT_URI,
+ responseType: "token id_token",
+});
+
+export const AppContext = createContext({
+ userLocation: null,
+ authState: null,
+ setAuthState: () => {},
+ authClient,
+});
+
+export const useAppContext = () => useContext(AppContext);
+
+export const AppProvider = ({
+ children,
+ userLocation,
+}: {
+ children: React.ReactNode;
+ userLocation: GeoCoordinates | null;
+}) => {
+ const authObject = SessionCacher.getAuthObject();
+ const [authState, setAuthState] = useState(authObject);
+
+ // We have to use useMemo here to manage the contextValue to ensure that the user's authState
+ // propagates downward after authentication. I couldn't find a way to get this to work with
+ // useState. Moreover, we can't use a simple object to define contextValue, as the object would
+ // be recreated at each render and thus force all of its child components to re-render as well.
+ const contextValue = useMemo(() => {
+ return {
+ userLocation,
+ authState,
+ setAuthState,
+ authClient,
+ };
+ }, [authState, userLocation]);
+
+ useEffect(() => {
+ // This effect runs after any changes to the AppContext's authState and syncs the changes
+ // to the authObject in sessionStorage.
+ SessionCacher.setAuthObject(authState);
+
+ // If the SessionCacher has userSignUpData object, that means a user has just been created
+ // in Auth0 and a redirect has occurred. Therefore, we save the new user data to our database.
+ // The authState must also have propagated or else we won't have the token yet.
+ const newUserData = SessionCacher.getUserSignUpData();
+ if (newUserData && authState) {
+ SessionCacher.clearUserSignUpData();
+ AuthService.saveUser(
+ newUserData,
+ authState.user.id,
+ authState.accessTokenObject.token
+ );
+ }
+ }, [authState]);
+
+ if (
+ authObject &&
+ AuthService.hasAccessTokenExpired(
+ new Date(authObject.accessTokenObject.expiresAt)
+ )
+ ) {
+ AuthService.refreshAccessToken(contextValue.authClient)
+ .then((authResult: Auth0Result) => {
+ if (
+ authState &&
+ authResult.accessToken &&
+ authResult.expiresIn !== undefined
+ ) {
+ setAuthState({
+ ...authState,
+ accessTokenObject: {
+ token: authResult.accessToken,
+ expiresAt: AuthService.calculateSessionExpiration(
+ authResult.expiresIn
+ ),
+ },
+ });
+ } else {
+ throw new Error("Token does not exist or is unexpected token");
+ }
+ })
+ .catch((err) => {
+ Sentry.captureException(err);
+ });
+ }
+
+ return (
+ {children}
+ );
+};
diff --git a/config.example.yml b/config.example.yml
index 0c170f951..ee577734e 100644
--- a/config.example.yml
+++ b/config.example.yml
@@ -19,3 +19,9 @@ MOHCD_SUBDOMAIN: "testing"
# uncomment this to simulate sfserviceguide whitelabel UI locally, assuming that
# you're pointing to localhost:8080
# MOHCD_DOMAIN: 'localhost:8080'
+
+# Auth0 config values
+AUTH0_AUDIENCE: "http://localhost:8080/api"
+AUTH0_CLIENT_ID: "UcnuRrX6S0SeDEhW9PRe01wEhcvIRuwc"
+AUTH0_DOMAIN: "dev-nykixf8szsm220fi.us.auth0.com"
+AUTH0_REDIRECT_URI: "http://localhost:8080"
diff --git a/package-lock.json b/package-lock.json
index 08e52db0d..29b284678 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@babel/polyfill": "^7.4.4",
"@sentry/browser": "^4.0.6",
"algoliasearch": "^4.10.5",
+ "auth0-js": "^9.22.1",
"classnames": "^2.2.6",
"copy-webpack-plugin": "^11.0.0",
"google-map-react": "^1.1.4",
@@ -50,6 +51,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.13.0",
"@babel/register": "^7.4.4",
+ "@types/auth0-js": "^9.21.0",
"@types/chai": "^4.2.22",
"@types/enzyme": "^3.10.9",
"@types/google-map-react": "^2.1.3",
@@ -4638,6 +4640,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/auth0-js": {
+ "version": "9.21.0",
+ "resolved": "https://registry.npmjs.org/@types/auth0-js/-/auth0-js-9.21.0.tgz",
+ "integrity": "sha512-tnF0BKFwI+Vzqwb9p7KgpaKStg/WHqbiGWz5GPpn+ZeBvJ1iY7NkmeNJUsHIN/4c7CF2zr8FT5JRhs3F5aAPNw==",
+ "dev": true
+ },
"node_modules/@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -6595,7 +6603,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -6620,6 +6627,21 @@
"node": ">= 4.5.0"
}
},
+ "node_modules/auth0-js": {
+ "version": "9.22.1",
+ "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.22.1.tgz",
+ "integrity": "sha512-AcyJiWhsyG5zdx40O9i/okpLLEvB23/6CivWynmGtP43s2C4GSq3E+XdCRw64ifmZ7t6ZK4Yzfpiqy5KVXEtJg==",
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "idtoken-verifier": "^2.2.2",
+ "js-cookie": "^2.2.0",
+ "minimist": "^1.2.5",
+ "qs": "^6.10.1",
+ "superagent": "^7.1.5",
+ "url-join": "^4.0.1",
+ "winchan": "^0.2.2"
+ }
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@@ -8984,7 +9006,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -9271,6 +9292,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
+ },
"node_modules/copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -9609,6 +9635,11 @@
"node": "*"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
+ },
"node_modules/crypto-md5": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-md5/-/crypto-md5-1.0.0.tgz",
@@ -10634,7 +10665,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -10699,6 +10729,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -13060,6 +13099,11 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
+ },
"node_modules/fast-xml-parser": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz",
@@ -13636,6 +13680,20 @@
"node": ">= 0.12"
}
},
+ "node_modules/formidable": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
+ "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
+ "dependencies": {
+ "dezalgo": "^1.0.4",
+ "hexoid": "^1.0.0",
+ "once": "^1.4.0",
+ "qs": "^6.11.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -14570,6 +14628,14 @@
"node": ">= 8"
}
},
+ "node_modules/hexoid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
+ "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/highlight-es": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.3.tgz",
@@ -15076,6 +15142,24 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/idtoken-verifier": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.2.3.tgz",
+ "integrity": "sha512-hhpzB+MRgEvbwqzRLFdVbG55lKdXQVfeYEjAA2qu0UC72MSLeR0nX7P7rY5Dycz1aISHPOwq80hIPFoJ/+SItA==",
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.1.1",
+ "es6-promise": "^4.2.8",
+ "jsbn": "^1.1.0",
+ "unfetch": "^4.2.0",
+ "url-join": "^4.0.1"
+ }
+ },
+ "node_modules/idtoken-verifier/node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -17108,6 +17192,11 @@
"@sideway/pinpoint": "^2.0.0"
}
},
+ "node_modules/js-cookie": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
+ "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -18463,7 +18552,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -18636,7 +18724,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -25755,6 +25842,99 @@
"integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==",
"peer": true
},
+ "node_modules/superagent": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.5.tgz",
+ "integrity": "sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==",
+ "dependencies": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.3",
+ "debug": "^4.3.4",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.0",
+ "formidable": "^2.0.1",
+ "methods": "^1.1.2",
+ "mime": "^2.5.0",
+ "qs": "^6.10.3",
+ "readable-stream": "^3.6.0",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": ">=6.4.0 <13 || >=14"
+ }
+ },
+ "node_modules/superagent/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/superagent/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/superagent/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/superagent/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/superagent/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/superagent/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -27716,6 +27896,11 @@
"through": "^2.3.8"
}
},
+ "node_modules/unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+ },
"node_modules/unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
@@ -28004,6 +28189,11 @@
"querystring": "0.2.0"
}
},
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+ },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
@@ -29493,6 +29683,11 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true
},
+ "node_modules/winchan": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz",
+ "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ=="
+ },
"node_modules/windows-release": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.0.1.tgz",
@@ -29708,8 +29903,7 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yaml": {
"version": "1.10.2",
@@ -33070,6 +33264,12 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
+ "@types/auth0-js": {
+ "version": "9.21.0",
+ "resolved": "https://registry.npmjs.org/@types/auth0-js/-/auth0-js-9.21.0.tgz",
+ "integrity": "sha512-tnF0BKFwI+Vzqwb9p7KgpaKStg/WHqbiGWz5GPpn+ZeBvJ1iY7NkmeNJUsHIN/4c7CF2zr8FT5JRhs3F5aAPNw==",
+ "dev": true
+ },
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -34561,8 +34761,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"at-least-node": {
"version": "1.0.0",
@@ -34575,6 +34774,21 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
+ "auth0-js": {
+ "version": "9.22.1",
+ "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.22.1.tgz",
+ "integrity": "sha512-AcyJiWhsyG5zdx40O9i/okpLLEvB23/6CivWynmGtP43s2C4GSq3E+XdCRw64ifmZ7t6ZK4Yzfpiqy5KVXEtJg==",
+ "requires": {
+ "base64-js": "^1.5.1",
+ "idtoken-verifier": "^2.2.2",
+ "js-cookie": "^2.2.0",
+ "minimist": "^1.2.5",
+ "qs": "^6.10.1",
+ "superagent": "^7.1.5",
+ "url-join": "^4.0.1",
+ "winchan": "^0.2.2"
+ }
+ },
"available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@@ -36357,7 +36571,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -36575,6 +36788,11 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"dev": true
},
+ "cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
+ },
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -36822,6 +37040,11 @@
"randomfill": "^1.0.3"
}
},
+ "crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
+ },
"crypto-md5": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-md5/-/crypto-md5-1.0.0.tgz",
@@ -37583,8 +37806,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"denodeify": {
"version": "1.2.1",
@@ -37632,6 +37854,15 @@
"integrity": "sha512-fYXbFSeilT7bnKWFi4OERSPHdtaEoDGn4aUhV5Nly6/I+Tp6JZ/6Icmd7LVIF5euyodGpxz2e/bfUmDnIdSIDw==",
"dev": true
},
+ "dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "requires": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -39384,6 +39615,11 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
+ "fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
+ },
"fast-xml-parser": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz",
@@ -39773,6 +40009,17 @@
"mime-types": "^2.1.12"
}
},
+ "formidable": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
+ "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
+ "requires": {
+ "dezalgo": "^1.0.4",
+ "hexoid": "^1.0.0",
+ "once": "^1.4.0",
+ "qs": "^6.11.0"
+ }
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -40419,6 +40666,11 @@
}
}
},
+ "hexoid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
+ "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g=="
+ },
"highlight-es": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.3.tgz",
@@ -40815,6 +41067,26 @@
"dev": true,
"requires": {}
},
+ "idtoken-verifier": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.2.3.tgz",
+ "integrity": "sha512-hhpzB+MRgEvbwqzRLFdVbG55lKdXQVfeYEjAA2qu0UC72MSLeR0nX7P7rY5Dycz1aISHPOwq80hIPFoJ/+SItA==",
+ "requires": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.1.1",
+ "es6-promise": "^4.2.8",
+ "jsbn": "^1.1.0",
+ "unfetch": "^4.2.0",
+ "url-join": "^4.0.1"
+ },
+ "dependencies": {
+ "jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
+ }
+ }
+ },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -42225,6 +42497,11 @@
"@sideway/pinpoint": "^2.0.0"
}
},
+ "js-cookie": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
+ "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -43261,7 +43538,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"requires": {
"yallist": "^4.0.0"
}
@@ -43388,8 +43664,7 @@
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "dev": true
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"metro": {
"version": "0.70.3",
@@ -48661,6 +48936,72 @@
"integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==",
"peer": true
},
+ "superagent": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.5.tgz",
+ "integrity": "sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==",
+ "requires": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.3",
+ "debug": "^4.3.4",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.0",
+ "formidable": "^2.0.1",
+ "methods": "^1.1.2",
+ "mime": "^2.5.0",
+ "qs": "^6.10.3",
+ "readable-stream": "^3.6.0",
+ "semver": "^7.3.7"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -50093,6 +50434,11 @@
"through": "^2.3.8"
}
},
+ "unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+ },
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
@@ -50303,6 +50649,11 @@
}
}
},
+ "url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+ },
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
@@ -51396,6 +51747,11 @@
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true
},
+ "winchan": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz",
+ "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ=="
+ },
"windows-release": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.0.1.tgz",
@@ -51536,8 +51892,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yaml": {
"version": "1.10.2",
diff --git a/package.json b/package.json
index 86044618b..e60ec37a6 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"@babel/polyfill": "^7.4.4",
"@sentry/browser": "^4.0.6",
"algoliasearch": "^4.10.5",
+ "auth0-js": "^9.22.1",
"classnames": "^2.2.6",
"copy-webpack-plugin": "^11.0.0",
"google-map-react": "^1.1.4",
@@ -49,6 +50,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.13.0",
"@babel/register": "^7.4.4",
+ "@types/auth0-js": "^9.21.0",
"@types/chai": "^4.2.22",
"@types/enzyme": "^3.10.9",
"@types/google-map-react": "^2.1.3",