feat: view customize on member's role
This commit is contained in:
@@ -119,10 +119,12 @@ const membersIndexRoute = require('./routes/members/index')(sequelize);
|
|||||||
const membersNewRoute = require('./routes/members/new')(sequelize);
|
const membersNewRoute = require('./routes/members/new')(sequelize);
|
||||||
const membersEditRoute = require('./routes/members/edit')(sequelize);
|
const membersEditRoute = require('./routes/members/edit')(sequelize);
|
||||||
const membersDeleteRoute = require('./routes/members/delete')(sequelize);
|
const membersDeleteRoute = require('./routes/members/delete')(sequelize);
|
||||||
|
const membersCheckRoute = require('./routes/members/check')(sequelize);
|
||||||
app.use('/members', membersIndexRoute);
|
app.use('/members', membersIndexRoute);
|
||||||
app.use('/members', membersNewRoute);
|
app.use('/members', membersNewRoute);
|
||||||
app.use('/members', membersEditRoute);
|
app.use('/members', membersEditRoute);
|
||||||
app.use('/members', membersDeleteRoute);
|
app.use('/members', membersDeleteRoute);
|
||||||
|
app.use('/members', membersCheckRoute);
|
||||||
|
|
||||||
// "/home"
|
// "/home"
|
||||||
const homeIndexRoute = require('./routes/home/index')(sequelize);
|
const homeIndexRoute = require('./routes/home/index')(sequelize);
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ function authMiddleware(sequelize) {
|
|||||||
async function verifyProjectDeveloper(req, res, next) {
|
async function verifyProjectDeveloper(req, res, next) {
|
||||||
const Project = defineProject(sequelize, DataTypes);
|
const Project = defineProject(sequelize, DataTypes);
|
||||||
const Folder = defineFolder(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;
|
let projectId = req.params.projectId || req.query.projectId;
|
||||||
const folderId = req.params.folderId || req.query.folderId;
|
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) {
|
if (!project) {
|
||||||
return res.status(404).send('Project not found');
|
return res.status(404).send('Project not found');
|
||||||
}
|
}
|
||||||
@@ -176,14 +187,7 @@ function authMiddleware(sequelize) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the user is manager or developer of the project
|
const member = project.Members && project.Members[0];
|
||||||
const Member = defineMember(sequelize, DataTypes);
|
|
||||||
const member = await Member.findOne({
|
|
||||||
where: {
|
|
||||||
userId: req.userId,
|
|
||||||
projectId: projectId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (member) {
|
if (member) {
|
||||||
const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager');
|
const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager');
|
||||||
const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer');
|
const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer');
|
||||||
|
|||||||
48
backend/routes/members/check.js
Normal file
48
backend/routes/members/check.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -4,16 +4,17 @@ import { FolderType, FoldersMessages } from '@/types/folder';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
folder: FolderType;
|
folder: FolderType;
|
||||||
|
isDisabled: boolean;
|
||||||
onEditClick: (folder: FolderType) => void;
|
onEditClick: (folder: FolderType) => void;
|
||||||
onDeleteClick: (deleteFolderId: number) => void;
|
onDeleteClick: (deleteFolderId: number) => void;
|
||||||
messages: FoldersMessages;
|
messages: FoldersMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FolderEditMenu({ folder, onEditClick, onDeleteClick, messages }: Props) {
|
export default function FolderEditMenu({ folder, isDisabled, onEditClick, onDeleteClick, messages }: Props) {
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button isIconOnly size="sm" className="bg-transparent rounded-full">
|
<Button isIconOnly isDisabled={isDisabled} size="sm" className="bg-transparent rounded-full">
|
||||||
<MoreVertical size={16} />
|
<MoreVertical size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
className="m-2"
|
className="m-2"
|
||||||
|
isDisabled={!context.isProjectEditable(Number(projectId))}
|
||||||
onClick={openDialogForCreate}
|
onClick={openDialogForCreate}
|
||||||
>
|
>
|
||||||
{messages.newFolder}
|
{messages.newFolder}
|
||||||
@@ -132,6 +133,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
|||||||
endContent={
|
endContent={
|
||||||
<FolderEditMenu
|
<FolderEditMenu
|
||||||
folder={folder}
|
folder={folder}
|
||||||
|
isDisabled={!context.isProjectEditable(Number(projectId))}
|
||||||
onEditClick={onEditClick}
|
onEditClick={onEditClick}
|
||||||
onDeleteClick={onDeleteClick}
|
onDeleteClick={onDeleteClick}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
|
|||||||
@@ -28,11 +28,19 @@ export type TokenContextType = {
|
|||||||
};
|
};
|
||||||
isSignedIn: () => Boolean;
|
isSignedIn: () => Boolean;
|
||||||
isAdmin: () => Boolean;
|
isAdmin: () => Boolean;
|
||||||
|
isProjectEditable: (projectId: number) => Boolean;
|
||||||
setToken: (token: TokenType) => {};
|
setToken: (token: TokenType) => {};
|
||||||
storeTokenToLocalStorage: (token: TokenType) => {};
|
storeTokenToLocalStorage: (token: TokenType) => {};
|
||||||
removeTokenFromLocalStorage: () => {};
|
removeTokenFromLocalStorage: () => {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProjectRoleType = {
|
||||||
|
projectId: number;
|
||||||
|
isOwner: boolean;
|
||||||
|
isMember: boolean;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthMessages = {
|
export type AuthMessages = {
|
||||||
title: string;
|
title: string;
|
||||||
linkTitle: string;
|
linkTitle: string;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { createContext, useState, useEffect, useContext } from 'react';
|
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 { TokenProps } from '@/types/user';
|
||||||
import { useRouter, usePathname } from '@/src/navigation';
|
import { useRouter, usePathname } from '@/src/navigation';
|
||||||
import {
|
import {
|
||||||
isSignedIn as tokenIsSinedIn,
|
isSignedIn as tokenIsSinedIn,
|
||||||
isAdmin as tokenIsAdmin,
|
isAdmin as tokenIsAdmin,
|
||||||
|
isProjectEditable as tokenIsProjectEditable,
|
||||||
checkSignInPage as tokenCheckSignInPage,
|
checkSignInPage as tokenCheckSignInPage,
|
||||||
|
fetchMyRoles,
|
||||||
} from './token';
|
} from './token';
|
||||||
import { ToastContext } from './ToastProvider';
|
import { ToastContext } from './ToastProvider';
|
||||||
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
||||||
@@ -43,6 +45,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
|||||||
expires_at: 0,
|
expires_at: 0,
|
||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
|
const [projectRoles, setProjectRoles] = useState<ProjectRoleType[]>([]);
|
||||||
|
|
||||||
const isSignedIn = () => {
|
const isSignedIn = () => {
|
||||||
return tokenIsSinedIn(token);
|
return tokenIsSinedIn(token);
|
||||||
@@ -52,10 +55,16 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
|||||||
return tokenIsAdmin(token);
|
return tokenIsAdmin(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isProjectEditable = (projectId: number) => {
|
||||||
|
return tokenIsProjectEditable(projectRoles, projectId);
|
||||||
|
};
|
||||||
|
|
||||||
const tokenContext = {
|
const tokenContext = {
|
||||||
token,
|
token,
|
||||||
|
projectRoles,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
isProjectEditable,
|
||||||
setToken,
|
setToken,
|
||||||
storeTokenToLocalStorage,
|
storeTokenToLocalStorage,
|
||||||
removeTokenFromLocalStorage,
|
removeTokenFromLocalStorage,
|
||||||
@@ -91,6 +100,23 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
|||||||
}
|
}
|
||||||
}, [pathname, hasRestoreFinished]);
|
}, [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 <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TokenType } from '@/types/user';
|
import { ProjectRoleType, TokenType } from '@/types/user';
|
||||||
import { roles } from '@/config/selection';
|
import { roles, memberRoles } from '@/config/selection';
|
||||||
|
import Config from '@/config/config';
|
||||||
|
const apiServer = Config.apiServer;
|
||||||
|
|
||||||
function tokenExists(token: TokenType) {
|
function tokenExists(token: TokenType) {
|
||||||
if (token && token.user && token.user.username) {
|
if (token && token.user && token.user.username) {
|
||||||
@@ -36,7 +38,52 @@ function isAdmin(token: TokenType) {
|
|||||||
return false;
|
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) => {
|
const isPrivatePath = (pathname: string) => {
|
||||||
return /^\/account(\/)?$/.test(pathname) || /^\/admin(\/.*)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname);
|
return /^\/account(\/)?$/.test(pathname) || /^\/admin(\/.*)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname);
|
||||||
};
|
};
|
||||||
@@ -66,4 +113,4 @@ function checkSignInPage(token: TokenType, pathname: string) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isSignedIn, isAdmin, isPrivatePath, checkSignInPage };
|
export { isSignedIn, isAdmin, isProjectEditable, isPrivatePath, checkSignInPage, fetchMyRoles };
|
||||||
|
|||||||
Reference in New Issue
Block a user