fix(medusa, medusa-file-*): Protected uploads for file services (#2433)
This commit is contained in:
1 parent
7d77d91a3d
commit
3c5e31c645
11 files changed
+187
-11
No files matched your search
@@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user