Added file upload functionality

Backend works when trying to use Postman to request the API endpoint. File upload in the frontend also works since the data is parsed properly as multi-part form data. The issue is the frontend seems to cannot directly send request to backend and is outright rejected.
This commit is contained in:
shb 2025-06-16 14:29:33 +08:00
parent 63942b275d
commit 40cf8ebab5
8 changed files with 2868 additions and 31 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ node_modules
# Uploads directory
public/uploads/
server/api/dms/sample-files-for-s3

View File

@ -327,9 +327,24 @@ const performUpload = async () => {
uploadProgress.value = Math.round(((i * 100) + progress) / selectedFiles.value.length);
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log(file);
// Here you would implement actual file upload
console.log('Uploading:', file.name, 'with metadata:', metadata);
const formData = new FormData();
formData.append('fileName', metadata.name);
formData.append('file', file);
console.log(formData);
const response = await fetch('/api/dms/upload-file', {
method: "POST",
// Let browser automatically set headers for multipart/form-data
body: formData,
});
console.log(response);
// console.log('Uploading:', file.name, 'with metadata:', metadata);
}
success(`Successfully uploaded ${selectedFiles.value.length} file(s)`);

1534
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,10 @@
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.8.0",
"@pinia-plugin-persistedstate/nuxt": "^1.1.1",
"@types/archiver": "^6.0.2",
"@types/jsonwebtoken": "^9.0.9",
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.11",
"@vite-pwa/nuxt": "^0.1.0",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.16.1",
@ -19,12 +22,10 @@
"nuxt-icon": "^0.1.7",
"nuxt-security": "^0.13.0",
"nuxt-typed-router": "^3.2.5",
"postcss-import": "^15.1.0",
"@types/multer": "^1.4.11",
"@types/mime-types": "^2.1.4",
"@types/archiver": "^6.0.2"
"postcss-import": "^15.1.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.828.0",
"@babel/eslint-parser": "^7.19.1",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-javascript": "^6.1.6",
@ -44,6 +45,8 @@
"@fullcalendar/scrollgrid": "^5.11.3",
"@fullcalendar/timegrid": "^5.11.3",
"@fullcalendar/vue3": "^5.11.2",
"@iconify/json": "^2.2.156",
"@iconify/vue": "^4.1.1",
"@kiwicom/eslint-config": "^12.7.3",
"@pinia/nuxt": "^0.4.11",
"@popperjs/core": "^2.11.8",
@ -54,19 +57,34 @@
"@vueuse/core": "^9.5.0",
"@vueuse/nuxt": "^9.5.0",
"apexcharts": "^3.36.0",
"archiver": "^6.0.1",
"chart.js": "^3.9.1",
"codemirror": "^6.0.1",
"cross-env": "^7.0.3",
"crypto-js": "^4.1.1",
"date-fns": "^2.30.0",
"file-type": "^18.7.0",
"floating-vue": "^2.0.0-beta.24",
"fuse.js": "^7.0.0",
"joi": "^17.11.0",
"jose": "^5.1.3",
"jsonwebtoken": "^8.5.1",
"jspdf": "^2.5.1",
"ldapjs": "^3.0.7",
"luxon": "^3.1.0",
"maska": "^1.5.0",
"mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1",
"node-stream-zip": "^1.15.0",
"passport": "^0.7.0",
"passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.7.0",
"passport-saml": "^3.2.4",
"pinia": "^2.1.6",
"prettier": "^2.8.1",
"prisma": "^5.1.1",
"sass": "^1.62.0",
"sharp": "^0.32.6",
"swiper": "^8.4.4",
"thememirror": "^2.0.1",
"uuid": "^10.0.0",
@ -84,24 +102,7 @@
"vue3-click-away": "^1.2.4",
"vue3-dropzone": "^2.0.1",
"vue3-recaptcha-v2": "^2.0.2",
"vuedraggable": "^4.1.0",
"multer": "^1.4.5-lts.1",
"file-type": "^18.7.0",
"mime-types": "^2.1.35",
"sharp": "^0.32.6",
"ldapjs": "^3.0.7",
"passport": "^0.7.0",
"passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.7.0",
"passport-saml": "^3.2.4",
"jose": "^5.1.3",
"node-stream-zip": "^1.15.0",
"archiver": "^6.0.1",
"@iconify/vue": "^4.1.1",
"@iconify/json": "^2.2.156",
"date-fns": "^2.30.0",
"fuse.js": "^7.0.0",
"joi": "^17.11.0"
"vuedraggable": "^4.1.0"
},
"overrides": {
"luxon": "^3.1.0"

96
pages/test.vue Normal file
View File

@ -0,0 +1,96 @@
<script setup>
definePageMeta({
title: "API Test Page"
});
const message = ref('');
const selectedFile = ref(null);
const isUploading = ref(false);
async function testApi() {
try {
const response = await $fetch('/api/test/test-response', {
method: 'POST'
});
message.value = JSON.stringify(response, null, 2);
} catch (error) {
message.value = 'Error: ' + error.message;
}
}
async function handleFileUpload() {
if (!selectedFile.value) {
message.value = 'Please select a file first';
return;
}
try {
isUploading.value = true;
message.value = 'Uploading file...';
const formData = new FormData();
formData.append('fileName', selectedFile.value.name);
formData.append('file', selectedFile.value);
const response = await $fetch('/api/test/test-response', {
method: 'POST',
body: formData
});
message.value = JSON.stringify(response, null, 2);
} catch (error) {
message.value = 'Error: ' + error.message;
} finally {
isUploading.value = false;
}
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
selectedFile.value = file;
message.value = `Selected file: ${file.name} (${formatFileSize(file.size)})`;
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
</script>
<template>
<div class="flex flex-col items-center justify-center min-h-screen p-4 space-y-6">
<!-- File Upload Section -->
<div class="w-full max-w-md space-y-4">
<div class="flex flex-col items-center space-y-4">
<input
type="file"
@change="handleFileSelect"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
/>
<button
@click="handleFileUpload"
:disabled="!selectedFile || isUploading"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors disabled:bg-blue-300 disabled:cursor-not-allowed w-full"
>
{{ isUploading ? 'Uploading...' : 'Upload File' }}
</button>
</div>
</div>
<!-- Test API Button -->
<button
@click="testApi"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Test API
</button>
<!-- Response Message -->
<pre v-if="message" class="mt-4 p-4 bg-gray-100 rounded w-full max-w-md overflow-x-auto">{{ message }}</pre>
</div>
</template>

View File

@ -0,0 +1,83 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // ES Modules import
import { readMultipartFormData } from 'h3';
export default defineEventHandler(async (event) => {
const parts = [];
// Allow headers for specific origins
// setHeader(event, 'Access-Control-Allow-Origin', 'http://localhost:3000');
// setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS');
// setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type');
if (event.method === 'OPTIONS') {
return new Response(null, { status: 204 });
}
try {
const parts = await readMultipartFormData(event, { maxSize: 20 * 1024 * 1024});
if (!parts) {
return {
status: 400,
message: "Bad request. No parts found."
}
}
} catch (error) {
console.error('Failed to read multi-part data:', error);
return {
status: 500,
message: "Failed to read multi-part data"
}
}
console.log("Hello from the backend!");
const S3_Config = {
region: process.env.NUXT_AWS_REGION,
credentials: {
accessKeyId: process.env.NUXT_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NUXT_AWS_SECRET_ACCESS_KEY
}
}
// Create S3 client with config
const client = new S3Client(S3_Config);
// const { fileName, file } = await readMultipartFormData(event);
const fileNamePart = parts.find(p => p.name === "fileName");
const filePart = parts.find(p => p.name === "file");
if (!fileNamePart || !filePart) {
return {
status: 400,
message: "Missing required fields { fileName, file }"
}
}
const fileName = fileNamePart.data.toString();
const file = filePart.data;
const input = {
Bucket: process.env.NUXT_AWS_BUCKET,
Key: fileName,
Body: file,
ContentType: filePart.type
}
const command = new PutObjectCommand(input);
try {
const response = await client.send(command);
console.log(response);
return response;
} catch (error) {
console.error(error);
return {
status: 500,
message: "Failed to upload file to S3",
error: error
}
}
})

View File

@ -0,0 +1,40 @@
import { readMultipartFormData } from 'h3';
export default defineEventHandler(async (event) => {
try {
const parts = await readMultipartFormData(event);
if (!parts) {
return {
status: 400,
message: "No form data received"
};
}
const fileNamePart = parts.find(p => p.name === "fileName");
const filePart = parts.find(p => p.name === "file");
if (!fileNamePart || !filePart) {
return {
status: 400,
message: "Missing required fields (fileName or file)"
};
}
const fileName = fileNamePart.data.toString();
console.log("Received file:", fileName);
return {
status: 200,
message: "File received successfully",
fileName: fileName
};
} catch (error) {
console.error("Error processing file upload:", error);
return {
status: 500,
message: "Error processing file upload",
error: error.message
};
}
});

1083
yarn.lock

File diff suppressed because it is too large Load Diff