diff --git a/server/controllers/memberControllers.ts b/server/controllers/memberControllers.ts index 0c3909b..c7b8623 100644 --- a/server/controllers/memberControllers.ts +++ b/server/controllers/memberControllers.ts @@ -156,7 +156,7 @@ export const getMemberRoleById = async ( throw new ConflictError("The user is not a member of the project"); next( - new SuccessResponse("Member role has been updated successfully.", { + new SuccessResponse("Member role has been fetched successfully.", { role: member.members[0].role, }) ); diff --git a/server/controllers/projectController.ts b/server/controllers/projectController.ts index 93e6e81..739bd98 100644 --- a/server/controllers/projectController.ts +++ b/server/controllers/projectController.ts @@ -8,6 +8,7 @@ import User from "@root/models/userModel.ts"; import SuccessResponse from "@root/success/SuccessResponse.ts"; import CreatedResponse from "@root/success/CreatedResponse.ts"; import Changelog from "@root/models/changelogModel.ts"; +import { ValidatedRoleRequest } from "@root/types/authTypes.ts"; /** * Get projects based on query parameters. @@ -80,20 +81,28 @@ export const getAllProjects = async ( // Get a project by ID export const getProjectById = async ( - req: Request, + req: ValidatedRoleRequest, res: Response, next: NextFunction ) => { try { + const userId = req.userId + // Find the project by ID - const project = await Project.findById(req.params.id).select('-members'); + const project = await Project.findById(req.params.projectId); // Check if the project exists if (!project) throw new NotFoundError("Project not found."); + const member = project.members.find((user) => user.userId.toString() === userId) + const userRole = member ? member.role : null + + const { members, ...projectData} = project.toJSON() + next( new SuccessResponse("Successfully fetched the project.", { - project, + project: projectData, + userRole }) ); } catch (err: any) { @@ -109,14 +118,14 @@ export const editProject = async ( ) => { try { // Find the project by ID - const project = await Project.findById(req.params.id); + const project = await Project.findById(req.params.projectId); // Check if the project exists if (!project) throw new NotFoundError("Project not found."); // Update the project const updatedProject = await Project.findByIdAndUpdate( - req.params.id, + req.params.projectId, req.body, { new: true } ).select('-data -members'); @@ -184,7 +193,7 @@ export const saveProject = async ( ) => { try { // Find the project by ID - const project = await Project.findById(req.params.id).select('-members'); + const project = await Project.findById(req.params.projectId).select('-members'); const { data, members = [] } = req.body; // Check if the project exists @@ -238,7 +247,7 @@ export const deleteProject = async ( next: NextFunction ) => { try { - const project = await Project.findByIdAndDelete(req.params.id); + const project = await Project.findByIdAndDelete(req.params.projectId); if (!project) throw new NotFoundError("Project not found."); diff --git a/server/middleware/validators/memberRoleValidator.ts b/server/middleware/validators/memberRoleValidator.ts index 4418d1d..31312f4 100644 --- a/server/middleware/validators/memberRoleValidator.ts +++ b/server/middleware/validators/memberRoleValidator.ts @@ -1,35 +1,58 @@ +import NotFoundError from "@root/errors/NotFoundError"; import ValidationError from "@root/errors/ValidationError"; import Project from "@root/models/projectModel" +import User from "@root/models/userModel"; import { Response, NextFunction } from "express"; + +const getMemberByEmail = async (email: string) => { + const user = await User.findOne( + { email: email } + ) + + if (!user) { + console.log("User not found") + throw new NotFoundError("User not found"); + } + + return user._id.toString() +} + const getMemberRole = async (projectId: string, userId: string) => { - const project = await Project.findOne( - { _id: projectId, "members.userId": userId }, - { "members.$": 1 } - ); + const project = await Project.findById(projectId).select("members generalAccess"); - console.log(projectId, userId) + if (!project) { + console.log("Project not found") + throw new NotFoundError("Project not found"); + } + + const member = project.members.find((user) => user.userId.toString() === userId); + const memberRole = member ? member.role : null; + const generalRole = project.generalAccess?.role || null; + const accessType = project.generalAccess?.accessType || "Restricted"; - if (!project || project.members.length === 0) { - console.log("Project or member not found") - return null; + if (accessType === "Restricted" && !memberRole) { + console.log("Access denied: User is not a member of the project."); + throw new NotFoundError("Access denied: User is not a member."); } - console.log("Got the member role") - return project.members[0].role; + return { memberRole, generalRole }; } export const checkRole = (allowedRoles: string[]) => async (req: any, res: Response, next: NextFunction) => { try { const { projectId } = req.params; - const userId = req.passport; - - console.log(req.user) + const userId = await getMemberByEmail(req.user.email); - const role = await getMemberRole(projectId, userId) + const { memberRole, generalRole } = await getMemberRole(projectId, userId) - if (!role || !allowedRoles.includes(role)) - throw new ValidationError("Access denied") + const hasAccess = (memberRole && allowedRoles.includes(memberRole)) || (generalRole && allowedRoles.includes(generalRole)); + + if (!hasAccess) { + throw new ValidationError("Access denied: Insufficient permissions."); + } + + req.userId = userId; next() } catch (error) { diff --git a/server/middleware/validators/projectValidator.ts b/server/middleware/validators/projectValidator.ts index e312799..efa3023 100644 --- a/server/middleware/validators/projectValidator.ts +++ b/server/middleware/validators/projectValidator.ts @@ -10,7 +10,7 @@ export const validate = (req: Request, res: Response, next: NextFunction) => { next(); }; -export const validateProjectId = param("id") +export const validateProjectId = param("projectId") .trim() .isMongoId() .withMessage("The project ID must be a valid MongoDB ID."); diff --git a/server/routes/memberRoutes.ts b/server/routes/memberRoutes.ts index 83c43b6..15e39c6 100644 --- a/server/routes/memberRoutes.ts +++ b/server/routes/memberRoutes.ts @@ -24,6 +24,7 @@ const router = Router(); router.get( "/:projectId/members", [validateToken, validateProjectId, validate], + checkRole(["Viewer", "Editor", "Admin", "Owner"]), getMembersByProjectId ); router.post( @@ -34,7 +35,7 @@ router.post( validateRoleOptional, validate, ], - // checkRole(["Editor", "Admin", "Owner"]), + checkRole(["Editor", "Admin", "Owner"]), addMember ); router.patch( @@ -46,7 +47,7 @@ router.patch( validateRoleRequired, validate, ], - // checkRole(["Admin", "Owner"]), + checkRole(["Admin", "Owner"]), editMemberRole ); router.get( @@ -57,12 +58,13 @@ router.get( validateUserId, validate, ], + checkRole(["Viewer", "Editor", "Admin", "Owner"]), getMemberRoleById ); router.delete( "/:projectId/members/:userId", [validateToken, validateProjectId, validateUserId, validate], - // checkRole(["Admin", "Owner"]), + checkRole(["Admin", "Owner"]), removeMember ); router.patch( @@ -74,7 +76,7 @@ router.patch( validateAccessRole, validate ], - // checkRole(["Admin", "Owner"]), + checkRole(["Admin", "Owner"]), editGeneralAccess ) diff --git a/server/routes/projectRoutes.ts b/server/routes/projectRoutes.ts index efd2c5d..02619d6 100644 --- a/server/routes/projectRoutes.ts +++ b/server/routes/projectRoutes.ts @@ -23,6 +23,7 @@ import { validateOnlyDataField, } from "@root/middleware/validators/projectValidator"; import { validateToken } from "@root/middleware/validators/authValidator"; +import { checkRole } from "@root/middleware/validators/memberRoleValidator"; const router = express.Router(); @@ -44,7 +45,12 @@ router.get("", [validateToken, validateUserIdQuery, validate], getProjects); * - validateProjectId: Ensures the provided project ID is valid. * - validate: General validation middleware. */ -router.get("/:id", [validateToken, validateProjectId, validate], getProjectById); +router.get( + "/:projectId", + [validateToken, validateProjectId, validate], + checkRole(["Viewer", "Editor", "Admin", "Owner"]), + getProjectById +); /** * PATCH /projects/:id @@ -57,7 +63,7 @@ router.get("/:id", [validateToken, validateProjectId, validate], getProjectById) * - validate: General validation middleware. */ router.patch( - "/:id", + "/:projectId", [ validateToken, validateProjectId, @@ -80,7 +86,7 @@ router.patch( * */ router.patch( - "/:id/data", + "/:projectId/data", [ validateToken, validateProjectId, @@ -88,6 +94,7 @@ router.patch( validateOnlyDataField, validate, ], + checkRole(["Viewer", "Editor", "Admin", "Owner"]), saveProject ); @@ -113,7 +120,7 @@ router.post( * - validate: General validation middleware. */ router.delete( - "/:id", + "/:projectId", [validateToken, validateProjectId, validate], deleteProject ); diff --git a/server/types/authTypes.ts b/server/types/authTypes.ts index fbf5629..a21556d 100644 --- a/server/types/authTypes.ts +++ b/server/types/authTypes.ts @@ -1,6 +1,11 @@ import { Request } from "express"; import { Session } from "express-session"; + +export interface ValidatedRoleRequest extends Request { + userId?: string +} + export interface AuthRequest
extends Request
{
session: CustomSession;
user?: any;
diff --git a/src/data/repo/useDataInitializer.ts b/src/data/repo/useDataInitializer.ts
index 888c991..d2119b5 100644
--- a/src/data/repo/useDataInitializer.ts
+++ b/src/data/repo/useDataInitializer.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
-import { useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
import { useEmojiStore } from "../../store/globalStore";
import { getProjectsApi } from "../api/projectsApi";
import { db } from "../db/db";
@@ -9,6 +9,8 @@ import useHistoryRepo from "./useHistoryRepo";
import useProjectRepo from "./useProjectRepo";
import { useSettingsRepo } from "./useSettingsRepo";
import useUserRepo from "./useUserRepo";
+import CustomNotification from "../../components/ui/CustomNotification";
+import { AxiosError } from "axios";
export const useDataInitializer = () => {
const { user } = useUserRepo();
@@ -16,6 +18,7 @@ export const useDataInitializer = () => {
const { loadChangelogs } = useChangelogRepo();
const { fetchUserSettings } = useSettingsRepo();
const { resetHistory } = useHistoryRepo();
+ const navigate = useNavigate();
// Router
const params = useParams();
@@ -35,19 +38,34 @@ export const useDataInitializer = () => {
}, []);
useEffect(() => {
- if (id && user) {
- selectProject(id, user.id);
- loadChangelogs(id);
- resetHistory();
+ const loadSelectedProject = async () => {
+ try {
+ if (id && user && isLoaded) {
+ const res = await selectProject(id);
+ resetHistory();
+ if (res) {
+ loadChangelogs(id);
+ }
+
+ }
+ } catch (err) {
+ if (err instanceof AxiosError) {
+ CustomNotification({
+ status: "error",
+ title: "Failed to Load Project",
+ message: err.response?.data.message,
+ });
+ }
+ navigate("/", { replace: true })
+ }
}
+
+ loadSelectedProject()
}, [id, isLoaded]);
const loadProjects = async () => {
console.log("Loading projects from local storage");
- // Dexie fetching of projects
- // NOTE: Not needed anymore
- // const projectList = await db.projects.toArray();
if (!user) return;
const getProjectList = await getProjectsApi(user?.id);
diff --git a/src/data/repo/useMemberRepo.ts b/src/data/repo/useMemberRepo.ts
index dad9797..4f338f9 100644
--- a/src/data/repo/useMemberRepo.ts
+++ b/src/data/repo/useMemberRepo.ts
@@ -37,7 +37,7 @@ const useMemberRepo = () => {
try {
setLoading(true);
setError(null);
-
+ console.log("Get member")
const response = await getProjectMembersApi(projectId);
if (response.success) {
diff --git a/src/data/repo/useProjectRepo.ts b/src/data/repo/useProjectRepo.ts
index e76e231..e270707 100644
--- a/src/data/repo/useProjectRepo.ts
+++ b/src/data/repo/useProjectRepo.ts
@@ -15,14 +15,21 @@ import { getMemberRoleApi } from "../api/membersApi";
const useProjectRepo = () => {
const projects = useProjectStore((state) => state.projects);
const selectedProject = useProjectStore((state) => state.selectedProject);
+ const selectedProjectRole = useProjectStore((state) => state.selectedProjectRole);
const setProjects = useProjectStore((state) => state.setProjects);
- const getProjects = useProjectStore((state) => state.getProjects);
+
const setSelectedProject = useProjectStore(
(state) => state.setSelectedProject
);
const clearSelectedProject = useProjectStore(
(state) => state.clearSelectedProject
);
+ const setSelectedProjectRole = useProjectStore(
+ (state) => state.setSelectedProjectRole
+ );
+ const clearSelectedProjectRole = useProjectStore(
+ (state) => state.clearSelectedProjectRole
+ );
const clearStateSnap = useEditorStore((state) => state.clearStateSnapshot);
const addState = useProjectStore((state) => state.addProject);
@@ -41,7 +48,6 @@ const useProjectRepo = () => {
const setCanUndo = useEditorStore((state) => state.setCanUndo);
const setCanRedo = useEditorStore((state) => state.setCanRedo);
- const setAccess = useMemberStore((state) => state.setCurrentProjectAccess)
const getProjectsList = () => {
return projects;
@@ -97,7 +103,7 @@ const useProjectRepo = () => {
setCanRedo(false);
};
- const selectProject = async (projectId: string | undefined, userId: string | undefined) => {
+ const selectProject = async (projectId: string | undefined) => {
if (!projectId) return;
// Save the previous current project to cache
@@ -107,7 +113,9 @@ const useProjectRepo = () => {
// Fetch the new selected project
const res = await getProjectByIdApi(projectId);
+ console.log(res)
const selected = res.data.project;
+ const role = res.data.userRole
const cache = getCache().find((item) => item.id === projectId);
if (cache) {
@@ -119,17 +127,15 @@ const useProjectRepo = () => {
}
setSelectedProject(selected);
+ setSelectedProjectRole(role);
- if (!userId) return;
-
- const memberRes = await getMemberRoleApi(projectId, userId);
-
- setAccess(memberRes.data.role)
+ return res
};
const clearProject = () => {
// Set selected project to none
clearSelectedProject();
+ clearSelectedProjectRole();
clearStateSnap();
// Delete cleared project from cache
@@ -195,12 +201,18 @@ const useProjectRepo = () => {
// Implement the duplication of project in the backend
};
+ const validateRole = () => {
+ if(!selectedProject) return false;
+ return selectedProject.generalAccess.role != "Viewer" || (selectedProjectRole != null && selectedProjectRole != "Viewer")
+ };
+
return {
projects,
getProjectsList,
setProjectList,
getProjectById,
selectedProject,
+ selectedProjectRole,
selectProject,
clearProject,
addProject,
@@ -208,6 +220,7 @@ const useProjectRepo = () => {
deleteProject,
duplicateProject,
loadProjectData,
+ validateRole
};
};
diff --git a/src/data/repo/useUserRepo.ts b/src/data/repo/useUserRepo.ts
index 80a5c0b..6a1e14a 100644
--- a/src/data/repo/useUserRepo.ts
+++ b/src/data/repo/useUserRepo.ts
@@ -23,6 +23,9 @@ const useUserRepo = () => {
const clearSelectedProject = useProjectStore(
(state) => state.clearSelectedProject
);
+ const clearSelectedProjectRole = useProjectStore(
+ (state) => state.clearSelectedProjectRole
+ );
const clearStateSnapshot = useEditorStore(
(state) => state.clearStateSnapshot
);
@@ -144,6 +147,7 @@ const useUserRepo = () => {
// Clear the selected project
clearSelectedProject();
+ clearSelectedProjectRole();
// Clear the editor state snapshot
clearStateSnapshot();
diff --git a/src/layouts/TopLeftBar.tsx b/src/layouts/TopLeftBar.tsx
index ae0e362..c1422b3 100644
--- a/src/layouts/TopLeftBar.tsx
+++ b/src/layouts/TopLeftBar.tsx
@@ -44,6 +44,7 @@ import { determineTitle } from "../utils/successHelpers";
import useMemberRepo from "../data/repo/useMemberRepo";
import { useMemberStore } from "../store/useMemberStore";
import CustomNotification from "../components/ui/CustomNotification";
+import { APIResponse, SavedProject } from "../types/APITypes";
function TopLeftBar() {
const [drawerLocalStorage, setDrawerLocalStorage] = useLocalStorage({
@@ -81,21 +82,37 @@ function ActionButtons({
const { onSave, onUndo, canUndo, canRedo, onRedo } = useHistoryRepo();
const { saveChangelog } = useChangelogRepo();
const { hasPendingChanges, } = useEditorStore();
- const { selectedProject } = useProjectRepo()
- const { currentProjectAccess } = useMemberStore()
+ const { validateRole } = useProjectRepo()
const handleSave = async () => {
- const res = await onSave();
-
- if (res?.success) {
- const changelog = res.data.changelog;
- await saveChangelog(changelog);
+ try {
+ const res = await onSave();
+
+ if (res?.success) {
+ const changelog = res.data.changelog;
+ saveChangelog(changelog);
+ showNotification(res);
+ }
+ } catch (err) {
+ showNotification({
+ success: false,
+ message: "An error has occured while saving the project",
+ } as APIResponse