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

Merged
merged 11 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -256,7 +256,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 @@ -413,7 +412,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 @@ -430,4 +428,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 @@ -221,43 +221,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 @@ -267,7 +244,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 @@ -31,7 +31,7 @@ export type CreateUserDTO = Omit<UserDTO, "id" | "bookmarks"> & {
password: string;
};

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

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 @@ -108,6 +108,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 @@ -168,6 +198,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 Down Expand Up @@ -34,7 +37,40 @@ const getUsers = async (): 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,
getUsers,
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: "12px",
top: "80px",
borderRadius: "4px",
}}
>
Go Back
</button>
</div>
<Typography
variant="labelLarge"
sx={{
color: theme.palette[user.role].Default,
}}
>
Back
</Typography>
</Button>
</>
);
};

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/common/navbar/UserButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import { AccountCircle } from "@mui/icons-material";
import { Box, IconButton, Popover, useTheme } from "@mui/material";
import RefreshCredentials from "../../auth/RefreshCredentials";
import ResetPassword from "../../auth/ResetPassword";
import Logout from "../../auth/Logout";
import MyAccountButton from "../../auth/MyAccountButton";
import ProfilePicture from "../../profile/ProfilePicture";

const UserButton = () => {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
Expand All @@ -31,7 +31,7 @@ const UserButton = () => {
onClick={handleClick}
sx={{ color: theme.palette.Neutral[400] }}
>
<AccountCircle />
<ProfilePicture size={24} />
</IconButton>
<Popover
id={id}
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
Loading