feat: view customize on member's role

This commit is contained in:
Takeshi Kimata
2024-06-02 14:14:04 +09:00
parent 8300520a86
commit 7d42d2660c
8 changed files with 154 additions and 16 deletions

View File

@@ -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);

View File

@@ -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');

View 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;
};

View File

@@ -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>

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>;
}; };

View File

@@ -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 };