feat: even project manager should not edit/delete project (#14)

* fix: remove unused import

* feat: even project manager should not edit/delete project

* docs: even project manager should not edit/delete project
This commit is contained in:
Takeshi Kimata
2024-07-20 08:23:31 +09:00
committed by GitHub
parent 4c886e4600
commit a1ad775a7a
9 changed files with 64 additions and 37 deletions

View File

@@ -7,12 +7,12 @@ const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.delete('/:projectId', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { router.delete('/:projectId', verifySignedIn, verifyProjectOwner, async (req, res) => {
const projectId = req.params.projectId; const projectId = req.params.projectId;
const t = await sequelize.transaction(); const t = await sequelize.transaction();

View File

@@ -5,10 +5,10 @@ const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
router.put('/:projectId', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { router.put('/:projectId', verifySignedIn, verifyProjectOwner, async (req, res) => {
const projectId = req.params.projectId; const projectId = req.params.projectId;
const { name, detail, isPublic } = req.body; const { name, detail, isPublic } = req.body;
try { try {

View File

@@ -29,39 +29,32 @@ There are three types of roles:
#### Project #### Project
| Action | Manager/Owner | Developer | Reporter | Not member | | Action | Owner[^1] | Manager | Developer | Reporter | Not member[^2] |
| ------ | ------------- | --------- | -------- | ---------- | | ------ | --------- | ------- | --------- | -------- | -------------- |
| Delete | ✅ | ❌ | ❌ | ❌ | | Write | ✅ | ❌ | ❌ | ❌ | ❌ |
| Update | ✅ | | | | | Read | ✅ | ✅ | | | 🌓[^3] |
| Read | ✅ | ✅ | ✅ | 🌓 |
#### Project Members #### Project Members
| Action | Manager/Owner | Developer | Reporter | Not member | | Action | Owner | Manager | Developer | Reporter | Not member |
| ----------- | ------------- | --------- | -------- | ---------- | | ------ | ----- | ------- | --------- | -------- | ---------- |
| Add | ✅ | ❌ | ❌ | ❌ | | Write | ✅ | ✅ | ❌ | ❌ | ❌ |
| Delete | ✅ | | | | | Read | ✅ | ✅ | | | 🌓 |
| Change role | ✅ | ❌ | ❌ | ❌ |
| Read | ✅ | ✅ | ✅ | 🌓 |
#### Folders and Test cases #### Folders and Test cases
| Action | Manager/Owner | Developer | Reporter | Not member | | Action | Owner | Owner | Developer | Reporter | Not member |
| ------ | ------------- | --------- | -------- | ---------- | | ------ | ----- | ----- | --------- | -------- | ---------- |
| Create | ✅ | ✅ | ❌ | ❌ | | Write | ✅ | ✅ | ✅ | ❌ | ❌ |
| Delete | ✅ | ✅ | ❌ | ❌ | | Read | ✅ | ✅ | ✅ | ✅ | 🌓 |
| Update | ✅ | ✅ | ❌ | ❌ |
| Read | ✅ | ✅ | ✅ | 🌓 |
#### Test runs #### Test runs
| Action | Manager/Owner | Developer | Reporter | Not member | | Action | Owner | Manager | Developer | Reporter | Not member |
| ------ | ------------- | --------- | -------- | ---------- | | ------ | ----- | ------- | --------- | -------- | ---------- |
| Create | ✅ | ✅ | ✅ | ❌ | | Write | ✅ | ✅ | ✅ | ✅ | ❌ |
| Delete | ✅ | ✅ | ✅ | | | Read | ✅ | ✅ | ✅ | ✅ | 🌓 |
| Update | ✅ | ✅ | ✅ | ❌ |
| Read | ✅ | ✅ | ✅ | 🌓 |
1. "Owner" and "Not member" are not role. "Owner" is the user who created project. [^1]: "Owner" is not role. "Owner" is the user who created project.
"Not member" means a user who is not a project member [^2]: "Not member" is not role. "Not member" means a user who is not a project member
1. 🌓 means that read permission is only allowed if the project is set to public. [^3]: 🌓 means that read permission is only allowed if the project is set to public.

View File

@@ -1,4 +1,3 @@
import { title, subtitle } from '@/components/primitives';
import { Card, CardHeader, CardBody, Avatar } from '@nextui-org/react'; import { Card, CardHeader, CardBody, Avatar } from '@nextui-org/react';
import { Scale, Folder, Check, Globe } from 'lucide-react'; import { Scale, Folder, Check, Globe } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';

View File

@@ -1,5 +1,5 @@
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Image, Divider } from '@nextui-org/react'; import { Divider } from '@nextui-org/react';
import { title, subtitle } from '@/components/primitives'; import { title, subtitle } from '@/components/primitives';
import PaneMainTitle from './PaneMainTitle'; import PaneMainTitle from './PaneMainTitle';
import PaneMainFeatures from './PaneMainFeatures'; import PaneMainFeatures from './PaneMainFeatures';

View File

@@ -92,7 +92,7 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {
startContent={<Trash size={16} />} startContent={<Trash size={16} />}
size="sm" size="sm"
color="danger" color="danger"
isDisabled={!context.isProjectManager(Number(projectId))} isDisabled={!context.isProjectOwner(Number(projectId))}
onClick={() => setIsDeleteConfirmDialogOpen(true)} onClick={() => setIsDeleteConfirmDialogOpen(true)}
> >
{messages.deleteProject} {messages.deleteProject}
@@ -101,7 +101,7 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {
startContent={<Pencil size={16} />} startContent={<Pencil size={16} />}
size="sm" size="sm"
color="primary" color="primary"
isDisabled={!context.isProjectManager(Number(projectId))} isDisabled={!context.isProjectOwner(Number(projectId))}
onClick={() => setIsProjectDialogOpen(true)} onClick={() => setIsProjectDialogOpen(true)}
className="ms-2" className="ms-2"
> >

View File

@@ -18,16 +18,17 @@ export type TokenProps = {
export type TokenType = { export type TokenType = {
access_token: string; access_token: string;
expires_at: number; expires_at: number;
user: UserType; user: UserType | null;
}; };
export type TokenContextType = { export type TokenContextType = {
token: { token: {
access_token: string; access_token: string;
user: UserType; user: UserType | null;
}; };
isSignedIn: () => boolean; isSignedIn: () => boolean;
isAdmin: () => boolean; isAdmin: () => boolean;
isProjectOwner: (projectId: number) => boolean;
isProjectManager: (projectId: number) => boolean; isProjectManager: (projectId: number) => boolean;
isProjectDeveloper: (projectId: number) => boolean; isProjectDeveloper: (projectId: number) => boolean;
isProjectReporter: (projectId: number) => boolean; isProjectReporter: (projectId: number) => boolean;

View File

@@ -6,6 +6,7 @@ import { useRouter, usePathname } from '@/src/navigation';
import { import {
isSignedIn as tokenIsSinedIn, isSignedIn as tokenIsSinedIn,
isAdmin as tokenIsAdmin, isAdmin as tokenIsAdmin,
isProjectOnwer as tokenIsProjectOnwer,
isProjectManager as tokenIsProjectManager, isProjectManager as tokenIsProjectManager,
isProjectDeveloper as tokenIsProjectDeveloper, isProjectDeveloper as tokenIsProjectDeveloper,
isProjectReporter as tokenIsProjectReporter, isProjectReporter as tokenIsProjectReporter,
@@ -30,12 +31,19 @@ const defaultContext = {
}, },
isSignedIn: () => false, isSignedIn: () => false,
isAdmin: () => false, isAdmin: () => false,
isProjectOwner: (projectId: number) => {
return false;
},
isProjectManager: (projectId: number) => { isProjectManager: (projectId: number) => {
return false; return false;
}, },
isProjectDeveloper: (projectId: number) => { isProjectDeveloper: (projectId: number) => {
return false; return false;
}, },
isProjectReporter: (projectId: number) => {
return false;
},
refreshProjectRoles: () => {},
setToken: (token: TokenType) => {}, setToken: (token: TokenType) => {},
storeTokenToLocalStorage, storeTokenToLocalStorage,
removeTokenFromLocalStorage, removeTokenFromLocalStorage,
@@ -63,6 +71,10 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
return tokenIsAdmin(token); return tokenIsAdmin(token);
}; };
const isProjectOwner = (projectId: number) => {
return tokenIsProjectOnwer(projectRoles, projectId);
};
const isProjectManager = (projectId: number) => { const isProjectManager = (projectId: number) => {
return tokenIsProjectManager(projectRoles, projectId); return tokenIsProjectManager(projectRoles, projectId);
}; };
@@ -93,6 +105,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
projectRoles, projectRoles,
isSignedIn, isSignedIn,
isAdmin, isAdmin,
isProjectOwner,
isProjectManager, isProjectManager,
isProjectDeveloper, isProjectDeveloper,
isProjectReporter, isProjectReporter,

View File

@@ -30,7 +30,7 @@ function isSignedIn(token: TokenType): boolean {
function isAdmin(token: TokenType) { function isAdmin(token: TokenType) {
if (tokenExists(token) && isTokenValid(token)) { if (tokenExists(token) && isTokenValid(token)) {
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator'); const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
if (token.user.role === adminRoleIndex) { if (token.user && token.user.role === adminRoleIndex) {
return true; return true;
} }
} }
@@ -61,6 +61,26 @@ async function fetchMyRoles(jwt: string) {
} }
} }
function isProjectOnwer(projectRoles: ProjectRoleType[], projectId: number) {
if (!projectRoles) {
return false;
}
const found = projectRoles.find((role) => {
return role.projectId === projectId;
});
if (!found) {
return false;
}
if (found.isOwner === true) {
return true;
}
return false;
}
function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) { function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) {
if (!projectRoles) { if (!projectRoles) {
return false; return false;
@@ -172,6 +192,7 @@ function checkSignInPage(token: TokenType, pathname: string) {
export { export {
isSignedIn, isSignedIn,
isAdmin, isAdmin,
isProjectOnwer,
isProjectManager, isProjectManager,
isProjectDeveloper, isProjectDeveloper,
isProjectReporter, isProjectReporter,