diff --git a/backend/index.js b/backend/index.js index 0bf44a5..b7dccf8 100644 --- a/backend/index.js +++ b/backend/index.js @@ -119,10 +119,12 @@ const membersIndexRoute = require('./routes/members/index')(sequelize); const membersNewRoute = require('./routes/members/new')(sequelize); const membersEditRoute = require('./routes/members/edit')(sequelize); const membersDeleteRoute = require('./routes/members/delete')(sequelize); +const membersCheckRoute = require('./routes/members/check')(sequelize); app.use('/members', membersIndexRoute); app.use('/members', membersNewRoute); app.use('/members', membersEditRoute); app.use('/members', membersDeleteRoute); +app.use('/members', membersCheckRoute); // "/home" const homeIndexRoute = require('./routes/home/index')(sequelize); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index dc67525..8ff036c 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -149,6 +149,8 @@ function authMiddleware(sequelize) { async function verifyProjectDeveloper(req, res, next) { const Project = defineProject(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes); + const Member = defineMember(sequelize, DataTypes); + Project.hasMany(Member, { foreignKey: 'projectId' }); let projectId = req.params.projectId || req.query.projectId; const folderId = req.params.folderId || req.query.folderId; @@ -166,7 +168,16 @@ function authMiddleware(sequelize) { } } - 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'); } @@ -176,14 +187,7 @@ function authMiddleware(sequelize) { return; } - // check the user is manager or developer of the project - const Member = defineMember(sequelize, DataTypes); - const member = await Member.findOne({ - where: { - userId: req.userId, - projectId: projectId, - }, - }); + const member = project.Members && project.Members[0]; if (member) { const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer'); diff --git a/backend/routes/members/check.js b/backend/routes/members/check.js new file mode 100644 index 0000000..598dd0c --- /dev/null +++ b/backend/routes/members/check.js @@ -0,0 +1,48 @@ +const express = require('express'); +const router = express.Router(); +const defineMember = require('../../models/members'); +const defineProject = require('../../models/projects'); +const { DataTypes } = require('sequelize'); + +module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const Member = defineMember(sequelize, DataTypes); + const Project = defineProject(sequelize, DataTypes); + + router.get('/check', verifySignedIn, async (req, res) => { + const userId = req.userId; + + if (!userId) { + return res.status(400).json({ error: 'userId is required' }); + } + + try { + const members = await Member.findAll({ + where: { + userId: userId, + }, + }); + + const myProjects = await Project.findAll({ + where: { + userId: userId, + }, + }); + + const projectRoles = members.map((member) => { + return { projectId: member.projectId, isOwner: false, isMember: true, role: member.role }; + }); + const ownProjectRoles = myProjects.map((project) => { + return { projectId: project.id, isOwner: true, isMember: true, role: 0 }; + }); + projectRoles.push(...ownProjectRoles); + + res.json(projectRoles); + } catch (error) { + console.error(error); + res.status(500).send('Internal Server Error'); + } + }); + + return router; +}; diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx index 2c06d5c..f8d0e53 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderEditMenu.tsx @@ -4,16 +4,17 @@ import { FolderType, FoldersMessages } from '@/types/folder'; type Props = { folder: FolderType; + isDisabled: boolean; onEditClick: (folder: FolderType) => void; onDeleteClick: (deleteFolderId: number) => void; messages: FoldersMessages; }; -export default function FolderEditMenu({ folder, onEditClick, onDeleteClick, messages }: Props) { +export default function FolderEditMenu({ folder, isDisabled, onEditClick, onDeleteClick, messages }: Props) { return ( - diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx index c7428a7..138cee1 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx @@ -118,6 +118,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) { size="sm" variant="bordered" className="m-2" + isDisabled={!context.isProjectEditable(Number(projectId))} onClick={openDialogForCreate} > {messages.newFolder} @@ -132,6 +133,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) { endContent={ Boolean; isAdmin: () => Boolean; + isProjectEditable: (projectId: number) => Boolean; setToken: (token: TokenType) => {}; storeTokenToLocalStorage: (token: TokenType) => {}; removeTokenFromLocalStorage: () => {}; }; +export type ProjectRoleType = { + projectId: number; + isOwner: boolean; + isMember: boolean; + role: number; +}; + export type AuthMessages = { title: string; linkTitle: string; diff --git a/frontend/utils/TokenProvider.tsx b/frontend/utils/TokenProvider.tsx index 1cdf707..b405a5d 100644 --- a/frontend/utils/TokenProvider.tsx +++ b/frontend/utils/TokenProvider.tsx @@ -1,12 +1,14 @@ 'use client'; import { createContext, useState, useEffect, useContext } from 'react'; -import { TokenContextType, TokenType } from '@/types/user'; +import { ProjectRoleType, TokenContextType, TokenType } from '@/types/user'; import { TokenProps } from '@/types/user'; import { useRouter, usePathname } from '@/src/navigation'; import { isSignedIn as tokenIsSinedIn, isAdmin as tokenIsAdmin, + isProjectEditable as tokenIsProjectEditable, checkSignInPage as tokenCheckSignInPage, + fetchMyRoles, } from './token'; import { ToastContext } from './ToastProvider'; const LOCAL_STORAGE_KEY = 'testplat-auth-token'; @@ -43,6 +45,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { expires_at: 0, user: null, }); + const [projectRoles, setProjectRoles] = useState([]); const isSignedIn = () => { return tokenIsSinedIn(token); @@ -52,10 +55,16 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { return tokenIsAdmin(token); }; + const isProjectEditable = (projectId: number) => { + return tokenIsProjectEditable(projectRoles, projectId); + }; + const tokenContext = { token, + projectRoles, isSignedIn, isAdmin, + isProjectEditable, setToken, storeTokenToLocalStorage, removeTokenFromLocalStorage, @@ -91,6 +100,23 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { } }, [pathname, hasRestoreFinished]); + useEffect(() => { + async function refreshProjectRoles() { + if (!hasRestoreFinished || !token || !token.access_token) { + return; + } + + try { + const data = await fetchMyRoles(token.access_token); + setProjectRoles(data); + } catch (error: any) { + console.error('Error in effect:', error.message); + } + } + + refreshProjectRoles(); + }, [hasRestoreFinished, token]); + return {children}; }; diff --git a/frontend/utils/token.ts b/frontend/utils/token.ts index 5edab7a..fb2b2e1 100644 --- a/frontend/utils/token.ts +++ b/frontend/utils/token.ts @@ -1,5 +1,7 @@ -import { TokenType } from '@/types/user'; -import { roles } from '@/config/selection'; +import { ProjectRoleType, TokenType } from '@/types/user'; +import { roles, memberRoles } from '@/config/selection'; +import Config from '@/config/config'; +const apiServer = Config.apiServer; function tokenExists(token: TokenType) { if (token && token.user && token.user.username) { @@ -36,7 +38,52 @@ function isAdmin(token: TokenType) { return false; } -// pravate paths are '/account', '/admin', '/projects/*' +async function fetchMyRoles(jwt: string) { + const fetchOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }; + + const url = `${apiServer}/members/check`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error: any) { + console.error('Error fetching data:', error.message); + } +} + +function isProjectEditable(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'); + const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer'); + if (found.role === managerRoleIndex || found.role === developerRoleIndex) { + return true; + } + + return false; +} + +// private paths are '/account', '/admin', '/projects/*' const isPrivatePath = (pathname: string) => { return /^\/account(\/)?$/.test(pathname) || /^\/admin(\/.*)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname); }; @@ -66,4 +113,4 @@ function checkSignInPage(token: TokenType, pathname: string) { return ret; } -export { isSignedIn, isAdmin, isPrivatePath, checkSignInPage }; +export { isSignedIn, isAdmin, isProjectEditable, isPrivatePath, checkSignInPage, fetchMyRoles };