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:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user