Compare commits

...

6 Commits

Author SHA1 Message Date
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
shb
63942b275d Added delete folder functionality.
Folders can now be deleted using the API.
2025-06-12 15:51:14 +08:00
shb
f709707463 Added patch functionality for renaming and moving folders
Added extra functions:
- Move folder into different directories.
- Rename folders functionality
2025-06-12 13:17:33 +08:00
shb
15c5919d8c Added API for creating folders.
Use POST request for creating folder. Folders are indexed in the database.
2025-06-12 10:51:51 +08:00
shb
822d5e3ca4 Figuring out REST functionality for Nuxt.js 2025-06-12 09:46:56 +08:00
shb
52a080e69a Initialized on Shariff system
Added overrides for Luxon to package.json. Luxon had conflicting dependencies when trying to perform `npm install`
2025-06-12 09:31:47 +08:00
13 changed files with 5629 additions and 206 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)`);

3172
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,23 +102,9 @@
"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

@ -41,11 +41,7 @@ model department {
org_id Int
cabinets cabinets[]
organization organization @relation(fields: [org_id], references: [org_id], onDelete: Cascade, onUpdate: NoAction, map: "department_organization_FK")
<<<<<<< HEAD
user user[]
=======
users sys_user[]
>>>>>>> d4880c491e3491be4f09fbfbc0e0a9f8b5cfb1b8
@@index([org_id], map: "department_organization_FK")
}
@ -54,14 +50,17 @@ model cabinets {
cb_id Int @id @default(autoincrement())
cb_name String @unique(map: "cabinet_master_unique") @db.VarChar(255)
cb_parent_id Int?
cb_private Int? @default(0)
cb_owner Int?
cb_sector String @db.VarChar(7)
dp_id Int?
userID Int?
created_at DateTime? @db.DateTime(0)
modified_at DateTime? @db.DateTime(0)
department department? @relation(fields: [dp_id], references: [dp_id], onDelete: NoAction, onUpdate: NoAction, map: "cabinets_department_FK")
user user? @relation(fields: [userID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "cabinets_user_FK")
@@index([dp_id], map: "cabinets_department_FK")
@@index([userID], map: "cabinets_user_FK")
}
model role {
@ -75,18 +74,23 @@ model role {
}
model user {
userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255)
userUsername String? @db.VarChar(255)
userPassword String? @db.VarChar(255)
userFullName String? @db.VarChar(255)
userEmail String? @db.VarChar(255)
userPhone String? @db.VarChar(255)
userStatus String? @db.VarChar(255)
userCreatedDate DateTime? @db.DateTime(0)
userModifiedDate DateTime? @db.DateTime(0)
userID Int @id @default(autoincrement())
userSecretKey String? @db.VarChar(255)
userUsername String? @db.VarChar(255)
userPassword String? @db.VarChar(255)
userFullName String? @db.VarChar(255)
userEmail String? @db.VarChar(255)
userPhone String? @db.VarChar(255)
userStatus String? @db.VarChar(255)
dp_id Int?
userCreatedDate DateTime? @db.DateTime(0)
userModifiedDate DateTime? @db.DateTime(0)
audit audit[]
cabinets cabinets[]
department department? @relation(fields: [dp_id], references: [dp_id], onDelete: NoAction, onUpdate: NoAction, map: "user_department_FK")
userrole userrole[]
@@index([dp_id], map: "user_department_FK")
}
model userrole {

View File

@ -0,0 +1,40 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default defineEventHandler( async (event) => {
console.log("This is a test for a DELETE request to the backend");
const { cabinet_id } = await readBody(event);
if (!cabinet_id) {
return {
status: 400,
message: "cabinet_id was not provided. No folder was deleted."
};
};
const foundFolder = await prisma.cabinets.findUnique({
where: {
cb_id: cabinet_id
}
});
if (!foundFolder) {
return {
status: 404,
message: "Folder not found. No folder was deleted."
}
} else {
const deletedFolder = await prisma.cabinets.delete({
where: {
cb_id: cabinet_id
}
});
return {
status: 200,
message: "Folder deleted successfully",
folder: deletedFolder
};
}
});

View File

@ -0,0 +1,17 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default defineEventHandler( async (event) => {
console.log("This is a test for a GET request to the backend");
const successMsg = "Hello from the backend";
const folders = await prisma.cabinets.findMany();
return {
status: 200,
message: successMsg,
folders: folders
};
});

View File

@ -0,0 +1,91 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default defineEventHandler( async (event) => {
console.log("This is a test for a patch request.");
const body = await readBody(event);
/*
This is a sample of the expected body data structure:
{
function: "", ["rename", "move"] // Required to determine patch functionality.
cabinet_id: "", // REQUIRED to identify the folder to be patched.
new_name: "", // REQUIRED to rename the folder.
new_parent_id: "", // REQUIRED to move the folder to a new parent.
}
*/
// Function to check for body data.
if (!body.cabinet_id) {
return {
status: 400,
message: "Folder or cabinet_id is required",
}
} else {
const folder = await prisma.cabinets.findUnique({
where: {
cb_id: body.cabinet_id,
}
});
if (!folder) {
return {
status: 404,
message: "The requested folder was not found",
}
}
}
switch (body.function) {
case "rename":
if (!body.new_name) {
return {
status: 400,
message: "new_name was not provided. The folder was not renamed.",
}
}
const renameFolder = await prisma.cabinets.update({
where: {
cb_id: body.cabinet_id,
},
data: {
cb_name: body.new_name,
}
})
return {
status: 200,
message: "Folder renamed successfully",
folder: renameFolder,
}
case "move":
if (!body.new_parent_id || body.new_parent_id === null){
return
}
const moveFolder = await prisma.cabinets.update({
where: {
cb_id: body.cabinet_id,
},
data: {
cb_parent_id: body.new_parent_id,
}
})
return {
status: 200,
message: "Folder moved successfully",
folder: moveFolder,
}
default:
return {
status: 400,
message: "Invalid function",
}
}
})

View File

@ -0,0 +1,63 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default defineEventHandler( async (event) => {
console.log("This is a test for a POST request to the backend");
// const successMsg = "Hello from the backend";
const body = await readBody(event);
if (!body) {
return {
status: 400,
message: "Body was not received"
}
}
/*
This is a sample of the expected body data structure:
{
cabinet_name: "Cabinet 1",
cabinet_parent_id: "", // NULL means its a root folder and has no parents.
cabinet_owner: "", // OPTIONAL
cabinet_sector: "", // Dunno??? Has a max length of 7 characters.
dp_id: "", // LEAVE NULL department ID. Foreign key. Is disabled for development purposes.
userID: "", // LEAVE NULL User ID. Foreign key. Is disabled for development purposes.
}
*/
// The following are functions to check the body data structure.
console.log(body.cabinet_name);
if (!body.cabinet_name || !body.cabinet_sector) {
return {
status: 400,
message: "cabinet_name and cabinet_sector are required"
};
};
// Checked body data.
const folderData = {
cb_name: body.cabinet_name,
cb_parent_id: body.cabinet_parent_id,
cb_owner: body.cabinet_owner,
cb_sector: body.cabinet_sector,
dp_id: body.dp_id,
userID: body.userID,
created_at: new Date(),
modified_at: new Date()
};
// Create new folder using checked data.
const newFolder = await prisma.cabinets.create({ data: folderData })
return {
status: 201,
message: "Folder created successfully",
folder: newFolder
};
});

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
};
}
});

2133
yarn.lock

File diff suppressed because it is too large Load Diff