From fe8b82cb4ba350115d051d8521c5fe0935afd9fa Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:51:26 +0900 Subject: [PATCH] feat: inplement auth for cases --- backend/index.js | 2 - backend/routes/cases/bulkDelete.js | 25 ------- backend/routes/cases/delete.js | 15 +++-- backend/routes/cases/edit.js | 3 +- backend/routes/cases/index.js | 3 +- backend/routes/cases/new.js | 3 +- backend/routes/cases/show.js | 3 +- frontend/messages/en.json | 2 + frontend/messages/ja.json | 2 + .../folders/[folderId]/cases/CasesPane.tsx | 66 ++++++++++++++----- .../[folderId]/cases/TestCaseTable.tsx | 20 +----- .../folders/[folderId]/cases/caseControl.ts | 56 +++++++++------- .../folders/[folderId]/cases/page.tsx | 2 + frontend/types/case.ts | 2 + 14 files changed, 106 insertions(+), 98 deletions(-) delete mode 100644 backend/routes/cases/bulkDelete.js diff --git a/backend/index.js b/backend/index.js index b7dccf8..62b04e1 100644 --- a/backend/index.js +++ b/backend/index.js @@ -66,13 +66,11 @@ const casesShowRoute = require('./routes/cases/show')(sequelize); const casesNewRoute = require('./routes/cases/new')(sequelize); const casesEditRoute = require('./routes/cases/edit')(sequelize); const casesDeleteRoute = require('./routes/cases/delete')(sequelize); -const casesBulkDeleteRoute = require('./routes/cases/bulkDelete')(sequelize); app.use('/cases', casesIndexRoute); app.use('/cases', casesShowRoute); app.use('/cases', casesNewRoute); app.use('/cases', casesEditRoute); app.use('/cases', casesDeleteRoute); -app.use('/cases', casesBulkDeleteRoute); // "/steps" const stepsNewRoute = require('./routes/steps/new')(sequelize); diff --git a/backend/routes/cases/bulkDelete.js b/backend/routes/cases/bulkDelete.js deleted file mode 100644 index e1ad768..0000000 --- a/backend/routes/cases/bulkDelete.js +++ /dev/null @@ -1,25 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const defineCase = require('../../models/cases'); -const { DataTypes } = require('sequelize'); - -module.exports = function (sequelize) { - const Case = defineCase(sequelize, DataTypes); - - router.post('/bulkdelete', async (req, res) => { - const { caseIds } = req.body; - if (!caseIds || !Array.isArray(caseIds)) { - return res.status(400).send('Invalid caseIds array'); - } - - try { - await Case.destroy({ where: { id: caseIds } }); - res.status(204).send(); - } catch (error) { - console.error(error); - res.status(500).send('Internal Server Error'); - } - }); - - return router; -}; diff --git a/backend/routes/cases/delete.js b/backend/routes/cases/delete.js index 2cea646..0a20de6 100644 --- a/backend/routes/cases/delete.js +++ b/backend/routes/cases/delete.js @@ -4,16 +4,17 @@ const defineCase = require('../../models/cases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); const Case = defineCase(sequelize, DataTypes); - router.delete('/:caseId', async (req, res) => { - const caseId = req.params.caseId; + router.post('/bulkdelete', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + const { caseIds } = req.body; + if (!caseIds || !Array.isArray(caseIds)) { + return res.status(400).send('Invalid caseIds array'); + } + try { - const testcase = await Case.findByPk(caseId); - if (!testcase) { - return res.status(404).send('Case not found'); - } - await testcase.destroy(); + await Case.destroy({ where: { id: caseIds } }); res.status(204).send(); } catch (error) { console.error(error); diff --git a/backend/routes/cases/edit.js b/backend/routes/cases/edit.js index b238069..520cc74 100644 --- a/backend/routes/cases/edit.js +++ b/backend/routes/cases/edit.js @@ -5,10 +5,11 @@ const defineStep = require('../../models/steps'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { + const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); const Case = defineCase(sequelize, DataTypes); const Step = defineStep(sequelize, DataTypes); - router.put('/:caseId', async (req, res) => { + router.put('/:caseId', verifySignedIn, verifyProjectDeveloper, async (req, res) => { const caseId = req.params.caseId; const updateCase = req.body; try { diff --git a/backend/routes/cases/index.js b/backend/routes/cases/index.js index 56b260e..b89c25e 100644 --- a/backend/routes/cases/index.js +++ b/backend/routes/cases/index.js @@ -5,8 +5,9 @@ const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { const Case = defineCase(sequelize, DataTypes); + const { verifySignedIn, verifyProjectVisible } = require('../../middleware/auth')(sequelize); - router.get('/', async (req, res) => { + router.get('/', verifySignedIn, verifyProjectVisible, async (req, res) => { const { folderId } = req.query; if (!folderId) { diff --git a/backend/routes/cases/new.js b/backend/routes/cases/new.js index c339fa9..d83643c 100644 --- a/backend/routes/cases/new.js +++ b/backend/routes/cases/new.js @@ -14,9 +14,10 @@ function isEmpty(value) { } module.exports = function (sequelize) { + const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); const Case = defineCase(sequelize, DataTypes); - router.post('/', async (req, res) => { + router.post('/', verifySignedIn, verifyProjectDeveloper, async (req, res) => { try { if ( requiredFields.some((field) => { diff --git a/backend/routes/cases/show.js b/backend/routes/cases/show.js index 0fd200f..b1e6ff4 100644 --- a/backend/routes/cases/show.js +++ b/backend/routes/cases/show.js @@ -13,8 +13,9 @@ module.exports = function (sequelize) { Step.belongsToMany(Case, { through: 'caseSteps' }); Case.belongsToMany(Attachment, { through: 'caseAttachments' }); Attachment.belongsToMany(Case, { through: 'caseAttachments' }); + const { verifySignedIn, verifyProjectVisible } = require('../../middleware/auth')(sequelize); - router.get('/:caseId', async (req, res) => { + router.get('/:caseId', verifySignedIn, verifyProjectVisible, async (req, res) => { const caseId = req.params.caseId; if (!caseId) { diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9d258a0..e11fb90 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -154,6 +154,8 @@ "actions": "Actions", "delete_case": "Delete test case", "delete": "Delete", + "close": "Close", + "are_you_sure": "Are you sure you want to delete test cases?", "new_test_case": "New Test Case", "status": "Status", "critical": "Critical", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 5b5952f..798c1f4 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -153,6 +153,8 @@ "actions": "アクション", "delete_case": "テストケースを削除", "delete": "削除", + "close": "閉じる", + "are_you_sure": "テストケースを削除してもよろしいですか?", "new_test_case": "新規テストケース", "status": "ステータス", "critical": "致", 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 b0d45c6..65355bb 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 @@ -1,8 +1,10 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState, useEffect, useContext } from 'react'; +import { TokenContext } from '@/utils/TokenProvider'; import TestCaseTable from './TestCaseTable'; -import { fetchCases, createCase, deleteCase, deleteCases } from './caseControl'; -import { CasesMessages } from '@/types/case'; +import { fetchCases, createCase, deleteCases } from './caseControl'; +import { CaseType, CasesMessages } from '@/types/case'; +import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; type Props = { projectId: string; @@ -12,11 +14,18 @@ type Props = { }; export default function CasesPane({ projectId, folderId, messages, locale }: Props) { - const [cases, setCases] = useState([]); + const [cases, setCases] = useState([]); + const context = useContext(TokenContext); + useEffect(() => { async function fetchDataEffect() { + if (!context.isSignedIn()) { + return; + } try { - const data = await fetchCases(folderId); + const data = await fetchCases(context.token.access_token, folderId); + console.log(data); + setCases(data); } catch (error: any) { console.error('Error in effect:', error.message); @@ -24,25 +33,39 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro } fetchDataEffect(); - }, []); + }, [context, folderId]); const handleCreateCase = async (folderId: string) => { - const newCase = await createCase(folderId); + const newCase = await createCase(context.token.access_token, folderId); const updateCases = [...cases]; updateCases.push(newCase); setCases(updateCases); }; - const handleDeleteCase = async (caseId: number) => { - await deleteCase(caseId); - const data = await fetchCases(folderId); - setCases(data); + // Delete confirm dialog + const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false); + const [deleteCaseIds, setDeleteCaseIds] = useState([]); + const closeDeleteConfirmDialog = () => { + setIsDeleteConfirmDialogOpen(false); + setDeleteCaseIds([]); }; - const handleDeleteCases = async (deleteCaseIds: string[]) => { - await deleteCases(deleteCaseIds); - const data = await fetchCases(folderId); - setCases(data); + const onDeleteCases = (deleteCaseIds: number[]) => { + setDeleteCaseIds(deleteCaseIds); + setIsDeleteConfirmDialogOpen(true); + }; + + const onDeleteCase = async (deleteCaseId: number) => { + setDeleteCaseIds([deleteCaseId]); + setIsDeleteConfirmDialogOpen(true); + }; + + const onConfirm = async () => { + if (deleteCaseIds.length > 0) { + await deleteCases(context.token.access_token, deleteCaseIds); + setCases(cases.filter((entry) => !deleteCaseIds.includes(entry.id))); + closeDeleteConfirmDialog(); + } }; return ( @@ -51,11 +74,20 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro projectId={projectId} cases={cases} onCreateCase={() => handleCreateCase(folderId)} - onDeleteCase={handleDeleteCase} - onDeleteCases={handleDeleteCases} + onDeleteCase={() => onDeleteCase} + onDeleteCases={() => onDeleteCases} messages={messages} locale={locale} /> + + ); } diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx index af54e90..6a61c37 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx @@ -16,28 +16,12 @@ import { } from '@nextui-org/react'; import { Link, NextUiLinkClasses } from '@/src/navigation'; import { Plus, MoreVertical, Trash, Circle } from 'lucide-react'; -import { CasesMessages } from '@/types/case'; +import { CaseType, CasesMessages } from '@/types/case'; import { priorities } from '@/config/selection'; -type Case = { - id: number; - title: string; - state: number; - priority: number; - type: number; - automationStatus: number; - description: string; - template: number; - preConditions: string; - expectedResults: string; - folderId: number; - createdAt: string; - updatedAt: string; -}; - type Props = { projectId: string; - cases: Case[]; + cases: CaseType[]; onCreateCase: () => void; onDeleteCase: (caseId: number) => void; onDeleteCases: (selectedCases: string[]) => void; diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts index 42f85b1..f6bfc28 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/caseControl.ts @@ -2,7 +2,7 @@ import Config from '@/config/config'; const apiServer = Config.apiServer; import { CaseType } from '@/types/case'; -async function fetchCase(caseId: number) { +async function fetchCase(jwt: string, caseId: number) { const url = `${apiServer}/cases/${caseId}`; try { @@ -10,6 +10,7 @@ async function fetchCase(caseId: number) { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }); @@ -24,7 +25,7 @@ async function fetchCase(caseId: number) { } } -async function fetchCases(folderId: string) { +async function fetchCases(jwt: string, folderId: string) { const url = `${apiServer}/cases?folderId=${folderId}`; try { @@ -32,6 +33,7 @@ async function fetchCases(folderId: string) { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, }); @@ -46,7 +48,7 @@ async function fetchCases(folderId: string) { } } -async function createCase(folderId: string) { +async function createCase(jwt: string, folderId: string) { const newCase = { title: 'untitled case', state: 0, @@ -64,6 +66,7 @@ async function createCase(folderId: string) { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(newCase), }; @@ -83,11 +86,12 @@ async function createCase(folderId: string) { } } -async function updateCase(updateCaseData: CaseType) { +async function updateCase(jwt: string, updateCaseData: CaseType) { const fetchOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, body: JSON.stringify(updateCaseData), }; @@ -107,34 +111,36 @@ async function updateCase(updateCaseData: CaseType) { } } -async function deleteCase(caseId: number) { - const fetchOptions = { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }; +// async function deleteCase(jwt: string, caseId: number) { +// const fetchOptions = { +// method: 'DELETE', +// headers: { +// 'Content-Type': 'application/json', +// Authorization: `Bearer ${jwt}`, +// }, +// }; - const url = `${apiServer}/cases/${caseId}`; +// const url = `${apiServer}/cases/${caseId}`; - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - } catch (error: any) { - console.error('Error deleting case:', error); - throw error; - } -} +// try { +// const response = await fetch(url, fetchOptions); +// if (!response.ok) { +// throw new Error(`HTTP error! Status: ${response.status}`); +// } +// } catch (error: any) { +// console.error('Error deleting case:', error); +// throw error; +// } +// } -async function deleteCases(deleteCases: string[]) { +async function deleteCases(jwt: string, deleteCaseIds: number[]) { const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, }, - body: JSON.stringify({ caseIds: deleteCases }), + body: JSON.stringify({ caseIds: deleteCaseIds }), }; const url = `${apiServer}/cases/bulkdelete`; @@ -150,4 +156,4 @@ async function deleteCases(deleteCases: string[]) { } } -export { fetchCase, fetchCases, updateCase, createCase, deleteCase, deleteCases }; +export { fetchCase, fetchCases, updateCase, createCase, deleteCases }; diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx index 421c727..939d32a 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx @@ -11,6 +11,8 @@ export default function Page({ params }: { params: { projectId: string; folderId actions: t('actions'), deleteCase: t('delete_case'), delete: t('delete'), + close: t('close'), + areYouSure: t('are_you_sure'), newTestCase: t('new_test_case'), status: t('status'), critical: t('critical'), diff --git a/frontend/types/case.ts b/frontend/types/case.ts index cde45a2..c5f8a7b 100644 --- a/frontend/types/case.ts +++ b/frontend/types/case.ts @@ -66,6 +66,8 @@ export type CasesMessages = { priority: string; actions: string; deleteCase: string; + close: string; + areYouSure: string; delete: string; newTestCase: string; status: string;