Skip to content

Commit

Permalink
Password Reset (#805)
Browse files Browse the repository at this point in the history
Co-authored-by: Serena Li <[email protected]> :shipit:
  • Loading branch information
lowtorola authored Oct 4, 2024
1 parent 6ca5377 commit 650e397
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 19 deletions.
1 change: 1 addition & 0 deletions frontend2/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
plugins: ["react"],
rules: {
semi: ["error", "always"], // require semicolons ending statements
"@typescript-eslint/no-unused-vars": ["warn", { varsIgnorePattern: "^_" }],
},
settings: {
react: {
Expand Down
4 changes: 2 additions & 2 deletions frontend2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "npm run _eslint && npm run _prettier -- --check",
"format": "npm run _eslint -- --fix && npm run _prettier -- --write",
"lint": "npm run _eslint -- --max-warnings=0 && npm run _prettier -- --check",
"format": "npm run _eslint -- --max-warnings=0 --fix && npm run _prettier -- --write",
"_eslint": "eslint --ext .ts,.tsx ./src",
"_prettier": "prettier \"**/*.{ts,js,tsx,md,json}\""
},
Expand Down
2 changes: 2 additions & 0 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import ErrorBoundary from "./views/ErrorBoundary";
import { searchTeamsFactory } from "api/team/teamFactories";
import PageNotFound from "views/PageNotFound";
import TeamProfile from "views/TeamProfile";
import { passwordForgotLoader } from "api/loaders/passwordForgotLoader";

const queryClient = new QueryClient({
queryCache: new QueryCache({
Expand Down Expand Up @@ -131,6 +132,7 @@ const router = createBrowserRouter([
path: "/password_forgot",
element: <PasswordForgot />,
errorElement: <ErrorBoundary />,
loader: passwordForgotLoader(queryClient),
},
{
path: "/password_change",
Expand Down
4 changes: 2 additions & 2 deletions frontend2/src/api/auth/authApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const logout = async (queryClient: QueryClient): Promise<void> => {
});
};

export const tokenVerify = async (): Promise<boolean> => {
export const loginTokenVerify = async (): Promise<boolean> => {
const accessToken = Cookies.get("access");
if (accessToken === undefined) {
return false;
Expand All @@ -71,7 +71,7 @@ export const tokenVerify = async (): Promise<boolean> => {
export const loginCheck = async (
queryClient: QueryClient,
): Promise<boolean> => {
const verified = await tokenVerify();
const verified = await loginTokenVerify();
if (!verified) {
await logout(queryClient);
}
Expand Down
15 changes: 15 additions & 0 deletions frontend2/src/api/loaders/passwordForgotLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { QueryClient } from "@tanstack/react-query";
import { loginCheck } from "api/auth/authApi";
import { type LoaderFunction } from "react-router-dom";
import { DEFAULT_EPISODE } from "utils/constants";

export const passwordForgotLoader =
(queryClient: QueryClient): LoaderFunction =>
async () => {
// Check if user is logged in
if (await loginCheck(queryClient)) {
// If user is logged in, redirect to home
window.location.href = `/${DEFAULT_EPISODE}/home`;
}
return null;
};
51 changes: 47 additions & 4 deletions frontend2/src/api/user/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import type {
UserPasswordResetConfirmCreateRequest,
UserUAvatarCreateRequest,
UserUResumeUpdateRequest,
UserPasswordResetValidateTokenCreateRequest,
ResetToken,
} from "../_autogen";
import { userMutationKeys, userQueryKeys } from "./userKeys";
import {
createUser,
doResetPassword,
forgotPassword,
resumeUpload,
updateCurrentUser,
downloadResume,
Expand All @@ -32,7 +35,8 @@ import {
myUserInfoFactory,
otherUserInfoFactory,
otherUserTeamsFactory,
tokenVerifyFactory,
loginTokenVerifyFactory,
passwordResetTokenVerifyFactory,
} from "./userFactories";
import { buildKey } from "../helpers";

Expand All @@ -44,9 +48,26 @@ export const useIsLoggedIn = (
queryClient: QueryClient,
): UseQueryResult<boolean, Error> =>
useQuery({
queryKey: buildKey(tokenVerifyFactory.queryKey, { queryClient }),
queryFn: async () => await tokenVerifyFactory.queryFn({ queryClient }),
queryKey: buildKey(loginTokenVerifyFactory.queryKey, { queryClient }),
queryFn: async () => await loginTokenVerifyFactory.queryFn({ queryClient }),
staleTime: Infinity,
refetchOnWindowFocus: false,
});

export const usePasswordResetTokenValid = ({
resetTokenRequest,
}: UserPasswordResetValidateTokenCreateRequest): UseQueryResult<
ResetToken,
Error
> =>
useQuery({
queryKey: buildKey(passwordResetTokenVerifyFactory.queryKey, {
resetTokenRequest,
}),
queryFn: async () =>
await passwordResetTokenVerifyFactory.queryFn({ resetTokenRequest }),
staleTime: Infinity,
refetchOnWindowFocus: false,
});

/**
Expand Down Expand Up @@ -157,7 +178,28 @@ export const useUpdateCurrentUserInfo = (
});

/**
* For resetting a user's password. If successful, logs in the user.
* For requesting a password reset token to be sent to the provided email. If successful, sends an email.
*/
export const useForgotPassword = ({
episodeId,
}: {
episodeId: string;
}): UseMutationResult<void, Error, string, unknown> =>
useMutation({
mutationKey: userMutationKeys.forgotPassword({ episodeId }),
mutationFn: async (email: string) => {
await toast.promise(forgotPassword({ emailRequest: { email } }), {
loading: "Sending password reset email...",
success:
"Sent password reset email!\nWait a few minutes for it to arrive.",
error:
"Error sending password reset email.\nDid you enter the correct email?",
});
},
});

/**
* For resetting a user's password. If successful, navigates to the login page.
*/
export const useResetPassword = ({
episodeId,
Expand All @@ -179,6 +221,7 @@ export const useResetPassword = ({
success: "Reset password!",
error: "Error resetting password.",
});
window.location.href = "/login";
},
});

Expand Down
11 changes: 11 additions & 0 deletions frontend2/src/api/user/userApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
type UserUMePartialUpdateRequest,
type UserUAvatarCreateRequest,
type UserUResumeUpdateRequest,
type UserPasswordResetValidateTokenCreateRequest,
type ResetToken,
} from "../_autogen";
import { DEFAULT_API_CONFIGURATION, downloadFile } from "../helpers";

Expand All @@ -28,6 +30,15 @@ export const createUser = async ({
}: UserUCreateRequest): Promise<UserCreate> =>
await API.userUCreate({ userCreateRequest });

/**
* Verify that a password reset token is valid and not expired.
* @param resetTokenRequest The password reset token to verify.
*/
export const passwordResetTokenVerify = async (
resetTokenRequest: UserPasswordResetValidateTokenCreateRequest,
): Promise<ResetToken> =>
await API.userPasswordResetValidateTokenCreate(resetTokenRequest);

/**
* Confirm resetting a user's password.
* @param passwordTokenRequest The new password and password reset token.
Expand Down
34 changes: 29 additions & 5 deletions frontend2/src/api/user/userFactories.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import type { QueryClient } from "@tanstack/react-query";
import type { QueryFactory } from "../apiTypes";
import { userQueryKeys } from "./userKeys";
import { tokenVerify } from "../auth/authApi";
import { loginTokenVerify } from "../auth/authApi";
import type {
ResetToken,
TeamPublic,
UserPasswordResetValidateTokenCreateRequest,
UserPrivate,
UserPublic,
UserURetrieveRequest,
UserUTeamsRetrieveRequest,
} from "../_autogen";
import { getCurrentUserInfo, getTeamsByUser, getUserInfoById } from "./userApi";
import {
getCurrentUserInfo,
getTeamsByUser,
getUserInfoById,
passwordResetTokenVerify,
} from "./userApi";
import toast from "react-hot-toast";

export const tokenVerifyFactory: QueryFactory<
export const loginTokenVerifyFactory: QueryFactory<
{ queryClient: QueryClient },
boolean
> = {
queryKey: userQueryKeys.tokenVerify,
queryFn: async () => await tokenVerify(),
queryKey: userQueryKeys.loginTokenVerify,
queryFn: async () => await loginTokenVerify(),
} as const;

export const passwordResetTokenVerifyFactory: QueryFactory<
UserPasswordResetValidateTokenCreateRequest,
ResetToken
> = {
queryKey: userQueryKeys.passwordResetTokenVerify,
queryFn: async ({ resetTokenRequest }) => {
const toastFn = async (): Promise<ResetToken> =>
await passwordResetTokenVerify({ resetTokenRequest });
return await toast.promise(toastFn(), {
loading: "Verifying token...",
success: "Token verified!",
error: "Error verifying token. Is it expired?",
});
},
} as const;

export const myUserInfoFactory: QueryFactory<unknown, UserPrivate> = {
Expand Down
20 changes: 17 additions & 3 deletions frontend2/src/api/user/userKeys.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {
UserPasswordResetValidateTokenCreateRequest,
UserURetrieveRequest,
UserUTeamsRetrieveRequest,
} from "../_autogen";
import type { QueryKeyBuilder, QueryKeyHolder } from "../apiTypes";

interface UserKeys {
tokenVerify: QueryKeyHolder;
loginTokenVerify: QueryKeyHolder;
passwordResetTokenVerify: QueryKeyBuilder<UserPasswordResetValidateTokenCreateRequest>;
meBase: QueryKeyHolder;
myInfo: QueryKeyHolder;
otherBase: QueryKeyBuilder<UserURetrieveRequest>;
Expand All @@ -19,8 +21,17 @@ export const userQueryKeys: UserKeys = {
key: () => ["user", "me"] as const,
},

tokenVerify: {
key: () => [...userQueryKeys.meBase.key(), "tokenVerify"] as const,
loginTokenVerify: {
key: () => [...userQueryKeys.meBase.key(), "loginTokenVerify"] as const,
},

passwordResetTokenVerify: {
key: ({ resetTokenRequest }: UserPasswordResetValidateTokenCreateRequest) =>
[
...userQueryKeys.meBase.key(),
"passwordResetTokenVerify",
resetTokenRequest.token,
] as const,
},

myInfo: {
Expand Down Expand Up @@ -49,6 +60,9 @@ export const userMutationKeys = {
updateCurrent: ({ episodeId }: { episodeId: string }) =>
["user", "update", episodeId] as const,

forgotPassword: ({ episodeId }: { episodeId: string }) =>
["user", "forgotPass", episodeId] as const,

resetPassword: ({ episodeId }: { episodeId: string }) =>
["user", "resetPass", episodeId] as const,

Expand Down
Loading

0 comments on commit 650e397

Please sign in to comment.