feat: inplement auth for runs and runCases

This commit is contained in:
Takeshi Kimata
2024-06-08 08:52:19 +09:00
parent 07259f6daf
commit 2b5a806aee
24 changed files with 293 additions and 67 deletions

View File

@@ -4,6 +4,7 @@ const defineMember = require('../models/members');
const defineProject = require('../models/projects'); const defineProject = require('../models/projects');
const defineFolder = require('../models/folders'); const defineFolder = require('../models/folders');
const defineCase = require('../models/cases'); const defineCase = require('../models/cases');
const defineRun = require('../models/runs');
function verifyEditableMiddleware(sequelize) { function verifyEditableMiddleware(sequelize) {
/** /**
@@ -195,12 +196,99 @@ function verifyEditableMiddleware(sequelize) {
return false; 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 { return {
verifyProjectOwner, verifyProjectOwner,
verifyProjectManagerFromProjectId, verifyProjectManagerFromProjectId,
verifyProjectDeveloperFromProjectId, verifyProjectDeveloperFromProjectId,
verifyProjectDeveloperFromFolderId, verifyProjectDeveloperFromFolderId,
verifyProjectDeveloperFromCaseId, verifyProjectDeveloperFromCaseId,
verifyProjectReporterFromProjectId,
verifyProjectReporterFromRunId,
}; };
} }

View File

@@ -3,6 +3,7 @@ const defineMember = require('../models/members');
const defineProject = require('../models/projects'); const defineProject = require('../models/projects');
const defineFolder = require('../models/folders'); const defineFolder = require('../models/folders');
const defineCase = require('../models/cases'); const defineCase = require('../models/cases');
const defineRun = require('../models/runs');
function verifyVisibleMiddleware(sequelize) { function verifyVisibleMiddleware(sequelize) {
/** /**
@@ -88,6 +89,34 @@ function verifyVisibleMiddleware(sequelize) {
return res.status(403).json({ error: 'Forbidden' }); 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) { async function isVisible(projectId, userId) {
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
@@ -128,6 +157,7 @@ function verifyVisibleMiddleware(sequelize) {
verifyProjectVisibleFromProjectId, verifyProjectVisibleFromProjectId,
verifyProjectVisibleFromFolderId, verifyProjectVisibleFromFolderId,
verifyProjectVisibleFromCaseId, verifyProjectVisibleFromCaseId,
verifyProjectVisibleFromRunId,
}; };
} }

View File

@@ -5,10 +5,14 @@ const { DataTypes, Op } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const RunCase = defineRunCase(sequelize, DataTypes); 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; 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 { try {
const existingRunCases = await RunCase.findAll({ const existingRunCases = await RunCase.findAll({
where: { where: {

View File

@@ -4,11 +4,15 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes, Op } = require('sequelize'); const { DataTypes, Op } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.post('/bulknew', async (req, res) => { router.post('/bulknew', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const recordsToCreate = req.body; 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 { try {
const existingRunCases = await RunCase.findAll({ const existingRunCases = await RunCase.findAll({
where: { where: {

View File

@@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.delete('/', async (req, res) => { router.delete('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const runId = req.query.runId; const runId = req.query.runId;
const caseId = req.query.caseId; const caseId = req.query.caseId;

View File

@@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.put('/', async (req, res) => { router.put('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const runId = req.query.runId; const runId = req.query.runId;
const caseId = req.query.caseId; const caseId = req.query.caseId;
const status = req.query.status; const status = req.query.status;

View File

@@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.get('/', async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => {
const { runId } = req.query; const { runId } = req.query;
if (!runId) { if (!runId) {

View File

@@ -4,9 +4,11 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.post('/', async (req, res) => { router.post('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const runId = req.query.runId; const runId = req.query.runId;
const caseId = req.query.caseId; const caseId = req.query.caseId;

View File

@@ -4,9 +4,11 @@ const defineRun = require('../../models/runs');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.delete('/:runId', async (req, res) => { router.delete('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const runId = req.params.runId; const runId = req.params.runId;
try { try {
const testrun = await Run.findByPk(runId); const testrun = await Run.findByPk(runId);

View File

@@ -5,8 +5,10 @@ const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const Run = defineRun(sequelize, DataTypes); 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 runId = req.params.runId;
const updateRun = req.body; const updateRun = req.body;
try { try {

View File

@@ -4,9 +4,11 @@ const defineRun = require('../../models/runs');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.get('/', async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => {
const { projectId } = req.query; const { projectId } = req.query;
if (!projectId) { if (!projectId) {

View File

@@ -4,11 +4,14 @@ const defineRun = require('../../models/runs');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectReporterFromProjectId } = require('../../middleware/verifyEditable')(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.post('/', async (req, res) => { router.post('/', verifySignedIn, verifyProjectReporterFromProjectId, async (req, res) => {
try { try {
const { name, configurations, description, state, projectId } = req.body; const projectId = req.query.projectId;
const { name, configurations, description, state } = req.body;
if (!name || !projectId) { if (!name || !projectId) {
return res.status(400).json({ error: 'Name and projectId are required' }); return res.status(400).json({ error: 'Name and projectId are required' });
} }

View File

@@ -5,10 +5,12 @@ const defineRunCase = require('../../models/runCases');
const { DataTypes, literal } = require('sequelize'); const { DataTypes, literal } = require('sequelize');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
const RunCase = defineRunCase(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; const runId = req.params.runId;
if (!runId) { if (!runId) {

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useContext } from 'react'; import { useState, useEffect, useContext } from 'react';
import { TokenContext } from '@/utils/TokenProvider'; import { TokenContext } from '@/utils/TokenProvider';
import TestCaseTable from './TestCaseTable'; import TestCaseTable from './TestCaseTable';
import { fetchCases, createCase, deleteCases } from './caseControl'; import { fetchCases, createCase, deleteCases } from '@/utils/caseControl';
import { CaseType, CasesMessages } from '@/types/case'; import { CaseType, CasesMessages } from '@/types/case';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
import CaseDialog from './CaseDialog'; import CaseDialog from './CaseDialog';
@@ -25,7 +25,7 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro
return; return;
} }
try { try {
const data = await fetchCases(context.token.access_token, folderId); const data = await fetchCases(context.token.access_token, Number(folderId));
setCases(data); setCases(data);
} catch (error: any) { } catch (error: any) {
console.error('Error in effect:', error.message); console.error('Error in effect:', error.message);

View File

@@ -7,7 +7,7 @@ import { priorities, testTypes, templates } from '@/config/selection';
import CaseStepsEditor from './CaseStepsEditor'; import CaseStepsEditor from './CaseStepsEditor';
import CaseAttachmentsEditor from './CaseAttachmentsEditor'; import CaseAttachmentsEditor from './CaseAttachmentsEditor';
import { CaseType, AttachmentType, CaseMessages } from '@/types/case'; import { CaseType, AttachmentType, CaseMessages } from '@/types/case';
import { fetchCase, updateCase } from '../caseControl'; import { fetchCase, updateCase } from '../../../../../../../../../utils/caseControl';
import { fetchCreateStep, fetchDeleteStep } from './stepControl'; import { fetchCreateStep, fetchDeleteStep } from './stepControl';
import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl'; import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl';
import { TokenContext } from '@/utils/TokenProvider'; import { TokenContext } from '@/utils/TokenProvider';

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState, useContext } from 'react';
import { Button } from '@nextui-org/react'; import { Button } from '@nextui-org/react';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import RunsTable from './RunsTable'; import RunsTable from './RunsTable';
@@ -7,6 +7,7 @@ import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl';
import { RunType, RunsMessages } from '@/types/run'; import { RunType, RunsMessages } from '@/types/run';
import RunDialog from './RunDialog'; import RunDialog from './RunDialog';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
import { TokenContext } from '@/utils/TokenProvider';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -15,18 +16,19 @@ type Props = {
}; };
const defaultRun = { const defaultRun = {
id: null, id: 0,
name: 'Untitled Run', name: 'Untitled Run',
configurations: null, configurations: 0,
description: null, description: '',
state: null, state: 0,
projectId: null, projectId: 0,
createdAt: null, createdAt: '',
updatedAt: null, updatedAt: '',
}; };
export default function RunsPage({ projectId, locale, messages }: Props) { export default function RunsPage({ projectId, locale, messages }: Props) {
const [runs, setRuns] = useState([]); const context = useContext(TokenContext);
const [runs, setRuns] = useState<RunType[]>([]);
// run dialog // run dialog
const [isRunDialogOpen, setIsRunDialogOpen] = useState(false); const [isRunDialogOpen, setIsRunDialogOpen] = useState(false);
@@ -51,8 +53,12 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
if (!context.isSignedIn()) {
return;
}
try { try {
const data = await fetchRuns(projectId); const data = await fetchRuns(context.token.access_token, Number(projectId));
setRuns(data); setRuns(data);
} catch (error: any) { } catch (error: any) {
console.error('Error in effect:', error.message); console.error('Error in effect:', error.message);
@@ -60,15 +66,15 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
} }
fetchDataEffect(); fetchDataEffect();
}, []); }, [context]);
const onSubmit = async (name: string, description: string) => { const onSubmit = async (name: string, description: string) => {
if (editingRun && editingRun.createdAt) { 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)); const updatedRuns = runs.map((run) => (run.id === updatedRun.id ? updatedRun : run));
setRuns(updatedRuns); setRuns(updatedRuns);
} else { } else {
const newRun = await createRun(projectId, name, description); const newRun = await createRun(context.token.access_token, Number(projectId), name, description);
setRuns([...runs, newRun]); setRuns([...runs, newRun]);
} }
closeDialog(); closeDialog();
@@ -81,7 +87,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
const onConfirm = async () => { const onConfirm = async () => {
if (deleteRunId) { if (deleteRunId) {
await deleteRun(deleteRunId); await deleteRun(context.token.access_token, deleteRunId);
setRuns(runs.filter((run) => run.id !== deleteRunId)); setRuns(runs.filter((run) => run.id !== deleteRunId));
closeDeleteConfirmDialog(); closeDeleteConfirmDialog();
} }
@@ -92,13 +98,26 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
<div className="w-full p-3 flex items-center justify-between"> <div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.runList}</h3> <h3 className="font-bold">{messages.runList}</h3>
<div> <div>
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={openDialogForCreate}> <Button
startContent={<Plus size={16} />}
size="sm"
isDisabled={!context.isProjectReporter(Number(projectId))}
color="primary"
onClick={openDialogForCreate}
>
{messages.newRun} {messages.newRun}
</Button> </Button>
</div> </div>
</div> </div>
<RunsTable projectId={projectId} runs={runs} onDeleteRun={onDeleteClick} messages={messages} locale={locale} /> <RunsTable
projectId={projectId}
isDisabled={!context.isProjectReporter(Number(projectId))}
runs={runs}
onDeleteRun={onDeleteClick}
messages={messages}
locale={locale}
/>
<RunDialog <RunDialog
isOpen={isRunDialogOpen} isOpen={isRunDialogOpen}

View File

@@ -20,13 +20,14 @@ import dayjs from 'dayjs';
type Props = { type Props = {
projectId: string; projectId: string;
isDisabled: boolean;
runs: RunType[]; runs: RunType[];
onDeleteRun: (runId: number) => void; onDeleteRun: (runId: number) => void;
messages: RunsMessages; messages: RunsMessages;
locale: string; 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 = [ const headerColumns = [
{ name: messages.id, uid: 'id', sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.name, uid: 'name', sortable: true }, { name: messages.name, uid: 'name', sortable: true },
@@ -83,7 +84,7 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="run actions"> <DropdownMenu aria-label="run actions">
<DropdownItem className="text-danger" onClick={() => onDeleteRun(run.id)}> <DropdownItem className="text-danger" isDisabled={isDisabled} onClick={() => onDeleteRun(run.id)}>
{messages.deleteRun} {messages.deleteRun}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect, useContext } from 'react';
import { useRouter } from '@/src/navigation'; import { useRouter } from '@/src/navigation';
import { import {
Button, Button,
@@ -36,7 +36,8 @@ import {
bulkDeleteRunCases, bulkDeleteRunCases,
} from '../runsControl'; } from '../runsControl';
import { fetchFolders } from '../../folders/foldersControl'; 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'; import { useTheme } from 'next-themes';
const defaultTestRun = { const defaultTestRun = {
@@ -46,6 +47,8 @@ const defaultTestRun = {
description: '', description: '',
state: 0, state: 0,
projectId: 0, projectId: 0,
createdAt: '',
updatedAt: '',
}; };
type Props = { type Props = {
@@ -56,29 +59,34 @@ type Props = {
}; };
export default function RunEditor({ projectId, runId, messages, locale }: Props) { export default function RunEditor({ projectId, runId, messages, locale }: Props) {
const context = useContext(TokenContext);
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const [testRun, setTestRun] = useState<RunType>(defaultTestRun); const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
const [folders, setFolders] = useState([]); const [folders, setFolders] = useState<FolderType[]>([]);
const [runCases, setRunCases] = useState<RunCaseType[]>([]); const [runCases, setRunCases] = useState<RunCaseType[]>([]);
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>(); const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>();
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([])); const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [selectedFolder, setSelectedFolder] = useState<FolderType>({}); const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
const [testcases, setTestCases] = useState<CaseType[]>([]); const [testcases, setTestCases] = useState<CaseType[]>([]);
const [isNameInvalid, setIsNameInvalid] = useState<boolean>(false); const [isNameInvalid, setIsNameInvalid] = useState<boolean>(false);
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const fetchRunAndStatusCount = async () => { const fetchRunAndStatusCount = async () => {
const { run, statusCounts } = await fetchRun(runId); const { run, statusCounts } = await fetchRun(context.token.access_token, Number(runId));
setTestRun(run); setTestRun(run);
setRunStatusCounts(statusCounts); setRunStatusCounts(statusCounts);
}; };
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
if (!context.isSignedIn()) {
return;
}
try { try {
await fetchRunAndStatusCount(); await fetchRunAndStatusCount();
const foldersData = await fetchFolders(projectId); const foldersData = await fetchFolders(context.token.access_token, projectId);
setFolders(foldersData); setFolders(foldersData);
setSelectedFolder(foldersData[0]); setSelectedFolder(foldersData[0]);
} catch (error: any) { } catch (error: any) {
@@ -87,16 +95,16 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props)
} }
fetchDataEffect(); fetchDataEffect();
}, []); }, [context]);
useEffect(() => { useEffect(() => {
async function fetchCasesData() { async function fetchCasesData() {
if (selectedFolder && selectedFolder.id) { if (selectedFolder && selectedFolder.id) {
try { try {
const latestRunCases = await fetchRunCases(runId); const latestRunCases: RunCaseType[] = await fetchRunCases(context.token.access_token, Number(runId));
setRunCases(latestRunCases); 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 // Check if each testCase has an association with testRun
// and add "isIncluded" property // and add "isIncluded" property
const updatedTestCasesData = testCasesData.map((testCase) => { const updatedTestCasesData = testCasesData.map((testCase) => {
@@ -122,7 +130,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props)
}, [selectedFolder]); }, [selectedFolder]);
const handleChangeStatus = async (changeCaseId: number, status: number) => { const handleChangeStatus = async (changeCaseId: number, status: number) => {
await updateRunCase(runId, changeCaseId, status); await updateRunCase(context.token.access_token, Number(runId), changeCaseId, status);
setTestCases((prevTestCases) => { setTestCases((prevTestCases) => {
return prevTestCases.map((testCase) => { return prevTestCases.map((testCase) => {
if (testCase.id === changeCaseId) { if (testCase.id === changeCaseId) {
@@ -135,12 +143,12 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props)
const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => { const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => {
if (isInclude) { if (isInclude) {
const createdRunCase = await createRunCase(runId, clickedTestCaseId); const createdRunCase = await createRunCase(context.token.access_token, Number(runId), clickedTestCaseId);
setRunCases((prevRunCases) => { setRunCases((prevRunCases) => {
return [...prevRunCases, createdRunCase]; return [...prevRunCases, createdRunCase];
}); });
} else { } else {
await deleteRunCase(runId, clickedTestCaseId); await deleteRunCase(context.token.access_token, Number(runId), clickedTestCaseId);
setRunCases((prevRunCases) => { setRunCases((prevRunCases) => {
return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId); 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) => ({ const runCaseInfo: RunCaseInfoType[] = keys.map((caseId) => ({
runId: runId, runId: Number(runId),
caseId: caseId, caseId: caseId,
})); }));
if (isInclude) { if (isInclude) {
const createdRunCases = await bulkCreateRunCases(runCaseInfo); const createdRunCases = await bulkCreateRunCases(context.token.access_token, Number(runId), runCaseInfo);
setRunCases((prevRunCases) => [...prevRunCases, ...createdRunCases]); setRunCases((prevRunCases) => [...prevRunCases, ...createdRunCases]);
} else { } else {
await bulkDeleteRunCases(runCaseInfo); await bulkDeleteRunCases(context.token.access_token, Number(runId), runCaseInfo);
setRunCases((prevRunCases) => { setRunCases((prevRunCases) => {
return prevRunCases.filter((runCase) => { return prevRunCases.filter((runCase) => {
return !runCaseInfo.some((info) => info.caseId === runCase.caseId); return !runCaseInfo.some((info) => info.caseId === runCase.caseId);
@@ -217,7 +225,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props)
isLoading={isUpdating} isLoading={isUpdating}
onPress={async () => { onPress={async () => {
setIsUpdating(true); setIsUpdating(true);
await updateRun(testRun); await updateRun(context.token.access_token, testRun);
setIsUpdating(false); setIsUpdating(false);
}} }}
> >

View File

@@ -8,7 +8,7 @@ const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
type Props = { type Props = {
statusCounts: RunStatusCountType[]; statusCounts: RunStatusCountType[];
messages: RunMessages; messages: RunMessages;
theme: string; theme: string | undefined;
}; };
export default function RunProgressDounut({ statusCounts, messages, theme }: Props) { export default function RunProgressDounut({ statusCounts, messages, theme }: Props) {

View File

@@ -2,7 +2,7 @@ import Config from '@/config/config';
const apiServer = Config.apiServer; const apiServer = Config.apiServer;
import { RunType, RunCaseInfoType } from '@/types/run'; import { RunType, RunCaseInfoType } from '@/types/run';
async function fetchRun(runId: string) { async function fetchRun(jwt: string, runId: number) {
const url = `${apiServer}/runs/${runId}`; const url = `${apiServer}/runs/${runId}`;
try { try {
@@ -10,6 +10,7 @@ async function fetchRun(runId: string) {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', '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}`; const url = `${apiServer}/runs?projectId=${projectId}`;
try { try {
@@ -32,6 +33,7 @@ async function fetchRuns(projectId: string) {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', '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 = { const newTestRun = {
name, name,
configurations: 0, configurations: 0,
description, description,
state: 0, state: 0,
projectId: projectId,
}; };
const fetchOptions = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
}, },
body: JSON.stringify(newTestRun), body: JSON.stringify(newTestRun),
}; };
const url = `${apiServer}/runs`; const url = `${apiServer}/runs?projectId=${projectId}`;
try { try {
const response = await fetch(url, fetchOptions); 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 = { const fetchOptions = {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
}, },
body: JSON.stringify(updateTestRun), 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 = { const fetchOptions = {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', '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}`; const url = `${apiServer}/runcases?runId=${runId}`;
try { try {
@@ -131,6 +135,7 @@ async function fetchRunCases(runId: string) {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', '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 = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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 = { const fetchOptions = {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', '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 = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
}, },
body: JSON.stringify(runCaseInfo), body: JSON.stringify(runCaseInfo),
}; };
const url = `${apiServer}/runcases/bulknew`; const url = `${apiServer}/runcases/bulknew?runId=${runId}`;
try { try {
const response = await fetch(url, fetchOptions); 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 = { const fetchOptions = {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', '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 = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
}, },
body: JSON.stringify(runCaseInfo), body: JSON.stringify(runCaseInfo),
}; };
const url = `${apiServer}/runcases/bulkdelete`; const url = `${apiServer}/runcases/bulkdelete?runId=${runId}`;
try { try {
const response = await fetch(url, fetchOptions); const response = await fetch(url, fetchOptions);

View File

@@ -30,6 +30,7 @@ export type TokenContextType = {
isAdmin: () => boolean; isAdmin: () => boolean;
isProjectManager: (projectId: number) => boolean; isProjectManager: (projectId: number) => boolean;
isProjectDeveloper: (projectId: number) => boolean; isProjectDeveloper: (projectId: number) => boolean;
isProjectReporter: (projectId: number) => boolean;
setToken: (token: TokenType) => void; setToken: (token: TokenType) => void;
storeTokenToLocalStorage: (token: TokenType) => void; storeTokenToLocalStorage: (token: TokenType) => void;
removeTokenFromLocalStorage: () => void; removeTokenFromLocalStorage: () => void;

View File

@@ -8,6 +8,7 @@ import {
isAdmin as tokenIsAdmin, isAdmin as tokenIsAdmin,
isProjectManager as tokenIsProjectManager, isProjectManager as tokenIsProjectManager,
isProjectDeveloper as tokenIsProjectDeveloper, isProjectDeveloper as tokenIsProjectDeveloper,
isProjectReporter as tokenIsProjectReporter,
checkSignInPage as tokenCheckSignInPage, checkSignInPage as tokenCheckSignInPage,
fetchMyRoles, fetchMyRoles,
} from './token'; } from './token';
@@ -70,6 +71,10 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
return tokenIsProjectDeveloper(projectRoles, projectId); return tokenIsProjectDeveloper(projectRoles, projectId);
}; };
const isProjectReporter = (projectId: number) => {
return tokenIsProjectReporter(projectRoles, projectId);
};
const tokenContext = { const tokenContext = {
token, token,
projectRoles, projectRoles,
@@ -77,6 +82,7 @@ const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
isAdmin, isAdmin,
isProjectManager, isProjectManager,
isProjectDeveloper, isProjectDeveloper,
isProjectReporter,
setToken, setToken,
storeTokenToLocalStorage, storeTokenToLocalStorage,
removeTokenFromLocalStorage, removeTokenFromLocalStorage,

View File

@@ -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}`; const url = `${apiServer}/cases?folderId=${folderId}`;
try { try {

View File

@@ -112,6 +112,33 @@ function isProjectDeveloper(projectRoles: ProjectRoleType[], projectId: number)
return false; 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/*' // 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);
@@ -142,4 +169,13 @@ function checkSignInPage(token: TokenType, pathname: string) {
return ret; return ret;
} }
export { isSignedIn, isAdmin, isProjectManager, isProjectDeveloper, isPrivatePath, checkSignInPage, fetchMyRoles }; export {
isSignedIn,
isAdmin,
isProjectManager,
isProjectDeveloper,
isProjectReporter,
isPrivatePath,
checkSignInPage,
fetchMyRoles,
};