From 2b5a806aee09b121b426044455bda2e02266983a Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Sat, 8 Jun 2024 08:52:19 +0900 Subject: [PATCH] feat: inplement auth for runs and runCases --- backend/middleware/verifyEditable.js | 88 +++++++++++++++++++ backend/middleware/verifyVisible.js | 30 +++++++ backend/routes/runcases/bulkDelete.js | 6 +- backend/routes/runcases/bulkNew.js | 6 +- backend/routes/runcases/delete.js | 4 +- backend/routes/runcases/edit.js | 4 +- backend/routes/runcases/index.js | 4 +- backend/routes/runcases/new.js | 4 +- backend/routes/runs/delete.js | 4 +- backend/routes/runs/edit.js | 4 +- backend/routes/runs/index.js | 4 +- backend/routes/runs/new.js | 7 +- backend/routes/runs/show.js | 4 +- .../folders/[folderId]/cases/CasesPane.tsx | 4 +- .../[folderId]/cases/[caseId]/CaseEditor.tsx | 2 +- .../projects/[projectId]/runs/RunsPage.tsx | 51 +++++++---- .../projects/[projectId]/runs/RunsTable.tsx | 5 +- .../[projectId]/runs/[runId]/RunEditor.tsx | 40 +++++---- .../runs/[runId]/RunPregressDonutChart.tsx | 2 +- .../projects/[projectId]/runs/runsControl.ts | 40 +++++---- frontend/types/user.ts | 1 + frontend/utils/TokenProvider.tsx | 6 ++ .../[folderId]/cases => utils}/caseControl.ts | 2 +- frontend/utils/token.ts | 38 +++++++- 24 files changed, 293 insertions(+), 67 deletions(-) rename frontend/{src/app/[locale]/projects/[projectId]/folders/[folderId]/cases => utils}/caseControl.ts (98%) diff --git a/backend/middleware/verifyEditable.js b/backend/middleware/verifyEditable.js index ec7c8fc..6a96493 100644 --- a/backend/middleware/verifyEditable.js +++ b/backend/middleware/verifyEditable.js @@ -4,6 +4,7 @@ const defineMember = require('../models/members'); const defineProject = require('../models/projects'); const defineFolder = require('../models/folders'); const defineCase = require('../models/cases'); +const defineRun = require('../models/runs'); function verifyEditableMiddleware(sequelize) { /** @@ -195,12 +196,99 @@ function verifyEditableMiddleware(sequelize) { return false; } + /** + * Verify user is reporter of the project by projectId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectReporterFromProjectId(req, res, next) { + const projectId = req.params.projectId || req.query.projectId; + if (!projectId) { + return res.status(400).json({ error: 'projectId is required' }); + } + + const isReporterRet = await isReporter(projectId, req.userId); + if (isReporterRet) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + /** + * Verify user is reporter of the project by runId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectReporterFromRunId(req, res, next) { + const Run = defineRun(sequelize, DataTypes); + + const runId = req.params.runId || req.query.runId; + if (!runId) { + return res.status(400).json({ error: 'runId is required' }); + } + + // find project id from runId + const run = await Run.findByPk(runId); + const projectId = run && run.id; + if (!projectId) { + return res.status(404).send('failed to find projectId'); + } + + const isReporterRet = await isReporter(projectId, req.userId); + if (isReporterRet) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + async function isReporter(projectId, userId) { + const Project = defineProject(sequelize, DataTypes); + const Member = defineMember(sequelize, DataTypes); + Project.hasMany(Member, { foreignKey: 'projectId' }); + Member.belongsTo(Project, { foreignKey: 'projectId' }); + + const project = await Project.findOne({ + where: { id: projectId }, + include: [ + { + model: Member, + where: { userId: userId }, + required: false, + }, + ], + }); + if (!project) { + return false; + } + + // owner has reporter or higher authority + if (project.userId === userId) { + return true; + } + + 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'); + const reporterRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'reporter'); + if (member.role === managerRoleIndex || member.role === developerRoleIndex || member.role === reporterRoleIndex) { + return true; + } + } + + return false; + } + return { verifyProjectOwner, verifyProjectManagerFromProjectId, verifyProjectDeveloperFromProjectId, verifyProjectDeveloperFromFolderId, verifyProjectDeveloperFromCaseId, + verifyProjectReporterFromProjectId, + verifyProjectReporterFromRunId, }; } diff --git a/backend/middleware/verifyVisible.js b/backend/middleware/verifyVisible.js index f138053..5f6645e 100644 --- a/backend/middleware/verifyVisible.js +++ b/backend/middleware/verifyVisible.js @@ -3,6 +3,7 @@ const defineMember = require('../models/members'); const defineProject = require('../models/projects'); const defineFolder = require('../models/folders'); const defineCase = require('../models/cases'); +const defineRun = require('../models/runs'); function verifyVisibleMiddleware(sequelize) { /** @@ -88,6 +89,34 @@ function verifyVisibleMiddleware(sequelize) { return res.status(403).json({ error: 'Forbidden' }); } + /** + * Verify user can read project by runId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectVisibleFromRunId(req, res, next) { + const Run = defineRun(sequelize, DataTypes); + + const runId = req.params.runId || req.query.runId; + if (!runId) { + return res.status(400).json({ error: 'runId is required' }); + } + + // find project id from runId + const run = await Run.findByPk(runId); + const projectId = run && run.id; + if (!projectId) { + return res.status(404).send('failed to find projectId'); + } + + const isVisble = await isVisible(projectId, req.userId); + if (isVisble) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + async function isVisible(projectId, userId) { const Project = defineProject(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes); @@ -128,6 +157,7 @@ function verifyVisibleMiddleware(sequelize) { verifyProjectVisibleFromProjectId, verifyProjectVisibleFromFolderId, verifyProjectVisibleFromCaseId, + verifyProjectVisibleFromRunId, }; } diff --git a/backend/routes/runcases/bulkDelete.js b/backend/routes/runcases/bulkDelete.js index 60d9711..62bad38 100644 --- a/backend/routes/runcases/bulkDelete.js +++ b/backend/routes/runcases/bulkDelete.js @@ -5,10 +5,14 @@ const { DataTypes, Op } = require('sequelize'); module.exports = function (sequelize) { const RunCase = defineRunCase(sequelize, DataTypes); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - router.post('/bulkdelete', async (req, res) => { + router.post('/bulkdelete', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const recordsToDelete = req.body; + // TODO Instead of receiving a combination of runId and caseId from frontend, + // receives only an array of caseId and constructs a pair from the query parameter runId try { const existingRunCases = await RunCase.findAll({ where: { diff --git a/backend/routes/runcases/bulkNew.js b/backend/routes/runcases/bulkNew.js index 1bf449b..e32b308 100644 --- a/backend/routes/runcases/bulkNew.js +++ b/backend/routes/runcases/bulkNew.js @@ -4,11 +4,15 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes, Op } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.post('/bulknew', async (req, res) => { + router.post('/bulknew', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const recordsToCreate = req.body; + // TODO Instead of receiving a combination of runId and caseId from frontend, + // receives only an array of caseId and constructs a pair from the query parameter runId try { const existingRunCases = await RunCase.findAll({ where: { diff --git a/backend/routes/runcases/delete.js b/backend/routes/runcases/delete.js index 0a13e0f..c7029c8 100644 --- a/backend/routes/runcases/delete.js +++ b/backend/routes/runcases/delete.js @@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.delete('/', async (req, res) => { + router.delete('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.query.runId; const caseId = req.query.caseId; diff --git a/backend/routes/runcases/edit.js b/backend/routes/runcases/edit.js index abc8b85..5f14ca6 100644 --- a/backend/routes/runcases/edit.js +++ b/backend/routes/runcases/edit.js @@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.put('/', async (req, res) => { + router.put('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.query.runId; const caseId = req.query.caseId; const status = req.query.status; diff --git a/backend/routes/runcases/index.js b/backend/routes/runcases/index.js index 39a240c..ba1dacc 100644 --- a/backend/routes/runcases/index.js +++ b/backend/routes/runcases/index.js @@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.get('/', async (req, res) => { + router.get('/', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => { const { runId } = req.query; if (!runId) { diff --git a/backend/routes/runcases/new.js b/backend/routes/runcases/new.js index 4a245fb..161854f 100644 --- a/backend/routes/runcases/new.js +++ b/backend/routes/runcases/new.js @@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.post('/', async (req, res) => { + router.post('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.query.runId; const caseId = req.query.caseId; diff --git a/backend/routes/runs/delete.js b/backend/routes/runs/delete.js index c7efc1b..3b32069 100644 --- a/backend/routes/runs/delete.js +++ b/backend/routes/runs/delete.js @@ -4,9 +4,11 @@ const defineRun = require('../../models/runs'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const Run = defineRun(sequelize, DataTypes); - router.delete('/:runId', async (req, res) => { + router.delete('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.params.runId; try { const testrun = await Run.findByPk(runId); diff --git a/backend/routes/runs/edit.js b/backend/routes/runs/edit.js index 037c00f..e921c3b 100644 --- a/backend/routes/runs/edit.js +++ b/backend/routes/runs/edit.js @@ -5,8 +5,10 @@ const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { const Run = defineRun(sequelize, DataTypes); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - router.put('/:runId', async (req, res) => { + router.put('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.params.runId; const updateRun = req.body; try { diff --git a/backend/routes/runs/index.js b/backend/routes/runs/index.js index 6b400bc..c77959c 100644 --- a/backend/routes/runs/index.js +++ b/backend/routes/runs/index.js @@ -4,9 +4,11 @@ const defineRun = require('../../models/runs'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const Run = defineRun(sequelize, DataTypes); - router.get('/', async (req, res) => { + router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => { const { projectId } = req.query; if (!projectId) { diff --git a/backend/routes/runs/new.js b/backend/routes/runs/new.js index f0aaed5..7c6655c 100644 --- a/backend/routes/runs/new.js +++ b/backend/routes/runs/new.js @@ -4,11 +4,14 @@ const defineRun = require('../../models/runs'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectReporterFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Run = defineRun(sequelize, DataTypes); - router.post('/', async (req, res) => { + router.post('/', verifySignedIn, verifyProjectReporterFromProjectId, async (req, res) => { try { - const { name, configurations, description, state, projectId } = req.body; + const projectId = req.query.projectId; + const { name, configurations, description, state } = req.body; if (!name || !projectId) { return res.status(400).json({ error: 'Name and projectId are required' }); } diff --git a/backend/routes/runs/show.js b/backend/routes/runs/show.js index c599f6b..2bf415c 100644 --- a/backend/routes/runs/show.js +++ b/backend/routes/runs/show.js @@ -5,10 +5,12 @@ const defineRunCase = require('../../models/runCases'); const { DataTypes, literal } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize); const Run = defineRun(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes); - router.get('/:runId', async (req, res) => { + router.get('/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => { const runId = req.params.runId; if (!runId) { diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx index 1f64644..bb8bf12 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useContext } from 'react'; import { TokenContext } from '@/utils/TokenProvider'; import TestCaseTable from './TestCaseTable'; -import { fetchCases, createCase, deleteCases } from './caseControl'; +import { fetchCases, createCase, deleteCases } from '@/utils/caseControl'; import { CaseType, CasesMessages } from '@/types/case'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import CaseDialog from './CaseDialog'; @@ -25,7 +25,7 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro return; } try { - const data = await fetchCases(context.token.access_token, folderId); + const data = await fetchCases(context.token.access_token, Number(folderId)); setCases(data); } catch (error: any) { console.error('Error in effect:', error.message); diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseEditor.tsx index 705270c..589abb3 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseEditor.tsx @@ -7,7 +7,7 @@ import { priorities, testTypes, templates } from '@/config/selection'; import CaseStepsEditor from './CaseStepsEditor'; import CaseAttachmentsEditor from './CaseAttachmentsEditor'; import { CaseType, AttachmentType, CaseMessages } from '@/types/case'; -import { fetchCase, updateCase } from '../caseControl'; +import { fetchCase, updateCase } from '../../../../../../../../../utils/caseControl'; import { fetchCreateStep, fetchDeleteStep } from './stepControl'; import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl'; import { TokenContext } from '@/utils/TokenProvider'; diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/RunsPage.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/RunsPage.tsx index e252c21..93a92e4 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/RunsPage.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/RunsPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useContext } from 'react'; import { Button } from '@nextui-org/react'; import { Plus } from 'lucide-react'; import RunsTable from './RunsTable'; @@ -7,6 +7,7 @@ import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl'; import { RunType, RunsMessages } from '@/types/run'; import RunDialog from './RunDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; +import { TokenContext } from '@/utils/TokenProvider'; type Props = { projectId: string; @@ -15,18 +16,19 @@ type Props = { }; const defaultRun = { - id: null, + id: 0, name: 'Untitled Run', - configurations: null, - description: null, - state: null, - projectId: null, - createdAt: null, - updatedAt: null, + configurations: 0, + description: '', + state: 0, + projectId: 0, + createdAt: '', + updatedAt: '', }; export default function RunsPage({ projectId, locale, messages }: Props) { - const [runs, setRuns] = useState([]); + const context = useContext(TokenContext); + const [runs, setRuns] = useState([]); // run dialog const [isRunDialogOpen, setIsRunDialogOpen] = useState(false); @@ -51,8 +53,12 @@ export default function RunsPage({ projectId, locale, messages }: Props) { useEffect(() => { async function fetchDataEffect() { + if (!context.isSignedIn()) { + return; + } + try { - const data = await fetchRuns(projectId); + const data = await fetchRuns(context.token.access_token, Number(projectId)); setRuns(data); } catch (error: any) { console.error('Error in effect:', error.message); @@ -60,15 +66,15 @@ export default function RunsPage({ projectId, locale, messages }: Props) { } fetchDataEffect(); - }, []); + }, [context]); const onSubmit = async (name: string, description: string) => { if (editingRun && editingRun.createdAt) { - const updatedRun = await updateRun(editingRun); + const updatedRun: RunType = await updateRun(context.token.access_token, editingRun); const updatedRuns = runs.map((run) => (run.id === updatedRun.id ? updatedRun : run)); setRuns(updatedRuns); } else { - const newRun = await createRun(projectId, name, description); + const newRun = await createRun(context.token.access_token, Number(projectId), name, description); setRuns([...runs, newRun]); } closeDialog(); @@ -81,7 +87,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) { const onConfirm = async () => { if (deleteRunId) { - await deleteRun(deleteRunId); + await deleteRun(context.token.access_token, deleteRunId); setRuns(runs.filter((run) => run.id !== deleteRunId)); closeDeleteConfirmDialog(); } @@ -92,13 +98,26 @@ export default function RunsPage({ projectId, locale, messages }: Props) {

{messages.runList}

-
- + void; messages: RunsMessages; locale: string; }; -export default function RunsTable({ projectId, runs, onDeleteRun, messages, locale }: Props) { +export default function RunsTable({ projectId, isDisabled, runs, onDeleteRun, messages, locale }: Props) { const headerColumns = [ { name: messages.id, uid: 'id', sortable: true }, { name: messages.name, uid: 'name', sortable: true }, @@ -83,7 +84,7 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca - onDeleteRun(run.id)}> + onDeleteRun(run.id)}> {messages.deleteRun} diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx index 28d8fde..d4967e9 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx @@ -1,6 +1,6 @@ 'use client'; import React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useContext } from 'react'; import { useRouter } from '@/src/navigation'; import { Button, @@ -36,7 +36,8 @@ import { bulkDeleteRunCases, } from '../runsControl'; import { fetchFolders } from '../../folders/foldersControl'; -import { fetchCases } from '../../folders/[folderId]/cases/caseControl'; +import { fetchCases } from '@/utils/caseControl'; +import { TokenContext } from '@/utils/TokenProvider'; import { useTheme } from 'next-themes'; const defaultTestRun = { @@ -46,6 +47,8 @@ const defaultTestRun = { description: '', state: 0, projectId: 0, + createdAt: '', + updatedAt: '', }; type Props = { @@ -56,29 +59,34 @@ type Props = { }; export default function RunEditor({ projectId, runId, messages, locale }: Props) { + const context = useContext(TokenContext); const { theme, setTheme } = useTheme(); const [testRun, setTestRun] = useState(defaultTestRun); - const [folders, setFolders] = useState([]); + const [folders, setFolders] = useState([]); const [runCases, setRunCases] = useState([]); const [runStatusCounts, setRunStatusCounts] = useState(); const [selectedKeys, setSelectedKeys] = useState(new Set([])); - const [selectedFolder, setSelectedFolder] = useState({}); + const [selectedFolder, setSelectedFolder] = useState(null); const [testcases, setTestCases] = useState([]); const [isNameInvalid, setIsNameInvalid] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const router = useRouter(); const fetchRunAndStatusCount = async () => { - const { run, statusCounts } = await fetchRun(runId); + const { run, statusCounts } = await fetchRun(context.token.access_token, Number(runId)); setTestRun(run); setRunStatusCounts(statusCounts); }; useEffect(() => { async function fetchDataEffect() { + if (!context.isSignedIn()) { + return; + } + try { await fetchRunAndStatusCount(); - const foldersData = await fetchFolders(projectId); + const foldersData = await fetchFolders(context.token.access_token, projectId); setFolders(foldersData); setSelectedFolder(foldersData[0]); } catch (error: any) { @@ -87,16 +95,16 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) } fetchDataEffect(); - }, []); + }, [context]); useEffect(() => { async function fetchCasesData() { if (selectedFolder && selectedFolder.id) { try { - const latestRunCases = await fetchRunCases(runId); + const latestRunCases: RunCaseType[] = await fetchRunCases(context.token.access_token, Number(runId)); setRunCases(latestRunCases); - const testCasesData = await fetchCases(selectedFolder.id); + const testCasesData: CaseType[] = await fetchCases(context.token.access_token, selectedFolder.id); // Check if each testCase has an association with testRun // and add "isIncluded" property const updatedTestCasesData = testCasesData.map((testCase) => { @@ -122,7 +130,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) }, [selectedFolder]); const handleChangeStatus = async (changeCaseId: number, status: number) => { - await updateRunCase(runId, changeCaseId, status); + await updateRunCase(context.token.access_token, Number(runId), changeCaseId, status); setTestCases((prevTestCases) => { return prevTestCases.map((testCase) => { if (testCase.id === changeCaseId) { @@ -135,12 +143,12 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => { if (isInclude) { - const createdRunCase = await createRunCase(runId, clickedTestCaseId); + const createdRunCase = await createRunCase(context.token.access_token, Number(runId), clickedTestCaseId); setRunCases((prevRunCases) => { return [...prevRunCases, createdRunCase]; }); } else { - await deleteRunCase(runId, clickedTestCaseId); + await deleteRunCase(context.token.access_token, Number(runId), clickedTestCaseId); setRunCases((prevRunCases) => { return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId); }); @@ -165,14 +173,14 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) } const runCaseInfo: RunCaseInfoType[] = keys.map((caseId) => ({ - runId: runId, + runId: Number(runId), caseId: caseId, })); if (isInclude) { - const createdRunCases = await bulkCreateRunCases(runCaseInfo); + const createdRunCases = await bulkCreateRunCases(context.token.access_token, Number(runId), runCaseInfo); setRunCases((prevRunCases) => [...prevRunCases, ...createdRunCases]); } else { - await bulkDeleteRunCases(runCaseInfo); + await bulkDeleteRunCases(context.token.access_token, Number(runId), runCaseInfo); setRunCases((prevRunCases) => { return prevRunCases.filter((runCase) => { return !runCaseInfo.some((info) => info.caseId === runCase.caseId); @@ -217,7 +225,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) isLoading={isUpdating} onPress={async () => { setIsUpdating(true); - await updateRun(testRun); + await updateRun(context.token.access_token, testRun); setIsUpdating(false); }} > diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunPregressDonutChart.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunPregressDonutChart.tsx index 3b0e143..c492a45 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunPregressDonutChart.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunPregressDonutChart.tsx @@ -8,7 +8,7 @@ const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); type Props = { statusCounts: RunStatusCountType[]; messages: RunMessages; - theme: string; + theme: string | undefined; }; export default function RunProgressDounut({ statusCounts, messages, theme }: Props) { diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts index afa264e..811f5f4 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts @@ -2,7 +2,7 @@ import Config from '@/config/config'; const apiServer = Config.apiServer; import { RunType, RunCaseInfoType } from '@/types/run'; -async function fetchRun(runId: string) { +async function fetchRun(jwt: string, runId: number) { const url = `${apiServer}/runs/${runId}`; try { @@ -10,6 +10,7 @@ async function fetchRun(runId: string) { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }); @@ -24,7 +25,7 @@ async function fetchRun(runId: string) { } } -async function fetchRuns(projectId: string) { +async function fetchRuns(jwt: string, projectId: number) { const url = `${apiServer}/runs?projectId=${projectId}`; try { @@ -32,6 +33,7 @@ async function fetchRuns(projectId: string) { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }); @@ -46,24 +48,24 @@ async function fetchRuns(projectId: string) { } } -async function createRun(projectId: string, name: string, description: string) { +async function createRun(jwt: string, projectId: number, name: string, description: string) { const newTestRun = { name, configurations: 0, description, state: 0, - projectId: projectId, }; const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(newTestRun), }; - const url = `${apiServer}/runs`; + const url = `${apiServer}/runs?projectId=${projectId}`; try { const response = await fetch(url, fetchOptions); @@ -78,11 +80,12 @@ async function createRun(projectId: string, name: string, description: string) { } } -async function updateRun(updateTestRun: RunType) { +async function updateRun(jwt: string, updateTestRun: RunType) { const fetchOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(updateTestRun), }; @@ -102,11 +105,12 @@ async function updateRun(updateTestRun: RunType) { } } -async function deleteRun(runId: number) { +async function deleteRun(jwt: string, runId: number) { const fetchOptions = { method: 'DELETE', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }; @@ -123,7 +127,7 @@ async function deleteRun(runId: number) { } } -async function fetchRunCases(runId: string) { +async function fetchRunCases(jwt: string, runId: number) { const url = `${apiServer}/runcases?runId=${runId}`; try { @@ -131,6 +135,7 @@ async function fetchRunCases(runId: string) { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }); @@ -145,11 +150,12 @@ async function fetchRunCases(runId: string) { } } -async function createRunCase(runId: string, caseId: number) { +async function createRunCase(jwt: string, runId: number, caseId: number) { const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }; @@ -168,11 +174,12 @@ async function createRunCase(runId: string, caseId: number) { } } -async function updateRunCase(runId: string, caseId: number, status: number) { +async function updateRunCase(jwt: string, runId: number, caseId: number, status: number) { const fetchOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }; @@ -191,16 +198,17 @@ async function updateRunCase(runId: string, caseId: number, status: number) { } } -async function bulkCreateRunCases(runCaseInfo: RunCaseInfoType[]) { +async function bulkCreateRunCases(jwt: string, runId: number, runCaseInfo: RunCaseInfoType[]) { const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(runCaseInfo), }; - const url = `${apiServer}/runcases/bulknew`; + const url = `${apiServer}/runcases/bulknew?runId=${runId}`; try { const response = await fetch(url, fetchOptions); @@ -215,11 +223,12 @@ async function bulkCreateRunCases(runCaseInfo: RunCaseInfoType[]) { } } -async function deleteRunCase(runId: string, caseId: number) { +async function deleteRunCase(jwt: string, runId: number, caseId: number) { const fetchOptions = { method: 'DELETE', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }; @@ -236,16 +245,17 @@ async function deleteRunCase(runId: string, caseId: number) { } } -async function bulkDeleteRunCases(runCaseInfo: RunCaseInfoType[]) { +async function bulkDeleteRunCases(jwt: string, runId: number, runCaseInfo: RunCaseInfoType[]) { const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(runCaseInfo), }; - const url = `${apiServer}/runcases/bulkdelete`; + const url = `${apiServer}/runcases/bulkdelete?runId=${runId}`; try { const response = await fetch(url, fetchOptions); diff --git a/frontend/types/user.ts b/frontend/types/user.ts index 2c85d36..387cf1a 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -30,6 +30,7 @@ export type TokenContextType = { isAdmin: () => boolean; isProjectManager: (projectId: number) => boolean; isProjectDeveloper: (projectId: number) => boolean; + isProjectReporter: (projectId: number) => boolean; setToken: (token: TokenType) => void; storeTokenToLocalStorage: (token: TokenType) => void; removeTokenFromLocalStorage: () => void; diff --git a/frontend/utils/TokenProvider.tsx b/frontend/utils/TokenProvider.tsx index b641035..f586ace 100644 --- a/frontend/utils/TokenProvider.tsx +++ b/frontend/utils/TokenProvider.tsx @@ -8,6 +8,7 @@ import { isAdmin as tokenIsAdmin, isProjectManager as tokenIsProjectManager, isProjectDeveloper as tokenIsProjectDeveloper, + isProjectReporter as tokenIsProjectReporter, checkSignInPage as tokenCheckSignInPage, fetchMyRoles, } from './token'; @@ -70,6 +71,10 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { return tokenIsProjectDeveloper(projectRoles, projectId); }; + const isProjectReporter = (projectId: number) => { + return tokenIsProjectReporter(projectRoles, projectId); + }; + const tokenContext = { token, projectRoles, @@ -77,6 +82,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { isAdmin, isProjectManager, isProjectDeveloper, + isProjectReporter, setToken, storeTokenToLocalStorage, removeTokenFromLocalStorage, diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts b/frontend/utils/caseControl.ts similarity index 98% rename from frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts rename to frontend/utils/caseControl.ts index aec9e18..0177518 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts +++ b/frontend/utils/caseControl.ts @@ -25,7 +25,7 @@ async function fetchCase(jwt: string, caseId: number) { } } -async function fetchCases(jwt: string, folderId: string) { +async function fetchCases(jwt: string, folderId: number) { const url = `${apiServer}/cases?folderId=${folderId}`; try { diff --git a/frontend/utils/token.ts b/frontend/utils/token.ts index 5b2ee99..f49ab59 100644 --- a/frontend/utils/token.ts +++ b/frontend/utils/token.ts @@ -112,6 +112,33 @@ function isProjectDeveloper(projectRoles: ProjectRoleType[], projectId: number) return false; } +function isProjectReporter(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; + } + + const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); + const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer'); + const reporterRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'reporter'); + if (found.role === managerRoleIndex || found.role === developerRoleIndex || found.role === reporterRoleIndex) { + return true; + } + + return false; +} + // private paths are '/account', '/admin', '/projects/*' const isPrivatePath = (pathname: string) => { return /^\/account(\/)?$/.test(pathname) || /^\/admin(\/.*)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname); @@ -142,4 +169,13 @@ function checkSignInPage(token: TokenType, pathname: string) { return ret; } -export { isSignedIn, isAdmin, isProjectManager, isProjectDeveloper, isPrivatePath, checkSignInPage, fetchMyRoles }; +export { + isSignedIn, + isAdmin, + isProjectManager, + isProjectDeveloper, + isProjectReporter, + isPrivatePath, + checkSignInPage, + fetchMyRoles, +};