Skip to content

Commit

Permalink
Merge pull request #26 from uwblueprint/S24/jerry/private-routes
Browse files Browse the repository at this point in the history
Private Routes With Roles
  • Loading branch information
jerry-cheng5 authored Jul 24, 2024
2 parents c3c1bc0 + ed9c926 commit abe4eb6
Show file tree
Hide file tree
Showing 14 changed files with 114 additions and 50 deletions.
8 changes: 4 additions & 4 deletions backend/typescript/middlewares/validators/userValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const createUserDtoValidator = async (
if (!validatePrimitive(req.body.email, "string")) {
return res.status(400).send(getApiValidationError("email", "string"));
}
if (!validatePrimitive(req.body.roleId, "integer")) {
return res.status(400).send(getApiValidationError("roleId", "integer"));
if (!validatePrimitive(req.body.role, "string")) {
return res.status(400).send(getApiValidationError("role", "string"));
}
if (!validatePrimitive(req.body.password, "string")) {
return res.status(400).send(getApiValidationError("password", "string"));
Expand Down Expand Up @@ -72,8 +72,8 @@ export const updateUserDtoValidator = async (
if (!validatePrimitive(req.body.email, "string")) {
return res.status(400).send(getApiValidationError("email", "string"));
}
if (!validatePrimitive(req.body.roleId, "integer")) {
return res.status(400).send(getApiValidationError("roleId", "integer"));
if (!validatePrimitive(req.body.role, "string")) {
return res.status(400).send(getApiValidationError("role", "string"));
}
if (
req.body.skillLevel !== undefined &&
Expand Down
2 changes: 1 addition & 1 deletion backend/typescript/rest/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ authRouter.post("/register", registerRequestValidator, async (req, res) => {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
roleId: req.body.roleId,
role: req.body.role,
skillLevel: req.body.skillLevel ?? null,
canSeeAllLogs: req.body.canSeeAllLogs ?? null,
canAssignUsersToTasks: req.body.canAssignUsersToTasks ?? null,
Expand Down
7 changes: 2 additions & 5 deletions backend/typescript/rest/entityRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@ import {
} from "../services/interfaces/IEntityService";
import { getErrorMessage } from "../utilities/errorUtils";
import { sendResponseByMimeType } from "../utilities/responseUtil";
import { RoleEnum } from "../types";

const upload = multer({ dest: "uploads/" });

const entityRouter: Router = Router();
entityRouter.use(
isAuthorizedByRole(
new Set(["Administrator", "Animal Behaviourist", "Staff", "Volunteer"]),
),
);
entityRouter.use(isAuthorizedByRole(new Set(Object.values(RoleEnum))));

const defaultBucket = process.env.FIREBASE_STORAGE_DEFAULT_BUCKET || "";
const fileStorageService: IFileStorageService = new FileStorageService(
Expand Down
7 changes: 2 additions & 5 deletions backend/typescript/rest/simpleEntityRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ import {
} from "../services/interfaces/simpleEntityService";
import { getErrorMessage } from "../utilities/errorUtils";
import { sendResponseByMimeType } from "../utilities/responseUtil";
import { RoleEnum } from "../types";

const simpleEntityRouter: Router = Router();
simpleEntityRouter.use(
isAuthorizedByRole(
new Set(["Administrator", "Animal Behaviourist", "Staff", "Volunteer"]),
),
);
simpleEntityRouter.use(isAuthorizedByRole(new Set(Object.values(RoleEnum))));

const simpleEntityService: ISimpleEntityService = new SimpleEntityService();

Expand Down
4 changes: 2 additions & 2 deletions backend/typescript/rest/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ userRouter.post("/", createUserDtoValidator, async (req, res) => {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
roleId: req.body.roleId,
role: req.body.role,
skillLevel: req.body.skillLevel ?? null,
canSeeAllLogs: req.body.canSeeAllLogs ?? null,
canAssignUsersToTasks: req.body.canSeeAllUsers ?? null,
Expand Down Expand Up @@ -128,7 +128,7 @@ userRouter.put("/:userId", updateUserDtoValidator, async (req, res) => {
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
roleId: req.body.roleId,
role: req.body.role,
skillLevel: req.body.skillLevel ?? null,
canSeeAllLogs: req.body.canSeeAllLogs ?? null,
canAssignUsersToTasks: req.body.canSeeAllUsers ?? null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { snakeCase } from "lodash";
import UserModel from "../../../models/user.model";
import UserService from "../userService";

import { RoleId, UserDTO, DTOTypes } from "../../../types";
import { RoleEnum, UserDTO, DTOTypes } from "../../../types";

import { testSql } from "../../../testUtils/testDb";

Expand All @@ -12,13 +12,13 @@ const testUsers = [
firstName: "Peter",
lastName: "Pan",
authId: "123",
roleId: RoleId.Administrator,
role: RoleEnum.Administrator,
},
{
firstName: "Wendy",
lastName: "Darling",
authId: "321",
roleId: RoleId.Staff,
role: RoleEnum.Staff,
},
];

Expand Down Expand Up @@ -58,7 +58,7 @@ describe("pg userService", () => {
res.forEach((user: UserDTO, i) => {
expect(user.firstName).toEqual(testUsers[i].firstName);
expect(user.lastName).toEqual(testUsers[i].lastName);
expect(user.roleId).toEqual(testUsers[i].roleId);
expect(user.role).toEqual(testUsers[i].role);
});
});
});
4 changes: 2 additions & 2 deletions backend/typescript/services/implementations/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as firebaseAdmin from "firebase-admin";
import IAuthService from "../interfaces/authService";
import IEmailService from "../interfaces/emailService";
import IUserService from "../interfaces/userService";
import { AuthDTO, Role, RoleId, Token } from "../../types";
import { AuthDTO, Role, RoleEnum, Token } from "../../types";
import { getErrorMessage } from "../../utilities/errorUtils";
import FirebaseRestClient from "../../utilities/firebaseRestClient";
import logger from "../../utilities/logger";
Expand Down Expand Up @@ -63,7 +63,7 @@ class AuthService implements IAuthService {
firstName: googleUser.firstName,
lastName: googleUser.lastName,
email: googleUser.email,
roleId: RoleId.Staff,
role: RoleEnum.Staff,
password: "",
},
googleUser.localId,
Expand Down
68 changes: 50 additions & 18 deletions backend/typescript/services/implementations/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class UserService implements IUserService {
firstName: user.first_name,
lastName: user.last_name,
email: firebaseUser.email ?? "",
roleId: user.role_id,
role: user.role.role_name,
skillLevel: user.skill_level,
canSeeAllLogs: user.can_see_all_logs,
canAssignUsersToTasks: user.can_assign_users_to_tasks,
Expand All @@ -48,6 +48,7 @@ class UserService implements IUserService {
firebaseUser = await firebaseAdmin.auth().getUserByEmail(email);
user = await PgUser.findOne({
where: { auth_id: firebaseUser.uid },
include: [{ model: PgRole, as: "role" }],
});

if (!user) {
Expand All @@ -65,7 +66,7 @@ class UserService implements IUserService {
firstName: user.first_name,
lastName: user.last_name,
email: firebaseUser.email ?? "",
roleId: user.role_id,
role: user.role.role_name,
skillLevel: user.skill_level,
canSeeAllLogs: user.can_see_all_logs,
canAssignUsersToTasks: user.can_assign_users_to_tasks,
Expand Down Expand Up @@ -128,7 +129,9 @@ class UserService implements IUserService {
async getUsers(): Promise<Array<UserDTO>> {
let userDtos: Array<UserDTO> = [];
try {
const users: Array<PgUser> = await PgUser.findAll();
const users: Array<PgUser> = await PgUser.findAll({
include: [{ model: PgRole, as: "role" }],
});

userDtos = await Promise.all(
users.map(async (user) => {
Expand All @@ -148,7 +151,7 @@ class UserService implements IUserService {
firstName: user.first_name,
lastName: user.last_name,
email: firebaseUser.email ?? "",
roleId: user.role_id,
role: user.role.role_name,
skillLevel: user.skill_level,
canSeeAllLogs: user.can_see_all_logs,
canAssignUsersToTasks: user.can_assign_users_to_tasks,
Expand Down Expand Up @@ -184,18 +187,37 @@ class UserService implements IUserService {
});
}

let roleId;
if (user.role) {
const role = await PgRole.findOne({ where: { role_name: user.role } });
if (!role) {
Logger.error(`Role ${user.role} not found in database.`);
throw new Error(`Role ${user.role} not found in database.`);
}
roleId = role.id;
}

try {
newUser = await PgUser.create({
first_name: user.firstName,
last_name: user.lastName,
auth_id: firebaseUser.uid,
role_id: user.roleId,
email: firebaseUser.email ?? "",
skill_level: user.skillLevel,
can_see_all_logs: user.canSeeAllLogs,
can_assign_users_to_tasks: user.canAssignUsersToTasks,
phone_number: user.phoneNumber,
});
newUser = await PgUser.create(
{
first_name: user.firstName,
last_name: user.lastName,
auth_id: firebaseUser.uid,
role_id: roleId,
email: firebaseUser.email ?? "",
skill_level: user.skillLevel,
can_see_all_logs: user.canSeeAllLogs,
can_assign_users_to_tasks: user.canAssignUsersToTasks,
phone_number: user.phoneNumber,
},
{
include: [
{
include: ["role"],
},
],
},
);
} catch (postgresError) {
try {
await firebaseAdmin.auth().deleteUser(firebaseUser.uid);
Expand All @@ -221,7 +243,7 @@ class UserService implements IUserService {
firstName: newUser.first_name,
lastName: newUser.last_name,
email: firebaseUser.email ?? "",
roleId: newUser.role_id,
role: newUser.role.role_name,
skillLevel: newUser.skill_level,
canSeeAllLogs: newUser.can_see_all_logs,
canAssignUsersToTasks: newUser.can_assign_users_to_tasks,
Expand All @@ -233,11 +255,21 @@ class UserService implements IUserService {
let updatedFirebaseUser: firebaseAdmin.auth.UserRecord;

try {
let roleId;
if (user.role) {
const role = await PgRole.findOne({ where: { role_name: user.role } });
if (!role) {
Logger.error(`Role ${user.role} not found in database.`);
throw new Error(`Role ${user.role} not found in database.`);
}
roleId = role.id;
}

const updateResult = await PgUser.update(
{
first_name: user.firstName,
last_name: user.lastName,
role_id: user.roleId,
role_id: roleId,
skill_level: user.skillLevel,
can_see_all_logs: user.canSeeAllLogs,
can_assign_users_to_tasks: user.canAssignUsersToTasks,
Expand Down Expand Up @@ -302,7 +334,7 @@ class UserService implements IUserService {
firstName: user.firstName,
lastName: user.lastName,
email: updatedFirebaseUser.email ?? "",
roleId: user.roleId,
role: user.role,
skillLevel: user.skillLevel,
canSeeAllLogs: user.canSeeAllLogs,
canAssignUsersToTasks: user.canAssignUsersToTasks,
Expand Down
9 changes: 8 additions & 1 deletion backend/typescript/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ export enum RoleId {
Volunteer = 4,
}

export enum RoleEnum {
Administrator = "Administrator",
AnimalBehaviourist = "Animal Behaviourist",
Staff = "Staff",
Volunteer = "Volunteer",
}

export type Token = {
accessToken: string;
refreshToken: string;
Expand All @@ -21,7 +28,7 @@ export type UserDTO = {
firstName: string;
lastName: string;
email: string;
roleId: number;
role: string;
skillLevel?: number | null;
canSeeAllLogs?: boolean | null;
canAssignUsersToTasks?: boolean | null;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/APIClients/AuthAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const loginWithGoogle = async (idToken: string): Promise<AuthenticatedUser> => {
}
};

const logout = async (userId: string | undefined): Promise<boolean> => {
const logout = async (userId: number | undefined): Promise<boolean> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import NotFound from "./components/pages/NotFound";
import UpdatePage from "./components/pages/UpdatePage";
import SimpleEntityUpdatePage from "./components/pages/SimpleEntityUpdatePage";
import * as Routes from "./constants/Routes";
import * as AuthConstants from "./constants/AuthConstants";
import AUTHENTICATED_USER_KEY from "./constants/AuthConstants";
import AuthContext from "./contexts/AuthContext";
import { getLocalStorageObj } from "./utils/LocalStorageUtils";
Expand Down Expand Up @@ -59,46 +60,55 @@ const App = (): React.ReactElement => {
exact
path={Routes.HOME_PAGE}
component={PetListPage}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<PrivateRoute
exact
path={Routes.CREATE_ENTITY_PAGE}
component={CreatePage}
allowedRoles={AuthConstants.ADMIN_AND_BEHAVIOURISTS}
/>
<PrivateRoute
exact
path={Routes.UPDATE_ENTITY_PAGE}
component={UpdatePage}
allowedRoles={AuthConstants.ADMIN_AND_BEHAVIOURISTS}
/>
<PrivateRoute
exact
path={Routes.DISPLAY_ENTITY_PAGE}
component={DisplayPage}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<PrivateRoute
exact
path={Routes.CREATE_SIMPLE_ENTITY_PAGE}
component={SimpleEntityCreatePage}
allowedRoles={AuthConstants.ADMIN_AND_BEHAVIOURISTS}
/>
<PrivateRoute
exact
path={Routes.UPDATE_SIMPLE_ENTITY_PAGE}
component={SimpleEntityUpdatePage}
allowedRoles={AuthConstants.ADMIN_AND_BEHAVIOURISTS}
/>
<PrivateRoute
exact
path={Routes.DISPLAY_SIMPLE_ENTITY_PAGE}
component={SimpleEntityDisplayPage}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<PrivateRoute
exact
path={Routes.EDIT_TEAM_PAGE}
component={EditTeamInfoPage}
allowedRoles={AuthConstants.ADMIN_AND_BEHAVIOURISTS}
/>
<PrivateRoute
exact
path={Routes.HOOKS_PAGE}
component={HooksDemo}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<Route exact path="*" component={NotFound} />
</Switch>
Expand Down
15 changes: 10 additions & 5 deletions frontend/src/components/auth/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ type PrivateRouteProps = {
component: React.FC;
path: string;
exact: boolean;
allowedRoles: Set<string>;
};

const PrivateRoute: React.FC<PrivateRouteProps> = ({
component,
exact,
path,
allowedRoles,
}: PrivateRouteProps) => {
const { authenticatedUser } = useContext(AuthContext);

return authenticatedUser ? (
<Route path={path} exact={exact} component={component} />
) : (
<Redirect to={LOGIN_PAGE} />
);
if (!authenticatedUser) {
return <Redirect to={LOGIN_PAGE} />;
}
if (!allowedRoles.has(authenticatedUser.role)) {
return <Redirect to="*" />;
}

return <Route path={path} exact={exact} component={component} />;
};

export default PrivateRoute;
Loading

0 comments on commit abe4eb6

Please sign in to comment.