Skip to content

Abeer/account designs #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
3 changes: 0 additions & 3 deletions backend/middlewares/validators/userValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ export const updateUserDtoValidator = async (
if (!validatePrimitive(req.body.lastName, "string")) {
return res.status(400).send(getApiValidationError("lastName", "string"));
}
if (!validatePrimitive(req.body.email, "string")) {
return res.status(400).send(getApiValidationError("email", "string"));
}
if (!validatePrimitive(req.body.role, "string")) {
return res.status(400).send(getApiValidationError("role", "string"));
}
Expand Down
19 changes: 19 additions & 0 deletions backend/rest/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,25 @@ authRouter.post(
},
);

authRouter.put(
"/changePassword",
updateTemporaryPasswordRequestValidator,
async (req, res) => {
try {
const accessToken = getAccessToken(req)!;
const newAccessToken = await authService.changeUserPassword(
accessToken,
req.body.newPassword,
);
res.status(200).json({
accessToken: newAccessToken,
});
} catch (error: unknown) {
res.status(500).json({ error: getErrorMessage(error) });
}
},
);

authRouter.post(
"/updateUserStatus",
updateUserStatusRequestValidator,
Expand Down
21 changes: 19 additions & 2 deletions backend/rest/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ userRouter.put(
const updatedUser = await userService.updateUserById(req.params.userId, {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
role: req.body.role,
status: "Active",
});
Expand Down Expand Up @@ -321,7 +320,6 @@ userRouter.put(
const updateLearnerPayload: UpdateUserDTO = {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
role: req.body.role,
status: "Active",
};
Expand All @@ -338,4 +336,23 @@ userRouter.put(
},
);

userRouter.put(
"/updateMyAccount/:userId",
isAuthorizedByRole(new Set(["Administrator", "Facilitator", "Learner"])),
updateUserDtoValidator,
async (req, res) => {
try {
const updatedUser = await userService.updateUserById(req.params.userId, {
firstName: req.body.firstName,
lastName: req.body.lastName,
role: req.body.role,
status: "Active",
});
res.status(200).json(updatedUser);
} catch (error: unknown) {
res.status(500).send(getErrorMessage(error));
}
},
);

export default userRouter;
44 changes: 10 additions & 34 deletions backend/services/implementations/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,43 +210,20 @@ class UserService implements IUserService {

try {
// must explicitly specify runValidators when updating through findByIdAndUpdate
oldUser = await MgUser.findByIdAndUpdate(userId, user, {
runValidators: true,
});
oldUser = await MgUser.findByIdAndUpdate(
userId,
{
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
status: user.status,
},
{ runValidators: true },
);

if (!oldUser) {
throw new Error(`userId ${userId} not found.`);
}

try {
await firebaseAdmin
.auth()
.updateUser(oldUser.authId, { email: user.email });
} catch (error) {
// rollback MongoDB user updates
try {
await MgUser.findByIdAndUpdate(
userId,
{
firstName: oldUser.firstName,
lastName: oldUser.lastName,
role: oldUser.role,
status: oldUser.status,
},
{ runValidators: true },
);
} catch (mongoDbError: unknown) {
const errorMessage = [
"Failed to rollback MongoDB user update after Firebase user update failure. Reason =",
getErrorMessage(mongoDbError),
"MongoDB user id with possibly inconsistent data =",
oldUser.id,
];
Logger.error(errorMessage.join(" "));
}

throw error;
}
} catch (error: unknown) {
Logger.error(`Failed to update user. Reason = ${getErrorMessage(error)}`);
throw error;
Expand All @@ -256,7 +233,6 @@ class UserService implements IUserService {
...oldUser.toObject(),
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
role: user.role,
status: user.status,
};
Expand Down
2 changes: 1 addition & 1 deletion backend/types/userTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type UserDTO = {

export type CreateUserDTO = Omit<UserDTO, "id"> & { password: string };

export type UpdateUserDTO = Omit<UserDTO, "id">;
export type UpdateUserDTO = Omit<UserDTO, "id" | "email">;

export type SignupUserDTO = Omit<CreateUserDTO, "role">;

Expand Down
31 changes: 31 additions & 0 deletions frontend/src/APIClients/AuthAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ const updateTemporaryPassword = async (
}
};

const changePassword = async (
email: string,
newPassword: string,
role: Role,
): Promise<boolean> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
)}`;
try {
await baseAPIClient.put(
`/auth/changePassword`,
{ newPassword },
{ headers: { Authorization: bearerToken } },
);
const newAuthenticatedUser = await login(email, newPassword, role);
if (!newAuthenticatedUser) {
throw new Error("Unable to authenticate user after logging in.");
}
setLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
newAuthenticatedUser.accessToken,
);
return true;
} catch (error) {
return false;
}
};

const updateUserStatus = async (newStatus: Status): Promise<boolean> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
Expand Down Expand Up @@ -165,6 +195,7 @@ export default {
signup,
resetPassword,
updateTemporaryPassword,
changePassword,
updateUserStatus,
refresh,
isUserVerified,
Expand Down
38 changes: 37 additions & 1 deletion frontend/src/APIClients/UserAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
import { Role } from "../types/AuthTypes";
import { User } from "../types/UserTypes";
import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils";
import {
getLocalStorageObjProperty,
setLocalStorageObjProperty,
} from "../utils/LocalStorageUtils";
import baseAPIClient from "./BaseAPIClient";

const getUsersByRole = async (role: Role): Promise<User[]> => {
Expand All @@ -19,6 +22,39 @@ const getUsersByRole = async (role: Role): Promise<User[]> => {
}
};

const updateUserDetails = async (
userId: string,
firstName: string,
lastName: string,
role: string,
status: string,
): Promise<User> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
)}`;
try {
const response = await baseAPIClient.put(
`/users/updateMyAccount/${userId}`,
{
firstName,
lastName,
role,
status,
},
{
headers: { Authorization: bearerToken },
},
);
setLocalStorageObjProperty(AUTHENTICATED_USER_KEY, "firstName", firstName);
setLocalStorageObjProperty(AUTHENTICATED_USER_KEY, "lastName", lastName);
return response.data;
} catch (error) {
throw new Error("Failed to update user details");
}
};

export default {
getUsersByRole,
updateUserDetails,
};
43 changes: 35 additions & 8 deletions frontend/src/components/common/MainPageButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import React from "react";
import { Typography, Button, useTheme } from "@mui/material";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import { useHistory } from "react-router-dom";
import { HOME_PAGE } from "../../constants/Routes";
import { useUser } from "../../hooks/useUser";

const MainPageButton = (): React.ReactElement => {
const history = useHistory();
const theme = useTheme();
const user = useUser();

const navigateTo = () => history.push(HOME_PAGE);
return (
<div>
<button
<>
<Button
variant="text"
onClick={navigateTo}
className="btn btn-primary"
type="button"
style={{ textAlign: "center" }}
startIcon={
<ChevronLeftIcon sx={{ color: theme.palette[user.role].Default }} />
}
sx={{
display: "flex",
padding: "10px 16px 10px 12px",
justifyContent: "center",
alignItems: "center",
gap: "8px",
flex: "1 0 0",
alignSelf: "stretch",
position: "absolute",
left: "36px",
top: "86px",
borderRadius: "4px",
}}
>
Go Back
</button>
</div>
<Typography
variant="labelLarge"
sx={{
color: theme.palette[user.role].Default,
}}
>
Back
</Typography>
</Button>
</>
);
};

Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/learners/LearnersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { Typography } from "@mui/material";
import {
isAuthenticatedFacilitator,
isAuthenticatedLearner,
} from "../../types/AuthTypes";
import { useUser } from "../../hooks/useUser";

const LearnerList = (): React.ReactElement => {
const user = useUser();

return (
<>
{isAuthenticatedFacilitator(user) && (
<Typography variant="body1">Learners: {user.learners}</Typography>
)}
{isAuthenticatedLearner(user) && (
<Typography variant="body1">Facilitator: {user.facilitator}</Typography>
)}
</>
);
};

export default LearnerList;
Loading