From e0599b48c389afa1e3d7fac448c32eb00f4ddb0d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 31 May 2024 14:50:50 +0700 Subject: [PATCH] feat: social media --- .../src/api/routes/media.controller.ts | 13 ++--- .../src/services/auth/auth.middleware.ts | 1 - .../components/launches/add.edit.model.tsx | 2 +- .../launches/helpers/date.picker.tsx | 5 -- .../launches/helpers/linkedin.component.tsx | 1 - .../pinterest/pinterest.provider.tsx | 4 +- .../src/components/media/media.component.tsx | 8 ++- .../database/prisma/posts/posts.service.ts | 1 - .../integrations/social/pinterest.provider.ts | 58 +++++++++++++++---- .../src/upload/custom.upload.validation.ts | 46 +++++++++++++++ .../src/upload/upload.module.ts | 4 +- .../src/helpers/video.frame.tsx | 2 +- 12 files changed, 110 insertions(+), 35 deletions(-) create mode 100644 libraries/nestjs-libraries/src/upload/custom.upload.validation.ts diff --git a/apps/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts index 3b0d371d..a8f910b5 100644 --- a/apps/backend/src/api/routes/media.controller.ts +++ b/apps/backend/src/api/routes/media.controller.ts @@ -8,6 +8,7 @@ import { Query, UploadedFile, UseInterceptors, + UsePipes, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { Express } from 'express'; @@ -15,6 +16,7 @@ import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.reque import { Organization } from '@prisma/client'; import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service'; import { ApiTags } from '@nestjs/swagger'; +import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation'; @ApiTags('Media') @Controller('/media') @@ -22,17 +24,10 @@ export class MediaController { constructor(private _mediaService: MediaService) {} @Post('/') @UseInterceptors(FileInterceptor('file')) + @UsePipes(new CustomFileValidationPipe()) async uploadFile( @GetOrgFromRequest() org: Organization, - @UploadedFile( - 'file', - new ParseFilePipe({ - validators: [ - new MaxFileSizeValidator({ maxSize: 10 * 1024 * 1024 }), - new FileTypeValidator({ fileType: /^(image\/.+|video\/mp4)$/ }), - ], - }) - ) + @UploadedFile('file') file: Express.Multer.File ) { const filePath = diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index d2040774..cdf6aac6 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -65,7 +65,6 @@ export class AuthMiddleware implements NestMiddleware { } catch (err) { throw new Error('Unauthorized'); } - console.log('Request...'); next(); } } diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index b911eb6c..2a8c7b87 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -255,7 +255,7 @@ export const AddEditModal: FC<{ for (const key of allKeys) { if (key.checkValidity) { - const check = await key.checkValidity(key?.value.map((p: any) => p.image || {path: ''})); + const check = await key.checkValidity(key?.value.map((p: any) => p.image || [])); if (typeof check === 'string') { toaster.show(check, 'warning'); return; diff --git a/apps/frontend/src/components/launches/helpers/date.picker.tsx b/apps/frontend/src/components/launches/helpers/date.picker.tsx index d23bcd5f..52aa7f63 100644 --- a/apps/frontend/src/components/launches/helpers/date.picker.tsx +++ b/apps/frontend/src/components/launches/helpers/date.picker.tsx @@ -22,11 +22,6 @@ export const DatePicker: FC<{ const changeDate = useCallback( (type: 'date' | 'time') => (day: Date) => { - console.log( - type === 'time' - ? date.format('YYYY-MM-DD') + ' ' + dayjs(day).format('HH:mm:ss') - : dayjs(day).format('YYYY-MM-DD') + ' ' + date.format('HH:mm:ss') - ); onChange( dayjs( type === 'time' diff --git a/apps/frontend/src/components/launches/helpers/linkedin.component.tsx b/apps/frontend/src/components/launches/helpers/linkedin.component.tsx index d1b2691c..29e9bb74 100644 --- a/apps/frontend/src/components/launches/helpers/linkedin.component.tsx +++ b/apps/frontend/src/components/launches/helpers/linkedin.component.tsx @@ -181,7 +181,6 @@ export const linkedinCompany = (identifier: string, id: string): ICommand[] => { const state1 = api.setSelectionRange(newSelectionRange); const media = await showPostSelector(id); - console.log(media); executeCommand({ api, selectedText: state1.selectedText, diff --git a/apps/frontend/src/components/launches/providers/pinterest/pinterest.provider.tsx b/apps/frontend/src/components/launches/providers/pinterest/pinterest.provider.tsx index 7d94d5b2..135052bc 100644 --- a/apps/frontend/src/components/launches/providers/pinterest/pinterest.provider.tsx +++ b/apps/frontend/src/components/launches/providers/pinterest/pinterest.provider.tsx @@ -131,8 +131,8 @@ export default withProvider( PinterestPreview, PinterestSettingsDto, async ([firstItem, ...otherItems]) => { - const isMp4 = firstItem.find((item) => item.path.indexOf('mp4') > -1); - const isPicture = firstItem.find((item) => item.path.indexOf('mp4') === -1); + const isMp4 = firstItem?.find((item) => item.path.indexOf('mp4') > -1); + const isPicture = firstItem?.find((item) => item.path.indexOf('mp4') === -1); if (firstItem.length === 0) { return 'Pinterest requires at least one media'; diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index e2698b0e..2ae51e3c 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -67,12 +67,16 @@ export const MediaBox: FC<{ const uploadMedia = useCallback( async (file: ChangeEvent) => { - const maxFileSize = 10 * 1024 * 1024; + const maxFileSize = + (file?.target?.files?.[0].name.indexOf('mp4') || -1) > -1 + ? 100 * 1024 * 1024 + : 10 * 1024 * 1024; + if ( !file?.target?.files?.length || file?.target?.files?.[0]?.size > maxFileSize ) { - toaster.show('Maximum file size 10mb', 'warning'); + toaster.show(`Maximum file size ${maxFileSize / 1024 / 1024}mb`, 'warning'); return; } const formData = new FormData(); diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 6a346f6a..7bacf814 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -300,7 +300,6 @@ export class PostsService { integrationId: string ) { if (!(await this._messagesService.canAddPost(id, order, integrationId))) { - console.log('hello'); throw new Error('You can not add a post to this publication'); } const getOrgByOrder = await this._messagesService.getOrgByOrder(order); diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 36ad8673..415e64bb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -15,15 +15,43 @@ export class PinterestProvider implements SocialProvider { name = 'Pinterest'; isBetweenSteps = false; - async refreshToken(refresh_token: string): Promise { + async refreshToken(refreshToken: string): Promise { + const { access_token, expires_in } = await ( + await fetch('https://api-sandbox.pinterest.com/v5/oauth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + `${process.env.PINTEREST_CLIENT_ID}:${process.env.PINTEREST_CLIENT_SECRET}` + ).toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + scope: + 'boards:read,boards:write,pins:read,pins:write,user_accounts:read', + redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/pinterest`, + }), + }) + ).json(); + + const { id, profile_image, username } = await ( + await fetch('https://api-sandbox.pinterest.com/v5/user_account', { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + return { - refreshToken: '', - expiresIn: 0, - accessToken: '', - id: '', - name: '', - picture: '', - username: '', + id: id, + name: username, + accessToken: access_token, + refreshToken: refreshToken, + expiresIn: expires_in, + picture: profile_image, + username, }; } @@ -110,7 +138,14 @@ export class PinterestProvider implements SocialProvider { postDetails: PostDetails[] ): Promise { let mediaId = ''; - if ((postDetails?.[0]?.media?.[0]?.path?.indexOf('mp4') || -1) > -1) { + const findMp4 = postDetails?.[0]?.media?.find( + (p) => (p.path?.indexOf('mp4') || -1) > -1 + ); + const picture = postDetails?.[0]?.media?.find( + (p) => (p.path?.indexOf('mp4') || -1) === -1 + ); + + if (findMp4) { const { upload_url, media_id, upload_parameters } = await ( await fetch('https://api-sandbox.pinterest.com/v5/media', { method: 'POST', @@ -197,6 +232,7 @@ export class PinterestProvider implements SocialProvider { ? { source_type: 'video_id', media_id: mediaId, + cover_image_url: picture?.url, } : mapImages?.length === 1 ? { @@ -213,9 +249,9 @@ export class PinterestProvider implements SocialProvider { return [ { - id, + id: postDetails?.[0]?.id, postId: pId, - releaseURL: link, + releaseURL: `https://www.pinterest.com/pin/${pId}`, status: 'success', }, ]; diff --git a/libraries/nestjs-libraries/src/upload/custom.upload.validation.ts b/libraries/nestjs-libraries/src/upload/custom.upload.validation.ts new file mode 100644 index 00000000..add65c38 --- /dev/null +++ b/libraries/nestjs-libraries/src/upload/custom.upload.validation.ts @@ -0,0 +1,46 @@ +import { + BadRequestException, + FileTypeValidator, + Injectable, + MaxFileSizeValidator, + ParseFilePipe, + PipeTransform, +} from '@nestjs/common'; + +@Injectable() +export class CustomFileValidationPipe implements PipeTransform { + async transform(value: any) { + if (!value) { + throw 'No file provided.'; + } + + if (!value.mimetype) { + return value; + } + + // Set the maximum file size based on the MIME type + const maxSize = this.getMaxSize(value.mimetype); + const validation = + (value.mimetype.startsWith('image/') || + value.mimetype.startsWith('video/mp4')) && + value.size <= maxSize; + + if (validation) { + return value; + } + + throw new BadRequestException( + `File size exceeds the maximum allowed size of ${maxSize} bytes.` + ); + } + + private getMaxSize(mimeType: string): number { + if (mimeType.startsWith('image/')) { + return 10 * 1024 * 1024; // 10 MB + } else if (mimeType.startsWith('video/')) { + return 1024 * 1024 * 1024; // 1 GB + } else { + throw new BadRequestException('Unsupported file type.'); + } + } +} diff --git a/libraries/nestjs-libraries/src/upload/upload.module.ts b/libraries/nestjs-libraries/src/upload/upload.module.ts index 1c94dfa8..c74ec29e 100644 --- a/libraries/nestjs-libraries/src/upload/upload.module.ts +++ b/libraries/nestjs-libraries/src/upload/upload.module.ts @@ -4,6 +4,7 @@ import { diskStorage } from 'multer'; import { mkdirSync } from 'fs'; import { extname } from 'path'; import CloudflareStorage from '@gitroom/nestjs-libraries/upload/cloudflare.storage'; +import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation'; const storage = process.env.CLOUDFLARE_ACCOUNT_ID && @@ -51,8 +52,9 @@ const storage = storage, }), ], + providers: [CustomFileValidationPipe], get exports() { - return this.imports; + return [...this.imports, ...this.providers]; }, }) export class UploadModule {} diff --git a/libraries/react-shared-libraries/src/helpers/video.frame.tsx b/libraries/react-shared-libraries/src/helpers/video.frame.tsx index a7275660..37325d21 100644 --- a/libraries/react-shared-libraries/src/helpers/video.frame.tsx +++ b/libraries/react-shared-libraries/src/helpers/video.frame.tsx @@ -5,5 +5,5 @@ import { FC } from 'react'; export const VideoFrame: FC<{ url: string }> = (props) => { const { url } = props; - return ; + return ; };