Compare commits

...

5 Commits

Author SHA1 Message Date
shb
384d571997 Merged AWS upload functionality
Uploading now works with AWS. Documentation can be found in dms-api.md at the root folder.
2025-06-18 12:00:36 +08:00
shb
9333c7085a Merge branch 'development' into cursor-testing 2025-06-17 15:11:28 +08:00
shb
0ba58a1efb Testing path for uploading files
Test route and test file uploading functionality added and confirmed to run properly. Utilized direct-to-storage upload method to bypass sending file data to backend
2025-06-17 13:22:12 +08:00
shb
ffec2a43ee Added testing route to check server issues
Added test routes to pinpoint frontend to backend communication issues
2025-06-17 08:17:53 +08:00
shb
40cf8ebab5 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.
2025-06-16 14:29:33 +08:00
9 changed files with 3021 additions and 38 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

@ -322,14 +322,37 @@ const performUpload = async () => {
const file = selectedFiles.value[i];
const metadata = fileMetadata.value[i];
// Simulate upload progress
for (let progress = 0; progress <= 100; progress += 10) {
uploadProgress.value = Math.round(((i * 100) + progress) / selectedFiles.value.length);
await new Promise(resolve => setTimeout(resolve, 100));
// First get the signed URL
const formData = new FormData();
formData.append('fileName', metadata.name);
formData.append('fileType', file.type);
const signedUrlResponse = await fetch('/api/dms/upload-file', {
method: "POST",
body: formData,
});
const signedUrlData = await signedUrlResponse.json();
if (!signedUrlResponse.ok || !signedUrlData.signedUrl) {
throw new Error(signedUrlData.message || 'Failed to get signed URL');
}
// Here you would implement actual file upload
console.log('Uploading:', file.name, 'with metadata:', metadata);
// Upload directly to S3 using the signed URL
const uploadResponse = await fetch(signedUrlData.signedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type
}
});
if (!uploadResponse.ok) {
throw new Error(`Failed to upload file ${metadata.name} to S3`);
}
// Update progress
uploadProgress.value = Math.round(((i + 1) * 100) / selectedFiles.value.length);
}
success(`Successfully uploaded ${selectedFiles.value.length} file(s)`);

View File

@ -115,4 +115,45 @@
"message": "DMS settings updated successfully",
"data": { "settingID": 1 }
}
```
```
## Files
### POST /api/dms/upload-file
- **Description**: Generates a signed URL for direct file upload to S3
- **Method**: POST
- **Content-Type**: multipart/form-data
- **Request Body**:
```json
{
"fileName": "example.pdf",
"fileType": "application/pdf"
}
```
- **Required Fields**: fileName, fileType
- **Response Example**:
```json
{
"status": 200,
"message": "Signed URL generated for file: example.pdf",
"signedUrl": "https://bucket-name.s3.region.amazonaws.com/..."
}
```
- **Error Responses**:
```json
{
"status": 400,
"message": "Missing required fields { fileName, fileType }"
}
```
```json
{
"status": 500,
"message": "Failed to generate signed URL",
"error": "Error details..."
}
```
- **Notes**:
- The signed URL expires after 60 seconds
- Use the returned signedUrl with a PUT request to upload the file directly to S3
- Include the file's Content-Type header when uploading to S3

1567
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,11 @@
"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",
"@aws-sdk/s3-request-presigner": "^3.830.0",
"@babel/eslint-parser": "^7.19.1",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-javascript": "^6.1.6",
@ -44,6 +46,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 +58,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 +103,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"

126
pages/test.vue Normal file
View File

@ -0,0 +1,126 @@
<script setup>
import { ref } from 'vue';
definePageMeta({
title: "API Test Page"
});
const message = ref('');
const formData = ref({
name: '',
animal: ''
});
const selectedFile = ref(null);
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
// Check file size (10MB limit)
if (file.size > 10 * 1024 * 1024) {
alert('File size must be less than 10MB');
event.target.value = ''; // Clear the input
selectedFile.value = null;
return;
}
selectedFile.value = file;
}
}
async function handleSubmit() {
try {
const multipartFormData = new FormData();
multipartFormData.append('name', formData.value.name);
multipartFormData.append('animal', formData.value.animal);
// Only send file metadata if a file is selected
if (selectedFile.value) {
multipartFormData.append('fileName', selectedFile.value.name);
multipartFormData.append('fileType', selectedFile.value.type);
}
const response = await fetch('/api/test/test-response', {
method: 'POST',
body: multipartFormData
});
const data = await response.json();
message.value = data.message;
// If we have a file and received a signed URL, upload to S3
if (selectedFile.value && data.signedUrl) {
try {
const s3Response = await fetch(data.signedUrl, {
method: 'PUT',
body: selectedFile.value,
headers: {
'Content-Type': selectedFile.value.type
}
});
if (!s3Response.ok) {
throw new Error('Failed to upload file to S3');
}
message.value += '\nFile successfully uploaded to S3!';
} catch (s3Error) {
message.value += '\nError uploading file to S3: ' + s3Error.message;
}
}
} catch (error) {
message.value = 'Error: ' + error.message;
}
}
</script>
<template>
<div class="flex flex-col items-center justify-center min-h-screen p-4 space-y-6">
<!-- Form Section -->
<div class="w-full max-w-md space-y-4">
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Your Name</label>
<input
id="name"
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your name"
required
/>
</div>
<div>
<label for="animal" class="block text-sm font-medium text-gray-700 mb-1">Favorite Animal</label>
<input
id="animal"
v-model="formData.animal"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your favorite animal"
required
/>
</div>
<div>
<label for="file" class="block text-sm font-medium text-gray-700 mb-1">Upload File (Max 10MB)</label>
<input
id="file"
type="file"
@change="handleFileSelect"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-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"
/>
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Submit
</button>
</form>
</div>
<!-- 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,65 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { readMultipartFormData } from 'h3';
export default defineEventHandler(async (event) => {
// Create S3 client with config
const client = new S3Client({
region: process.env.NUXT_AWS_REGION,
credentials: {
accessKeyId: process.env.NUXT_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NUXT_AWS_SECRET_ACCESS_KEY
}
});
try {
const parts = await readMultipartFormData(event, { maxSize: 1024 * 1024 }); // 1MB limit since we're only handling metadata
if (!parts) {
return {
status: 400,
message: "Bad request. No parts found."
}
}
// Extract form data
const fileNamePart = parts.find(p => p.name === "fileName");
const fileTypePart = parts.find(p => p.name === "fileType");
if (!fileNamePart || !fileTypePart) {
return {
status: 400,
message: "Missing required fields { fileName, fileType }"
}
}
const fileName = fileNamePart.data.toString();
const fileType = fileTypePart.data.toString();
// Generate a unique key using timestamp and filename
const uploadKey = `uploads/${Date.now()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.NUXT_AWS_BUCKET,
Key: uploadKey,
ContentType: fileType,
});
// Generate signed URL
const signedUrl = await getSignedUrl(client, command, { expiresIn: 60 });
return {
status: 200,
message: `Signed URL generated for file: ${fileName}`,
signedUrl
};
} catch (error) {
console.error('Error processing request:', error);
return {
status: 500,
message: "Failed to generate signed URL",
error: error.message
}
}
})

View File

@ -0,0 +1,67 @@
import { readMultipartFormData } from 'h3';
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export default defineEventHandler(async (event) => {
const s3 = new S3Client({
region: 'ap-southeast-1',
credentials: {
accessKeyId: process.env.NUXT_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NUXT_AWS_SECRET_ACCESS_KEY,
}
});
try {
const parts = await readMultipartFormData(event, { maxSize: 1024 * 1024 }); // 1MB limit since we're only handling metadata
if (!parts) {
return {
status: 400,
message: "No form data received"
};
}
// Extract form data
const name = parts.find(p => p.name === 'name')?.data.toString() || '';
const animal = parts.find(p => p.name === 'animal')?.data.toString() || '';
const fileName = parts.find(p => p.name === 'fileName')?.data.toString();
const fileType = parts.find(p => p.name === 'fileType')?.data.toString();
// Log the received data
console.log('Received name:', name);
console.log('Received favorite animal:', animal);
// Prepare response message
let message = `You are ${name} and your favorite animal is ${animal}`;
let signedUrl = null;
if (fileName && fileType) {
console.log('Received file metadata:', { fileName, fileType });
message += `. You will upload the file: ${fileName}`;
// Generate a unique key using timestamp and filename
const uploadKey = `uploads/${Date.now()}-${fileName}`;
const s3Command = new PutObjectCommand({
Bucket: process.env.NUXT_AWS_BUCKET,
Key: uploadKey,
ContentType: fileType,
});
signedUrl = await getSignedUrl(s3, s3Command, { expiresIn: 60 });
}
return {
status: 200,
message,
signedUrl
};
} catch (error) {
console.error("Error processing request:", error);
return {
status: 500,
message: "Error processing request",
error: error.message
};
}
});

1107
yarn.lock

File diff suppressed because it is too large Load Diff