From ed1e90c7144f6ca2e234007be72dd70f314b7a48 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:48:56 +0900 Subject: [PATCH 1/3] fix: cannot upload attachment files in docker (#241) --- backend/.gitignore | 3 ++- ...00-rename-path-to-filename-in-attachments.js | 13 +++++++++++++ backend/models/attachments.js | 2 +- backend/public/uploads/dummy.txt | 0 backend/routes/attachments/delete.js | 10 ++++------ backend/routes/attachments/download.js | 9 ++++----- backend/routes/attachments/new.js | 11 +++++------ backend/seeders/seed.js | 5 ++--- docker-compose.yaml | 1 + .../cases/[caseId]/CaseAttachmentsEditor.tsx | 17 ++++++++++++----- .../cases/[caseId]/attachmentControl.test.ts | 6 +++--- .../[folderId]/cases/[caseId]/isImage.ts | 4 ++-- frontend/types/case.ts | 2 +- 13 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 backend/migrations/20250712000000-rename-path-to-filename-in-attachments.js create mode 100644 backend/public/uploads/dummy.txt diff --git a/backend/.gitignore b/backend/.gitignore index 8f31ed2..efcb4ca 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,4 @@ node_modules/ .env -public/uploads/ +public/uploads/* +!public/uploads/dummy.txt diff --git a/backend/migrations/20250712000000-rename-path-to-filename-in-attachments.js b/backend/migrations/20250712000000-rename-path-to-filename-in-attachments.js new file mode 100644 index 0000000..5f615b2 --- /dev/null +++ b/backend/migrations/20250712000000-rename-path-to-filename-in-attachments.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + up: async (queryInterface) => { + // Rename column 'path' to 'filename' in 'Attachments' table + await queryInterface.renameColumn('Attachments', 'path', 'filename'); + }, + + down: async (queryInterface) => { + // Revert column name from 'filename' back to 'path' + await queryInterface.renameColumn('Attachments', 'filename', 'path'); + }, +}; diff --git a/backend/models/attachments.js b/backend/models/attachments.js index 7be6eb5..2f47dc2 100644 --- a/backend/models/attachments.js +++ b/backend/models/attachments.js @@ -8,7 +8,7 @@ function defineAttachment(sequelize, DataTypes) { type: DataTypes.STRING, allowNull: true, }, - path: { + filename: { type: DataTypes.STRING, allowNull: false, }, diff --git a/backend/public/uploads/dummy.txt b/backend/public/uploads/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/backend/routes/attachments/delete.js b/backend/routes/attachments/delete.js index 9bfee9b..2ded08e 100644 --- a/backend/routes/attachments/delete.js +++ b/backend/routes/attachments/delete.js @@ -1,9 +1,9 @@ +const fs = require('fs'); +const path = require('path'); const express = require('express'); const router = express.Router(); -const path = require('path'); -const fs = require('fs'); -const defineAttachment = require('../../models/attachments'); const { DataTypes } = require('sequelize'); +const defineAttachment = require('../../models/attachments'); module.exports = function (sequelize) { const Attachment = defineAttachment(sequelize, DataTypes); @@ -21,9 +21,7 @@ module.exports = function (sequelize) { // delete file from folder const uploadDir = path.join(__dirname, '../../public/uploads'); - const url = attachment.path; - const fileName = url.substring(url.lastIndexOf('/') + 1); - const filePath = path.join(uploadDir, fileName); + const filePath = path.join(uploadDir, attachment.filename); fs.unlink(filePath, (err) => { if (err) { console.error('Error deleting file:', err); diff --git a/backend/routes/attachments/download.js b/backend/routes/attachments/download.js index 35c9270..c2d8d28 100644 --- a/backend/routes/attachments/download.js +++ b/backend/routes/attachments/download.js @@ -1,9 +1,9 @@ +const fs = require('fs'); +const path = require('path'); const express = require('express'); const router = express.Router(); -const path = require('path'); -const fs = require('fs'); -const defineAttachment = require('../../models/attachments'); const { DataTypes } = require('sequelize'); +const defineAttachment = require('../../models/attachments'); module.exports = function (sequelize) { const Attachment = defineAttachment(sequelize, DataTypes); @@ -16,8 +16,7 @@ module.exports = function (sequelize) { return res.status(404).send('Attachment not found'); } - const filename = attachment.path.split('/').pop(); - const filePath = path.join(__dirname, `../../public/uploads/${filename}`); + const filePath = path.join(__dirname, `../../public/uploads/${attachment.filename}`); if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'File not found' }); diff --git a/backend/routes/attachments/new.js b/backend/routes/attachments/new.js index 2c60cc2..2771a60 100644 --- a/backend/routes/attachments/new.js +++ b/backend/routes/attachments/new.js @@ -1,11 +1,11 @@ +const fs = require('fs'); +const path = require('path'); const express = require('express'); const router = express.Router(); -const path = require('path'); -const fs = require('fs'); const multer = require('multer'); +const { DataTypes } = require('sequelize'); const defineAttachment = require('../../models/attachments'); const defineCaseAttachment = require('../../models/caseAttachments'); -const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { const Attachment = defineAttachment(sequelize, DataTypes); @@ -55,11 +55,9 @@ module.exports = function (sequelize) { return res.status(400).json({ error: 'No files uploaded' }); } - const host = req.get('host'); - const protocol = req.protocol; const attachmentsData = files.map((file) => ({ title: file.originalname, - path: `${protocol}://${host}/uploads/${file.filename}`, + filename: file.filename, })); const newAttachments = await Attachment.bulkCreate(attachmentsData, { @@ -74,6 +72,7 @@ module.exports = function (sequelize) { await t.commit(); res.json(newAttachments); } catch (error) { + console.error(error); await t.rollback(); res.status(500).json({ error: 'Internal server error' }); } diff --git a/backend/seeders/seed.js b/backend/seeders/seed.js index 6206161..83efa5c 100644 --- a/backend/seeders/seed.js +++ b/backend/seeders/seed.js @@ -550,19 +550,18 @@ module.exports = { }, ]); - const backendOrigin = process.env.BACKEND_ORIGIN || 'http://localhost:8001'; await queryInterface.bulkInsert('attachments', [ { title: 'Selenium logo', detail: '', - path: `${backendOrigin}/uploads/861px-Selenium_Logo.png`, + filename: '861px-Selenium_Logo.png', createdAt: new Date(), updatedAt: new Date(), }, { title: 'vitest logo', detail: '', - path: `${backendOrigin}/uploads/logo-shadow.svg`, + filename: 'logo-shadow.svg', createdAt: new Date(), updatedAt: new Date(), }, diff --git a/docker-compose.yaml b/docker-compose.yaml index b6e2ac5..74cfdf3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,7 @@ services: - IS_DEMO=false # set to true to seed the database volumes: - db-data:/app/backend/database + - ./backend/public/uploads:/app/backend/public/uploads volumes: db-data: diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseAttachmentsEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseAttachmentsEditor.tsx index 1357659..636e45f 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseAttachmentsEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/CaseAttachmentsEditor.tsx @@ -1,8 +1,11 @@ import { Image, Button, Tooltip, Card, CardBody } from '@heroui/react'; -import { AttachmentType, CaseMessages } from '@/types/case'; import { Trash, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'; -import { isImage } from './isImage'; import { ChangeEvent, DragEvent } from 'react'; +import { isImage } from './isImage'; +import { AttachmentType, CaseMessages } from '@/types/case'; +import Config from '@/config/config'; + +const apiServer = Config.apiServer; type Props = { isDisabled: boolean; @@ -23,8 +26,8 @@ export default function CaseAttachmentsEditor({ onFilesInput, messages, }: Props) { - let images: AttachmentType[] = []; - let others: AttachmentType[] = []; + const images: AttachmentType[] = []; + const others: AttachmentType[] = []; attachments.forEach((attachment) => { if (isImage(attachment)) { @@ -39,7 +42,11 @@ export default function CaseAttachmentsEditor({ {images.map((image, index) => ( - {image.title} + {image.title}

{image.title}

diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.test.ts b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.test.ts index 66c08fc..ff877d0 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.test.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachmentControl.test.ts @@ -22,16 +22,16 @@ describe('attachment control', () => { id: 1, title: '', detail: '', - path: '', + filename: '', createdAt: new Date(), updatedAt: new Date(), caseAttachments: sampleCaseAttachment, }; - sampleAttachment.path = 'public/uploads/abc.png'; + sampleAttachment.filename = 'abc.png'; expect(isImage(sampleAttachment)).toBe(true); - sampleAttachment.path = 'public/uploads/abc.mp3'; + sampleAttachment.filename = 'abc.mp3'; expect(isImage(sampleAttachment)).toBe(false); }); }); diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/isImage.ts b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/isImage.ts index a0bace7..83955d2 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/isImage.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/[caseId]/isImage.ts @@ -1,8 +1,8 @@ import { AttachmentType } from '@/types/case'; function isImage(attachmentFile: AttachmentType) { - let path = attachmentFile.path; - let extension = path.substring(path.lastIndexOf('.') + 1).toLowerCase(); + const filename = attachmentFile.filename; + const extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); if ( extension === 'png' || extension === 'jpg' || diff --git a/frontend/types/case.ts b/frontend/types/case.ts index aad9e39..6f10872 100644 --- a/frontend/types/case.ts +++ b/frontend/types/case.ts @@ -53,7 +53,7 @@ type AttachmentType = { id: number; title: string; detail: string; - path: string; + filename: string; createdAt: Date; updatedAt: Date; caseAttachments: CaseAttachmentType; From f3ebaafdf6659e220632e0b69f5c236887611a09 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:33:45 +0900 Subject: [PATCH 2/3] feat: add health check page (#244) --- backend/routes/health/index.js | 10 +++ backend/routes/index.js | 2 +- backend/server.js | 4 + frontend/components/Footer.tsx | 18 ++++ frontend/messages/en.json | 8 ++ frontend/messages/ja.json | 8 ++ .../src/app/[locale]/HeaderNavbarMenu.tsx | 7 -- frontend/src/app/[locale]/LandingPage.tsx | 5 +- frontend/src/app/[locale]/PaneMainTitle.tsx | 1 - .../src/app/[locale]/health/HealthPage.tsx | 85 +++++++++++++++++++ frontend/src/app/[locale]/health/page.tsx | 32 +++++++ frontend/types/health.ts | 8 ++ 12 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 backend/routes/health/index.js create mode 100644 frontend/components/Footer.tsx create mode 100644 frontend/src/app/[locale]/health/HealthPage.tsx create mode 100644 frontend/src/app/[locale]/health/page.tsx create mode 100644 frontend/types/health.ts diff --git a/backend/routes/health/index.js b/backend/routes/health/index.js new file mode 100644 index 0000000..6252d2c --- /dev/null +++ b/backend/routes/health/index.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); + +module.exports = function () { + router.get('/', async (req, res) => { + res.json({ status: 'ok' }); + }); + + return router; +}; diff --git a/backend/routes/index.js b/backend/routes/index.js index 5ca1411..8424234 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -3,7 +3,7 @@ const router = express.Router(); // "/" GET router.get('/', (req, res) => { - res.send('Test Case Management API Server'); + res.send('This is UnitTCMS API server'); }); module.exports = router; diff --git a/backend/server.js b/backend/server.js index 99270c6..062ef73 100644 --- a/backend/server.js +++ b/backend/server.js @@ -39,6 +39,10 @@ const sequelize = new Sequelize({ const indexRoute = require('./routes/index'); app.use('/', indexRoute); +// "/health" +const healthIndexRoute = require('./routes/health/index')(); +app.use('/health', healthIndexRoute); + // "users" const usersIndexRoute = require('./routes/users/index')(sequelize); const usersFindRoute = require('./routes/users/find')(sequelize); diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx new file mode 100644 index 0000000..649c096 --- /dev/null +++ b/frontend/components/Footer.tsx @@ -0,0 +1,18 @@ +import { Link, NextUiLinkClasses } from '@/src/i18n/routing'; +import { LocaleCodeType } from '@/types/locale'; + +type Props = { + locale: LocaleCodeType; +}; + +export default function Footer({ locale }: Props) { + return ( +
+
Copyright © 2024-present UnitTCMS
+ + + Status + +
+ ); +} diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 409a0e4..aebb48e 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -114,6 +114,14 @@ "not_own_any_projects": "You don't own any projects.", "find_projects": "Find projects" }, + "Health": { + "health_check": "Health Check", + "status": "Status", + "ok": "OK", + "error": "Error", + "api_server": "API Server", + "unittcms_version": "UnitTCMS Version" + }, "Admin": { "user_management": "User Management", "avatar": "Avatar", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index ccd1632..621bede 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -133,6 +133,14 @@ "lost_admin_auth": "管理者権限を失いました", "at_least": "最低1人以上の管理者が必要です。" }, + "Health": { + "health_check": "ヘルスチェック", + "status": "ステータス", + "ok": "正常", + "error": "エラー", + "api_server": "APIサーバー", + "unittcms_version": "UnitTCMS バージョン" + }, "Projects": { "project_list": "プロジェクト一覧", "new_project": "新規プロジェクト", diff --git a/frontend/src/app/[locale]/HeaderNavbarMenu.tsx b/frontend/src/app/[locale]/HeaderNavbarMenu.tsx index c8ff589..dc2f90b 100644 --- a/frontend/src/app/[locale]/HeaderNavbarMenu.tsx +++ b/frontend/src/app/[locale]/HeaderNavbarMenu.tsx @@ -97,13 +97,6 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {

UnitTCMS

- - - - 1.0.0-beta.14 - - - {commonLinks.map((link) => link.isExternal ? ( diff --git a/frontend/src/app/[locale]/LandingPage.tsx b/frontend/src/app/[locale]/LandingPage.tsx index 0dd7701..584a4bf 100644 --- a/frontend/src/app/[locale]/LandingPage.tsx +++ b/frontend/src/app/[locale]/LandingPage.tsx @@ -6,6 +6,7 @@ import DemoImage from './DemoImage'; import { title, subtitle } from '@/components/primitives'; import { PageType } from '@/types/base'; import { LocaleCodeType } from '@/types/locale'; +import Footer from '@/components/Footer'; export default function LandingPage({ params }: PageType) { const t = useTranslations('Index'); @@ -98,9 +99,7 @@ export default function LandingPage({ params }: PageType) {
-
-
Copyright © 2024-present UnitTCMS
-
+