Skip to content

Commit

Permalink
Merge branch '689-authentication' into qa-one
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Schroer committed Feb 23, 2024
2 parents 34f7e82 + 9d7f87c commit 6d646ee
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 113 deletions.
18 changes: 17 additions & 1 deletion app/components/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export interface AuthState {
};
}

export interface UserSignUpData {
email: string;
name: string;
organization: string | null;
}

export const defaultAuthObject: AuthState = {
isAuthenticated: false,
user: {
Expand Down Expand Up @@ -51,6 +57,16 @@ export const AppProvider = ({
// 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; thus the
// `authState.isAuthenticated` check
const newUserData = SessionCacher.getUserSignUpData();
if (newUserData && authState.isAuthenticated) {
SessionCacher.clearUserSignUpData();
AuthService.saveUser(newUserData, authState.user.id, authState.accessTokenObject.token);
}
}, [authState]);
console.log("config: ");
console.log(config);
Expand Down Expand Up @@ -96,7 +112,7 @@ export const AppProvider = ({
},
});
} else {
throw new Error("Token does not exist or is in unexpected token");
throw new Error("Token does not exist or is unexpected token");
}
})
.catch((err) => {
Expand Down
6 changes: 1 addition & 5 deletions app/pages/Auth/LogoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ export const LogoutPage = () => {
const authClient = context.authClient as WebAuth;

useEffect(() => {
AuthService.logout(
authClient,
Config.AUTH0_CLIENT_ID,
setAuthState
);
AuthService.logout(authClient, Config.AUTH0_CLIENT_ID, setAuthState);
});

return <Redirect to="/" />;
Expand Down
23 changes: 13 additions & 10 deletions app/pages/Auth/SignUpPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ export const SignUpPage = () => {
const [name, setName] = useState("");
const [organization, setOrganization] = useState("");
const authClient = useAppContext().authClient as WebAuth;
const { passwordlessStart, completeUserSignup, initializeUserSignUp } =
const { passwordlessStart, completeUserSignup } =
AuthService;

const signUp = (evt: React.SyntheticEvent) => {
evt.preventDefault();
initializeUserSignUp(authClient, email).then(() => {
setModalIsOpen(true);
}, (error) => {
if (error.message === 'userExists') {
// eslint-disable-next-line no-alert
// Todo: Handle this case with a proper error message
alert('Oops, there is already a user with that email in our system. Please try logging in instead.');
passwordlessStart(authClient, email).then(
() => {
setModalIsOpen(true);
},
(error) => {
if (error) {
// TODO: Handle errors
}
}
})
);
};

return (
Expand Down Expand Up @@ -71,7 +72,9 @@ export const SignUpPage = () => {
email={email}
modalIsOpen={modalIsOpen}
setModalIsOpen={setModalIsOpen}
verifyCode={(code) => completeUserSignup(authClient, code, email, name, organization)}
verifyCode={(code) =>
completeUserSignup(authClient, code, email, name, organization)
}
resendCode={() => passwordlessStart(authClient, email)}
buttonText="Sign up"
/>
Expand Down
44 changes: 12 additions & 32 deletions app/pages/Auth/VerificationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, createRef, useEffect } from "react";
import React, { useState, createRef } from "react";
import { Modal } from "components/ui/Modal/Modal";
import { Button } from "components/ui/inline/Button/Button";

Expand Down Expand Up @@ -98,39 +98,19 @@ export const VerificationModal = ({
};

const ResendCode = ({ resendCode }: { resendCode: () => Promise<unknown> }) => {
const [timeLeft, setTimeLeft] = useState(60);
const [codeResent, setCodeResent] = useState(false);

useEffect(() => {
const interval = setInterval(() => {
if (timeLeft === 0) {
clearInterval(interval);
resendCode();
setCodeResent(true);
return;
}

setTimeLeft(timeLeft - 1);
}, 1000);

return () => clearInterval(interval);
}, [resendCode, setTimeLeft, timeLeft]);

return (
<div>
{codeResent ? (
<p>
Didn&apos;t receive a code?
<button type="button" onClick={() => {resendCode()}}>
Resend
</button>
</p>
) : (
<>
<strong>Resend code in:</strong>
{timeLeft}
</>
)}
<p>
Didn&apos;t receive a code? Please check your spam folder.
<button
type="button"
onClick={() => {
resendCode();
}}
>
Send another code.
</button>
</p>
</div>
);
};
4 changes: 2 additions & 2 deletions app/pages/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ export const HomePage = () => {
});

useEffect(() => {
// Todo: This effect should be moved to the case worker UI homepage when that page is created
// 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.persistUser(
AuthService.initializeUserSession(
window.location.hash,
authClient as WebAuth,
setAuthState
Expand Down
95 changes: 34 additions & 61 deletions app/utils/AuthService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { WebAuth, Auth0Result } from "auth0-js";
import { defaultAuthObject } from "components/AppProvider";
import { post } from "utils/DataService";
import type { AuthState } from "components/AppProvider";
import { SessionCacher } from "utils";
import type { AuthState, UserSignUpData } from "components/AppProvider";

/*
This class provides a set of methods that serve as an interface between our application
and the Auth0 servers where the user's state and data is stored.
and the Auth0 servers where the user's auth state and data is stored.
*/

export default class AuthService {
Expand All @@ -18,7 +19,11 @@ export default class AuthService {
return expirationTime;
}

static persistUser(hash: string, authClient: WebAuth, setAuthState: any) {
static initializeUserSession(
hash: string,
authClient: WebAuth,
setAuthState: any
) {
authClient.parseHash({ hash }, (err, authResult) => {
if (err) {
// TODO: Handle errors
Expand All @@ -45,43 +50,20 @@ export default class AuthService {
});
}

static initializeUserSignUp = (authClient: WebAuth, email: string) => {
return new Promise((resolve, reject) => {
this.userExists(email).then((exists) => {
if (exists) {
reject(new Error('userExists'));
} else {
this.passwordlessStart(authClient, email).then((result) => {
resolve(result);
})
}
});
});
};

// Invokes the passwordlessLogin method and following that saves the user to our database
static completeUserSignup = (authClient: WebAuth,
// Invokes the passwordlessLogin method and following that stores the new user data in sessionStorage
static completeUserSignup = (
authClient: WebAuth,
verificationCode: string,
email: string,
name: string,
organization: (string | null) = null) => {
this.passwordlessLogin(authClient, email, verificationCode);
// We need to optimistically save the user to our database here. The user is saved to the _Auth0_
// database after the passwordlessLogin method succeeds. We also want to save user data in our
// backend. This should be done after a success callback after passwordlessLogin succceds; however,
// the passwordlessLogin success callback does not fire within our app, because, upon success, Auth0
// triggers a redirect to our home page. At that point, we do not have the user's name or organization,
// which we need to save in our database. Thus, we save the user here.
//
// If for some reason, the passwordlessLogin method errors, this code still save the user in our DB.
// At that point, the worst case scenario is that the user will be informed that they have already
// signed up if they try to sign up again and to log in instead. Since the Auth0 passwordless flow
// does not have a sign-up process separate from its log-in process, and thus the user will still
// be created within Auth0 upon going through our site's log-in flow.
this.saveUser(email, name, organization);
organization: string | null = null
) => {
this.passwordlessLogin(authClient, email, verificationCode);
// Store user sign up data, which will be saved to our backend after Auth0 success redirect
SessionCacher.setUserSignUpData({email, name, organization});
};

// This method initiates the sign-in/sign-up process by sending a code
// This method initiates the log-in/sign-up process by sending a code
// to the user's inbox.
static passwordlessStart = (authClient: WebAuth, email: string) => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -109,7 +91,7 @@ export default class AuthService {
authClient: WebAuth,
email: string,
verificationCode: string
) => {
) => {
authClient.passwordlessLogin(
{
connection: "email",
Expand Down Expand Up @@ -138,7 +120,7 @@ export default class AuthService {
};

static hasAccessTokenExpired = (tokenExpiration: Date) => {
return !tokenExpiration || (new Date(tokenExpiration) < new Date());
return !tokenExpiration || new Date(tokenExpiration) < new Date();
};

static refreshAccessToken = (authClient: WebAuth) => {
Expand All @@ -153,30 +135,21 @@ export default class AuthService {
});
};

static userExists = (email: string) => {
return new Promise((resolve, reject) => {
post('/api/users/user_exists', {
email
}).then((resp) => {
resp.json().then(result => resolve(result.user_exists));
}, (error) => {
reject(error);
})
});
}

static saveUser = (email: string, name: string, organization: (string | null) = null) => {
static saveUser = (
userSignUpData: UserSignUpData,
userExternalId: string,
authToken: string
) => {
return new Promise((resolve, reject) => {
const response = post('/api/users', {
email,
name,
organization,
});
response.then((result) => {
resolve(result);
}, (error) => {
reject(error);
})
const response = post("/api/users", {...userSignUpData, user_external_id: userExternalId}, {"Authorization": `Bearer ${authToken}`});
response.then(
(result) => {
resolve(result);
},
(error) => {
reject(error);
}
);
});
}
};
}
15 changes: 14 additions & 1 deletion app/utils/SessionCacher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AuthState } from "components/AppProvider";
import type { AuthState, UserSignUpData } from "components/AppProvider";

/*
This class exists to sync a user's auth state, which is managed by the AppProvider
Expand All @@ -19,4 +19,17 @@ export default class SessionCacher {
static clearSession() {
sessionStorage.removeItem("authObject");
}

static setUserSignUpData(userData: UserSignUpData) {
sessionStorage.setItem("userSignUpData", JSON.stringify(userData));
}

static getUserSignUpData(): UserSignUpData {
const object = sessionStorage.getItem("userSignUpData");
return object ? JSON.parse(object) : null;
}

static clearUserSignUpData() {
sessionStorage.removeItem("userSignUpData");
}
}
2 changes: 1 addition & 1 deletion config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ MOHCD_SUBDOMAIN: "testing"
AUTH0_AUDIENCE: "http://localhost:8080/api"
AUTH0_CLIENT_ID: "UcnuRrX6S0SeDEhW9PRe01wEhcvIRuwc"
AUTH0_DOMAIN: "dev-nykixf8szsm220fi.us.auth0.com"
AUTH0_REDIRECT_URI: "http://localhost:8080"
AUTH0_REDIRECT_URI: "http://localhost:8080"

0 comments on commit 6d646ee

Please sign in to comment.