generated from corrad-software/corrad-af-2024
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:
parent
63942b275d
commit
40cf8ebab5
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ node_modules
|
||||
|
||||
# Uploads directory
|
||||
public/uploads/
|
||||
server/api/dms/sample-files-for-s3
|
||||
|
@ -328,8 +328,23 @@ const performUpload = async () => {
|
||||
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
1534
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@ -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
96
pages/test.vue
Normal 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>
|
83
server/api/dms/upload-file.post.js
Normal file
83
server/api/dms/upload-file.post.js
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
40
server/api/test/test-response.post.js
Normal file
40
server/api/test/test-response.post.js
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user