fix(medusa, medusa-file-*): Protected uploads for file services (#2433)

This commit is contained in:
Philip Korsholm authored and GitHub committed 2022-10-18 10:46:47 +02:00
1 parent 7d77d91a3d
commit 3c5e31c645
11 files changed
+187 -11

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
---
"medusa-file-minio": patch
"medusa-file-s3": patch
"medusa-file-spaces": patch
"@medusajs/medusa-js": patch
"medusa-react": patch
"@medusajs/medusa": patch
---
Add protected uploads to fileservices
@@ -26,13 +26,24 @@ class MinioService extends AbstractFileService {
upload(file) { upload(file) {
this.updateAwsConfig_() this.updateAwsConfig_()
return this.uploadFile(file)
}
uploadProtected(file) {
this.validatePrivateBucketConfiguration_(true)
this.updateAwsConfig_(true)
return this.uploadFile(file, { isProtected: true })
}
uploadFile(file, options = { isProtected: false }) {
const parsedFilename = parse(file.originalname) const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const s3 = new aws.S3() const s3 = new aws.S3()
const params = { const params = {
ACL: "public-read", ACL: options.isProtected ? "private" : "public-read",
Bucket: this.bucket_, Bucket: options.isProtected ? this.private_bucket_ : this.bucket_,
Body: fs.createReadStream(file.path), Body: fs.createReadStream(file.path),
Key: fileKey, Key: fileKey,
} }
@@ -137,9 +148,12 @@ class MinioService extends AbstractFileService {
} }
validatePrivateBucketConfiguration_(usePrivateBucket) { validatePrivateBucketConfiguration_(usePrivateBucket) {
if (usePrivateBucket && !this.private_bucket_) { if (
usePrivateBucket &&
(!this.private_access_key_id_ || !this.private_bucket_)
) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_CONFIGURATION, MedusaError.Types.UNEXPECTED_STATE,
"Private bucket is not configured" "Private bucket is not configured"
) )
} }
+11 -1
View File
@@ -19,9 +19,19 @@ class S3Service extends AbstractFileService {
upload(file) { upload(file) {
this.updateAwsConfig() this.updateAwsConfig()
return this.uploadFile(file)
}
uploadProtected(file) {
this.updateAwsConfig()
return this.uploadFile(file, { acl: "private" })
}
uploadFile(file, options = { isProtected: false, acl: undefined }) {
const s3 = new aws.S3() const s3 = new aws.S3()
const params = { const params = {
ACL: "public-read", ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
Bucket: this.bucket_, Bucket: this.bucket_,
Body: fs.createReadStream(file.path), Body: fs.createReadStream(file.path),
Key: `${file.originalname}`, Key: `${file.originalname}`,
@@ -20,12 +20,22 @@ class DigitalOceanService extends AbstractFileService {
upload(file) { upload(file) {
this.updateAwsConfig() this.updateAwsConfig()
return this.uploadFile(file)
}
uploadProtected(file) {
this.updateAwsConfig()
return this.uploadFile(file, { acl: "private" })
}
uploadFile(file, options = { isProtected: false, acl: undefined }) {
const parsedFilename = parse(file.originalname) const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const s3 = new aws.S3() const s3 = new aws.S3()
const params = { const params = {
ACL: "public-read", ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
Bucket: this.bucket_, Bucket: this.bucket_,
Body: fs.createReadStream(file.path), Body: fs.createReadStream(file.path),
Key: fileKey, Key: fileKey,
@@ -24,6 +24,15 @@ class AdminUploadsResource extends BaseResource {
return this.client.request("POST", path, payload, {}, this.headers) return this.client.request("POST", path, payload, {}, this.headers)
} }
createProtected(file: IAdminPostUploadsFileReq): ResponsePromise<AdminUploadsRes> {
const path = `/admin/uploads/protected`
const payload = new FormData()
payload.append("files", file)
return this.client.request("POST", path, payload, {}, this.headers)
}
delete( delete(
payload: AdminDeleteUploadsReq, payload: AdminDeleteUploadsReq,
customHeaders: Record<string, any> = {} customHeaders: Record<string, any> = {}
@@ -26,6 +26,21 @@ export const useAdminUploadFile = (
}, buildOptions(queryClient, undefined, options)) }, buildOptions(queryClient, undefined, options))
} }
export const useAdminUploadProtectedFile = (
options?: UseMutationOptions<
Response<AdminUploadsRes>,
Error,
IAdminPostUploadsFileReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation((payload: IAdminPostUploadsFileReq) => {
return client.admin.uploads.createProtected(payload)
}, buildOptions(queryClient, undefined, options))
}
export const useAdminCreatePresignedDownloadUrl = ( export const useAdminCreatePresignedDownloadUrl = (
options?: UseMutationOptions< options?: UseMutationOptions<
Response<AdminUploadsDownloadUrlRes>, Response<AdminUploadsDownloadUrlRes>,
@@ -0,0 +1,89 @@
import fs from "fs"
import { IFileService } from "../../../../interfaces"
/**
* @oas [post] /uploads/protected
* operationId: "PostUploadsProtected"
* summary: "Upload files with acl or in a non-public bucket"
* description: "Uploads at least one file to the specific fileservice that is installed in Medusa."
* x-authenticated: true
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* files:
* type: string
* format: binary
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.uploads.createProtected(file)
* .then(({ uploads }) => {
* console.log(uploads.length);
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/admin/uploads/protected' \
* --header 'Authorization: Bearer {api_token}' \
* --header 'Content-Type: image/jpeg' \
* --form 'files=@"<FILE_PATH_1>"' \
* --form 'files=@"<FILE_PATH_1>"'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - Upload
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* uploads:
* type: array
* items:
* type: object
* properties:
* url:
* type: string
* description: The URL of the uploaded file.
* format: uri
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const fileService: IFileService = req.scope.resolve("fileService")
const result = await Promise.all(
req.files.map(async (f) => {
return fileService.uploadProtected(f).then((result) => {
fs.unlinkSync(f.path)
return result
})
})
)
res.status(200).json({ uploads: result })
}
export class IAdminPostUploadsFileReq {
originalName: string
path: string
}
@@ -69,7 +69,6 @@ import fs from "fs"
* $ref: "#/components/responses/500_error" * $ref: "#/components/responses/500_error"
*/ */
export default async (req, res) => { export default async (req, res) => {
try {
const fileService = req.scope.resolve("fileService") const fileService = req.scope.resolve("fileService")
const result = await Promise.all( const result = await Promise.all(
@@ -82,10 +81,6 @@ export default async (req, res) => {
) )
res.status(200).json({ uploads: result }) res.status(200).json({ uploads: result })
} catch (err) {
console.log(err)
throw err
}
} }
export class IAdminPostUploadsFileReq { export class IAdminPostUploadsFileReq {
@@ -18,6 +18,12 @@ export default (app) => {
middlewares.wrap(require("./create-upload").default) middlewares.wrap(require("./create-upload").default)
) )
route.post(
"/protected",
upload.array("files"),
middlewares.wrap(require("./create-protected-upload").default)
)
route.delete( route.delete(
"/", "/",
transformBody(AdminDeleteUploadsReq), transformBody(AdminDeleteUploadsReq),
@@ -37,6 +37,12 @@ export interface IFileService extends TransactionBaseService {
* */ * */
upload(file: Express.Multer.File): Promise<FileServiceUploadResult> upload(file: Express.Multer.File): Promise<FileServiceUploadResult>
/**
* upload private file to fileservice
* @param file Multer file from express multipart/form-data
* */
uploadProtected(file: Express.Multer.File): Promise<FileServiceUploadResult>
/** /**
* remove file from fileservice * remove file from fileservice
* @param fileData Remove file described by record * @param fileData Remove file described by record
@@ -76,6 +82,10 @@ export abstract class AbstractFileService
fileData: Express.Multer.File fileData: Express.Multer.File
): Promise<FileServiceUploadResult> ): Promise<FileServiceUploadResult>
abstract uploadProtected(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult>
abstract delete(fileData: DeleteFileType): Promise<void> abstract delete(fileData: DeleteFileType): Promise<void>
abstract getUploadStreamDescriptor( abstract getUploadStreamDescriptor(
+8
View File
@@ -17,6 +17,14 @@ class DefaultFileService extends AbstractFileService {
"Please add a file service plugin in order to manipulate files in Medusa" "Please add a file service plugin in order to manipulate files in Medusa"
) )
} }
async uploadProtected(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult> {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Please add a file service plugin in order to manipulate files in Medusa"
)
}
async delete(fileData: Record<string, any>): Promise<void> { async delete(fileData: Record<string, any>): Promise<void> {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE, MedusaError.Types.UNEXPECTED_STATE,