Skip to content

Commit

Permalink
Delete user & change password features
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminJohnson2204 committed May 28, 2024
1 parent 9375693 commit 129f479
Show file tree
Hide file tree
Showing 12 changed files with 493 additions and 25 deletions.
42 changes: 42 additions & 0 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { firebaseAuth } from "src/services/firebase";
import UserModel, { DisplayUser, UserRole } from "src/models/user";
import { validationResult } from "express-validator";
import validationErrorParser from "src/util/validationErrorParser";
import createHttpError from "http-errors";

/**
* Retrieves data about the current user (their MongoDB ID, Firebase UID, and role).
Expand Down Expand Up @@ -81,3 +82,44 @@ export const createUser: RequestHandler = async (req: PAPRequest, res, next) =>
next(error);
}
};

/**
* Changes a user's password, finding the user by their UID
*/
export const changeUserPassword: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

const { password } = req.body;
const { uid } = req.params;

const updatedUser = await firebaseAuth.updateUser(uid, {
password,
});

res.status(200).json(updatedUser);
} catch (error) {
next(error);
}
};

/**
* Deletes a user from the Firebase and MongoDB databases
*/
export const deleteUser: RequestHandler = async (req, res, next) => {
try {
const { uid } = req.params;

await firebaseAuth.deleteUser(uid);

const deletedUser = await UserModel.deleteOne({ uid });
if (deletedUser === null) {
throw createHttpError(404, "User not found at uid " + uid);
}
return res.status(204).send();
} catch (error) {
next(error);
}
};
8 changes: 8 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@ router.post(
UserValidator.createUser,
UserController.createUser,
);
router.patch(
"/:uid/password",
requireSignedIn,
requireAdmin,
UserValidator.changeUserPassword,
UserController.changeUserPassword,
);
router.delete("/:uid", requireSignedIn, requireAdmin, UserController.deleteUser);

export default router;
4 changes: 3 additions & 1 deletion backend/src/validators/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { body } from "express-validator";

/**
* Validators for creating users
* Validators for creating and updating users
*/

const makeNameValidator = () =>
Expand All @@ -26,3 +26,5 @@ const makePasswordValidator = () =>
.withMessage("Password must be a string");

export const createUser = [makeNameValidator(), makeEmailValidator(), makePasswordValidator()];

export const changeUserPassword = [makePasswordValidator()];
3 changes: 3 additions & 0 deletions frontend/public/ic_lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 29 additions & 1 deletion frontend/src/api/Users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { APIResult, get, handleAPIError, post } from "@/api/requests";
import { APIResult, get, handleAPIError, httpDelete, patch, post } from "@/api/requests";
import { User as FirebaseUser } from "firebase/auth";

export interface User {
_id: string;
Expand Down Expand Up @@ -43,6 +44,33 @@ export const getAllUsers = async (firebaseToken: string): Promise<APIResult<Disp
}
};

export const changeUserPassword = async (
uid: string,
password: string,
firebaseToken: string,
): Promise<APIResult<FirebaseUser>> => {
try {
const response = await patch(
`/api/user/${uid}/password`,
{ password },
createAuthHeader(firebaseToken),
);
const json = (await response.json()) as FirebaseUser;
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
};

export const deleteUser = async (uid: string, firebaseToken: string): Promise<APIResult<null>> => {
try {
await httpDelete(`/api/user/${uid}`, createAuthHeader(firebaseToken));
return { success: true, data: null };
} catch (error) {
return handleAPIError(error);
}
};

export const createUser = async (
firebaseToken: string,
request: CreateUserRequest,
Expand Down
44 changes: 42 additions & 2 deletions frontend/src/app/staff/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ enum AllUsersError {
}

export default function Profile() {
const { firebaseUser, papUser } = useContext(UserContext);
const {
firebaseUser,
papUser,
successNotificationOpen,
setSuccessNotificationOpen,
errorNotificationOpen,
setErrorNotificationOpen,
} = useContext(UserContext);
const [users, setUsers] = useState<DisplayUser[]>();
const [loadingUsers, setLoadingUsers] = useState(false);
const [usersError, setUsersError] = useState<AllUsersError>(AllUsersError.NONE);
Expand Down Expand Up @@ -53,6 +60,11 @@ export default function Profile() {
fetchUsers();
}, [firebaseUser, papUser]);

useEffect(() => {
setSuccessNotificationOpen(null);
setErrorNotificationOpen(null);
}, []);

useRedirectToLoginIfNotSignedIn();

const renderErrorNotification = () => {
Expand Down Expand Up @@ -108,7 +120,13 @@ export default function Profile() {
</div>
{users?.map((user, index) =>
user.uid != firebaseUser?.uid ? (
<UserProfile key={index} email={user.email ?? ""} name={user.displayName ?? ""} />
<UserProfile
key={index}
afterChangeUser={fetchUsers}
uid={user.uid}
email={user.email ?? ""}
name={user.displayName ?? ""}
/>
) : null,
)}
</>
Expand All @@ -122,6 +140,28 @@ export default function Profile() {
onClose={() => setCreateUserModalOpen(false)}
afterCreateUser={fetchUsers}
/>

<NotificationBanner
variant="success"
isOpen={successNotificationOpen !== null}
mainText={
successNotificationOpen === "deleteUser"
? "User Successfully Deleted"
: "Password Changed Successfully"
}
onDismissClicked={() => setSuccessNotificationOpen(null)}
/>
<NotificationBanner
variant="error"
isOpen={errorNotificationOpen !== null}
mainText={
errorNotificationOpen === "deleteUser"
? "Unable to Delete User"
: "Unable to Change password"
}
subText="An error occurred, please check your internet connection or try again later"
onDismissClicked={() => setErrorNotificationOpen(null)}
/>
</div>
);
}
153 changes: 153 additions & 0 deletions frontend/src/components/Profile/ChangePasswordModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Image from "next/image";
import { useContext, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { BaseModal } from "@/components/shared/BaseModal";
import { changeUserPassword } from "@/api/Users";
import { UserContext } from "@/contexts/userContext";
import TextField from "@/components/shared/input/TextField";
import { IconButton } from "@mui/material";
import { Button } from "@/components/shared/Button";
import styles from "@/components/Profile/ChangePasswordModal/styles.module.css";

interface IChangePasswordFormInput {
password: string;
confirmPassword: string;
}

interface ChangePasswordModalProps {
isOpen: boolean;
onClose: () => unknown;
uid: string;
afterChangePassword: () => unknown;
}

export const ChangePasswordModal = ({
isOpen,
onClose,
uid,
afterChangePassword,
}: ChangePasswordModalProps) => {
const { firebaseUser, setSuccessNotificationOpen, setErrorNotificationOpen } =
useContext(UserContext);
const [passwordVisible, setPasswordVisible] = useState(false);
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false);

const {
handleSubmit,
register,
reset,
formState: { errors, isValid },
watch,
} = useForm<IChangePasswordFormInput>();

const onSubmit: SubmitHandler<IChangePasswordFormInput> = async (data) => {
setLoading(true);
setSuccessNotificationOpen(null);
setErrorNotificationOpen(null);

const firebaseToken = await firebaseUser?.getIdToken();
const result = await changeUserPassword(uid, data.password, firebaseToken!);
if (result.success) {
setSuccessNotificationOpen("changePassword");
} else {
console.error(`Changing password failed with error: ${result.error}`);
setErrorNotificationOpen("changePassword");
}
setLoading(false);
reset();
afterChangePassword();
onClose();
};

return (
<>
<BaseModal
isOpen={isOpen}
onClose={onClose}
title="Change User's Password"
content={
<div className={styles.root}>
<p className={styles.subtitle}>Change this user’s login credentials</p>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<TextField
label="New Password"
variant="outlined"
placeholder="Enter New Password"
{...register("password", {
required: "New password is required",

validate: {
validate: (password) =>
password.length >= 6 || "New password must be at least 6 characters",
},
})}
required={false}
error={!!errors.password}
helperText={errors.password?.message}
type={passwordVisible ? "text" : "password"}
InputProps={{
endAdornment: (
<IconButton
onClick={() => setPasswordVisible((prevVisible) => !prevVisible)}
className={styles.visibilityButton}
>
<Image
src={passwordVisible ? "/ic_show.svg" : "/ic_hide.svg"}
alt={passwordVisible ? "Show" : "Hide"}
width={17}
height={17}
/>
</IconButton>
),
}}
/>

<TextField
label="Confirm New Password"
variant="outlined"
placeholder="Re-enter New Password"
{...register("confirmPassword", {
required: "Confirm Password is required",
validate: {
validate: (confirmPassword) =>
confirmPassword === watch().password || "Passwords do not match",
},
})}
required={false}
error={!!errors.confirmPassword}
helperText={errors.confirmPassword?.message}
type={confirmPasswordVisible ? "text" : "password"}
InputProps={{
endAdornment: (
<IconButton
onClick={() => setConfirmPasswordVisible((prevVisible) => !prevVisible)}
className={styles.visibilityButton}
>
<Image
src={confirmPasswordVisible ? "/ic_show.svg" : "/ic_hide.svg"}
alt={confirmPasswordVisible ? "Show" : "Hide"}
width={17}
height={17}
/>
</IconButton>
),
}}
/>

<Button
variant="primary"
outlined={false}
text="Change Password"
loading={loading}
type="submit"
className={`${styles.submitButton} ${isValid ? "" : styles.disabledButton}`}
/>
</form>
</div>
}
bottomRow={null}
/>
</>
);
};
Loading

0 comments on commit 129f479

Please sign in to comment.