diff --git a/README.md b/README.md index 06ac10d..cadd3ca 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # Quick Start ### After installing xest framework globally; you can create your api with one simple command -`xx ` +> `xx ` The above command will create your new api project then install requied packages and start your database. Please note in the project directory you will find many usefull utils, packages and middlewares. diff --git a/api-sample/package.json b/api-sample/package.json index a008cea..15af533 100644 --- a/api-sample/package.json +++ b/api-sample/package.json @@ -149,6 +149,8 @@ "winston-transport": "^4.3.0", "yamlify-object": "^0.5.1", "yamlify-object-colors": "^1.0.3", - "yup": "^0.27.0" + "yup": "^0.27.0", + "@aws-sdk/client-s3": "^3.608.0", + "@aws-sdk/s3-request-presigner": "^3.608.0" } } diff --git a/api-sample/src/app/controllers/files/download-file/index.js b/api-sample/src/app/controllers/files/download-file/index.js new file mode 100644 index 0000000..8381faa --- /dev/null +++ b/api-sample/src/app/controllers/files/download-file/index.js @@ -0,0 +1,46 @@ +const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const handleAPIError = require("~root/utils/handleAPIError"); + +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET + } +}); + +const uppyFileDownload = async (req, res) => { + const { fileUrl } = req.query; + + try { + if (!fileUrl) { + return res.status(400).json({ message: "File URL is required" }); + } + + const parsedUrl = new URL(fileUrl); + const bucketName = parsedUrl.hostname.split(".")[0]; + const fileKey = parsedUrl.pathname.substring(1); + + if (!bucketName || !fileKey) { + return res.status(400).json({ message: "Invalid file URL" }); + } + + const params = { + Bucket: bucketName, + Key: fileKey + }; + + const command = new GetObjectCommand(params); + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); + + res.status(200).send({ + url: signedUrl, + method: "GET" + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = uppyFileDownload; diff --git a/api-sample/src/app/controllers/files/upload-file/index.js b/api-sample/src/app/controllers/files/upload-file/index.js new file mode 100644 index 0000000..32f41ff --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/index.js @@ -0,0 +1,63 @@ +const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const { v4: uuidv4 } = require("uuid"); +const handleAPIError = require("~root/utils/handleAPIError"); +const uploadFileSchema = require("./schemas/uploadFileShema"); + +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET + } +}); + +function modifyFilename(originalFilename, userId) { + const parts = originalFilename.split("."); + const fileExtension = parts.pop(); + const baseName = parts.join("."); + const uniqueId = uuidv4() + .replace(/-/g, "") + .substring(0, 10); + + const newFilename = `${userId}-${uniqueId}-${baseName}.${fileExtension}`; + + return newFilename; +} + +const uppyFileUpload = async (req, res) => { + const { userId } = req.user; + const { fileName, contentType } = req.body; + + try { + await uploadFileSchema.validate( + { userId, fileName, contentType }, + { + abortEarly: false + } + ); + + const newFileName = modifyFilename(fileName, userId); + const bucketName = process.env.S3_BUCKET_NAME; + + const params = { + Bucket: bucketName, + Key: newFileName, + ContentType: contentType, + ACL: "private" + }; + + const command = new PutObjectCommand(params); + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); + + res.status(201).send({ + url: signedUrl, + method: "POST", + fileKey: newFileName + }); + } catch (err) { + handleAPIError(res, err); + } +}; + +module.exports = uppyFileUpload; diff --git a/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js b/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js new file mode 100644 index 0000000..556c440 --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/schemas/queries/selectUserId.js @@ -0,0 +1,9 @@ +const { submitQuery, getFirst } = require("~root/lib/database"); + +const selectUserById = ({ userId }) => submitQuery` + SELECT user_id + FROM users + WHERE user_id = ${userId} +`; + +module.exports = getFirst(selectUserById, "user_id"); diff --git a/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js b/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js new file mode 100644 index 0000000..e057878 --- /dev/null +++ b/api-sample/src/app/controllers/files/upload-file/schemas/uploadFileShema.js @@ -0,0 +1,53 @@ +const yup = require("yup"); +const selectUserById = require("./queries/selectUserId"); + +const uploadFileSchema = yup.object().shape({ + userId: yup + .number() + .required() + .label("User ID") + .typeError("User ID must be valid.") + .test("doesUserExist", "User ID must exist", function test(userId) { + return selectUserById({ userId }).then(userFound => { + return !!userFound; + }); + }), + fileName: yup + .string() + .required() + .label("File Name") + .typeError("File Name must be valid.") + .matches( + /^[a-zA-Z0-9_.-]+$/, + "File Name can only contain letters, numbers, underscores, dashes, and periods." + ), + contentType: yup + .string() + .required() + .label("Content Type") + .typeError("Content Type must be valid.") + .oneOf( + [ + "image/jpeg", + "image/png", + "application/pdf", + "image/gif" + // "image/bmp", + // "text/plain", + // "text/csv", + // "application/msword", + // "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + // "application/vnd.ms-excel", + // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // "application/zip", + // "application/x-rar-compressed", + // "audio/mpeg", + // "audio/wav", + // "video/mp4", + // "video/x-msvideo" + ], + "Content Type must be a valid MIME type." + ) +}); + +module.exports = uploadFileSchema; diff --git a/cli/utils/writeDotEnvFile.js b/cli/utils/writeDotEnvFile.js index 49421d3..8bd25c2 100644 --- a/cli/utils/writeDotEnvFile.js +++ b/cli/utils/writeDotEnvFile.js @@ -22,6 +22,12 @@ MAILGUN_API_KEY=anythinggoeshere MAILGUN_API_BASE_URL=anythinggoeshere MAILGUN_DOMAIN=anythinggoeshere +// S3 credentials +AWS_REGION=anythinggoeshere +S3_ACCESS_KEY=anythinggoeshere +S3_SECRET=anythinggoeshere +S3_BUCKET_NAME=anythinggoeshere + `; const filePath = path.join(projectRoot, "./.env"); const doesItExist = fs.ensureFileSync(filePath);