diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 8ff036c..c223d65 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -56,23 +56,47 @@ function authMiddleware(sequelize) { */ async function verifyProjectVisible(req, res, next) { const Project = defineProject(sequelize, DataTypes); + const Member = defineMember(sequelize, DataTypes); + Project.hasMany(Member, { foreignKey: 'projectId' }); const projectId = req.params.projectId || req.query.projectId; if (!projectId) { return res.status(400).json({ error: 'projectId is required' }); } - const project = await Project.findByPk(projectId); + const project = await Project.findOne({ + where: { id: projectId }, + include: [ + { + model: Member, + where: { userId: req.userId }, + required: false, + }, + ], + }); if (!project) { return res.status(404).send('Project not found'); } - // if project is private, only project owner can access - if (!project.isPublic && project.userId !== req.userId) { - return res.status(403).json({ error: 'Forbidden' }); + // if project is public, everyone can see + if (project.isPublic) { + next(); + return; } - next(); + // if project is private, owner and project member can see + if (project.userId === req.userId) { + next(); + return; + } + + const member = project.Members && project.Members[0]; + if (member) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); } /** diff --git a/backend/routes/members/index.js b/backend/routes/members/index.js index 36db17e..3316750 100644 --- a/backend/routes/members/index.js +++ b/backend/routes/members/index.js @@ -1,17 +1,16 @@ const express = require('express'); const router = express.Router(); -const defineMember = require('../../models/members'); const defineUser = require('../../models/users'); +const defineMember = require('../../models/members'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectManager } = require('../../middleware/auth')(sequelize); - const Member = defineMember(sequelize, DataTypes); + const { verifySignedIn, verifyProjectVisible } = require('../../middleware/auth')(sequelize); const User = defineUser(sequelize, DataTypes); - + const Member = defineMember(sequelize, DataTypes); Member.belongsTo(User, { foreignKey: 'userId' }); - router.get('/', verifySignedIn, verifyProjectManager, async (req, res) => { + router.get('/', verifySignedIn, verifyProjectVisible, async (req, res) => { const { projectId } = req.query; if (!projectId) { diff --git a/frontend/src/app/[locale]/projects/ProjectsTable.tsx b/frontend/src/app/[locale]/projects/ProjectsTable.tsx index 6330398..d8b0a57 100644 --- a/frontend/src/app/[locale]/projects/ProjectsTable.tsx +++ b/frontend/src/app/[locale]/projects/ProjectsTable.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { Table, TableHeader, diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx index f8d0e53..d3db837 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react'; import { MoreVertical } from 'lucide-react'; import { FolderType, FoldersMessages } from '@/types/folder'; @@ -11,14 +12,24 @@ type Props = { }; export default function FolderEditMenu({ folder, isDisabled, onEditClick, onDeleteClick, messages }: Props) { + const [disabledKeys, setDisabledKeys] = useState([]); + + useEffect(() => { + if (isDisabled) { + setDisabledKeys(['edit', 'delete']); + } else { + setDisabledKeys([]); + } + }, [isDisabled]); + return ( - - + onEditClick(folder)}> {messages.editFolder} diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx index 138cee1..4383063 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx @@ -118,7 +118,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) { size="sm" variant="bordered" className="m-2" - isDisabled={!context.isProjectEditable(Number(projectId))} + isDisabled={!context.isProjectDeveloper(Number(projectId))} onClick={openDialogForCreate} > {messages.newFolder} @@ -133,7 +133,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) { endContent={ void; onDeleteMember: (userDeleted: UserType) => void; messages: SettingsMessages; }; -export default function MembersTable({ members, onChangeRole, onDeleteMember, messages }: Props) { +export default function MembersTable({ members, isDisabled, onChangeRole, onDeleteMember, messages }: Props) { const headerColumns = [ { name: messages.avatar, uid: 'avatar' }, { name: messages.email, uid: 'email', sortable: true }, @@ -71,7 +72,7 @@ export default function MembersTable({ members, onChangeRole, onDeleteMember, me return ( - @@ -86,7 +87,13 @@ export default function MembersTable({ members, onChangeRole, onDeleteMember, me ); case 'delete': return ( - ); diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx index 12f67ea..4be3a56 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx @@ -69,13 +69,20 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {

{messages.memberManagement}

-
Boolean; - isAdmin: () => Boolean; - isProjectEditable: (projectId: number) => Boolean; - setToken: (token: TokenType) => {}; - storeTokenToLocalStorage: (token: TokenType) => {}; - removeTokenFromLocalStorage: () => {}; + isSignedIn: () => boolean; + isAdmin: () => boolean; + isProjectManager: (projectId: number) => boolean; + isProjectDeveloper: (projectId: number) => boolean; + setToken: (token: TokenType) => void; + storeTokenToLocalStorage: (token: TokenType) => void; + removeTokenFromLocalStorage: () => void; }; export type ProjectRoleType = { diff --git a/frontend/utils/TokenProvider.tsx b/frontend/utils/TokenProvider.tsx index 62348b8..b641035 100644 --- a/frontend/utils/TokenProvider.tsx +++ b/frontend/utils/TokenProvider.tsx @@ -6,7 +6,8 @@ import { useRouter, usePathname } from '@/src/navigation'; import { isSignedIn as tokenIsSinedIn, isAdmin as tokenIsAdmin, - isProjectEditable as tokenIsProjectEditable, + isProjectManager as tokenIsProjectManager, + isProjectDeveloper as tokenIsProjectDeveloper, checkSignInPage as tokenCheckSignInPage, fetchMyRoles, } from './token'; @@ -28,6 +29,12 @@ const defaultContext = { }, isSignedIn: () => false, isAdmin: () => false, + isProjectManager: (projectId: number) => { + return false; + }, + isProjectDeveloper: (projectId: number) => { + return false; + }, setToken: (token: TokenType) => {}, storeTokenToLocalStorage, removeTokenFromLocalStorage, @@ -55,8 +62,12 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { return tokenIsAdmin(token); }; - const isProjectEditable = (projectId: number) => { - return tokenIsProjectEditable(projectRoles, projectId); + const isProjectManager = (projectId: number) => { + return tokenIsProjectManager(projectRoles, projectId); + }; + + const isProjectDeveloper = (projectId: number) => { + return tokenIsProjectDeveloper(projectRoles, projectId); }; const tokenContext = { @@ -64,7 +75,8 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { projectRoles, isSignedIn, isAdmin, - isProjectEditable, + isProjectManager, + isProjectDeveloper, setToken, storeTokenToLocalStorage, removeTokenFromLocalStorage, diff --git a/frontend/utils/token.ts b/frontend/utils/token.ts index fb2b2e1..34405ad 100644 --- a/frontend/utils/token.ts +++ b/frontend/utils/token.ts @@ -61,7 +61,28 @@ async function fetchMyRoles(jwt: string) { } } -function isProjectEditable(projectRoles: ProjectRoleType[], projectId: number) { +function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) { + const found = projectRoles.find((role) => { + return role.projectId === projectId; + }); + + if (!found) { + return false; + } + + if (found.isOwner === true) { + return true; + } + + const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); + if (found.role === managerRoleIndex) { + return true; + } + + return false; +} + +function isProjectDeveloper(projectRoles: ProjectRoleType[], projectId: number) { const found = projectRoles.find((role) => { return role.projectId === projectId; }); @@ -113,4 +134,4 @@ function checkSignInPage(token: TokenType, pathname: string) { return ret; } -export { isSignedIn, isAdmin, isProjectEditable, isPrivatePath, checkSignInPage, fetchMyRoles }; +export { isSignedIn, isAdmin, isProjectManager, isProjectDeveloper, isPrivatePath, checkSignInPage, fetchMyRoles };