Connect profile picture upload to backend (#533)
* Connect profile picture upload to backend * Fix tests * Revert onboarding state changes
This commit is contained in:
@ -29,6 +29,7 @@ import GraphQLJSON from 'graphql-type-json';
|
||||
error.extensions.stacktrace = undefined;
|
||||
return error;
|
||||
},
|
||||
csrfPrevention: false,
|
||||
}),
|
||||
PrismaModule,
|
||||
HealthModule,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { GraphQLUpload, FileUpload } from 'graphql-upload';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { FileUploadService } from '../services/file-upload.service';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
@ -21,18 +20,15 @@ export class FileUploadResolver {
|
||||
): Promise<string> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const ext = filename.split('.')?.[1];
|
||||
const id = uuidV4();
|
||||
const name = `${id}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
const path = await this.fileUploadService.uploadFile({
|
||||
const { path } = await this.fileUploadService.uploadFile({
|
||||
file: buffer,
|
||||
name,
|
||||
filename,
|
||||
mimeType: mimetype,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
return path.name;
|
||||
return path;
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
@ -44,17 +40,14 @@ export class FileUploadResolver {
|
||||
): Promise<string> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const ext = filename.split('.')?.[1];
|
||||
const id = uuidV4();
|
||||
const name = `${id}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
const path = await this.fileUploadService.uploadImage({
|
||||
const { paths } = await this.fileUploadService.uploadImage({
|
||||
file: buffer,
|
||||
name,
|
||||
filename,
|
||||
mimeType: mimetype,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
return path.name;
|
||||
return paths[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,45 +4,73 @@ import { getCropSize } from 'src/utils/image';
|
||||
import { settings } from 'src/constants/settings';
|
||||
import { FileFolder } from '../interfaces/file-folder.interface';
|
||||
import { FileStorageService } from 'src/integrations/file-storage/file-storage.service';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class FileUploadService {
|
||||
constructor(private readonly fileStorage: FileStorageService) {}
|
||||
|
||||
async uploadFile({
|
||||
private async _uploadFile({
|
||||
file,
|
||||
name,
|
||||
filename,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
}: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
filename: string;
|
||||
mimeType: string | undefined;
|
||||
fileFolder: FileFolder;
|
||||
}) {
|
||||
await this.fileStorage.write({
|
||||
file,
|
||||
name,
|
||||
name: filename,
|
||||
mimeType,
|
||||
folder: fileFolder,
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile({
|
||||
file,
|
||||
filename,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
}: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
filename: string;
|
||||
mimeType: string | undefined;
|
||||
fileFolder: FileFolder;
|
||||
}) {
|
||||
const ext = filename.split('.')?.[1];
|
||||
const id = uuidV4();
|
||||
const name = `${id}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
await this._uploadFile({
|
||||
file,
|
||||
filename,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
return {
|
||||
name: `/${name}`,
|
||||
path: `${fileFolder}/${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadImage({
|
||||
file,
|
||||
name,
|
||||
filename,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
}: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
filename: string;
|
||||
mimeType: string | undefined;
|
||||
fileFolder: FileFolder;
|
||||
}) {
|
||||
const ext = filename.split('.')?.[1];
|
||||
const id = uuidV4();
|
||||
const name = `${id}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
// Get all cropSizes for this fileFolder
|
||||
const cropSizes = settings.storage.imageCropSizes[fileFolder];
|
||||
// Extract the values from ShortCropSize
|
||||
@ -56,14 +84,18 @@ export class FileUploadService {
|
||||
),
|
||||
);
|
||||
|
||||
const paths: Array<string> = [];
|
||||
|
||||
// Upload all images to corresponding folders
|
||||
await Promise.all(
|
||||
images.map(async (image, index) => {
|
||||
const buffer = await image.toBuffer();
|
||||
|
||||
paths.push(`profile-picture/${cropSizes[index]}/${name}`);
|
||||
|
||||
return this.uploadFile({
|
||||
file: buffer,
|
||||
name: `${cropSizes[index]}/${name}`,
|
||||
filename: `${cropSizes[index]}/${name}`,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
});
|
||||
@ -71,7 +103,7 @@ export class FileUploadService {
|
||||
);
|
||||
|
||||
return {
|
||||
name: `/${name}`,
|
||||
paths,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserResolver } from './user.resolver';
|
||||
import { FileModule } from '../file/file.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [FileModule],
|
||||
providers: [UserService, UserResolver],
|
||||
exports: [UserService],
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserResolver } from './user.resolver';
|
||||
import { UserService } from './user.service';
|
||||
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||
import { FileUploadService } from '../file/services/file-upload.service';
|
||||
|
||||
describe('UserResolver', () => {
|
||||
let resolver: UserResolver;
|
||||
@ -18,6 +19,10 @@ describe('UserResolver', () => {
|
||||
provide: AbilityFactory,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: FileUploadService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -29,11 +29,18 @@ import { AuthUser } from 'src/decorators/auth-user.decorator';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { UpdateOneUserArgs } from '../@generated/user/update-one-user.args';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
import { FileUploadService } from '../file/services/file-upload.service';
|
||||
import { FileFolder } from '../file/interfaces/file-folder.interface';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => User)
|
||||
export class UserResolver {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
async currentUser(
|
||||
@ -109,4 +116,31 @@ export class UserResolver {
|
||||
displayName(@Parent() parent: User): string {
|
||||
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async uploadProfilePicture(
|
||||
@AuthUser() { id }: User,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
): Promise<string> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const fileFolder = FileFolder.ProfilePicture;
|
||||
|
||||
const { paths } = await this.fileUploadService.uploadImage({
|
||||
file: buffer,
|
||||
filename,
|
||||
mimeType: mimetype,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
await this.userService.update({
|
||||
where: { id },
|
||||
data: {
|
||||
avatarUrl: paths[0],
|
||||
},
|
||||
});
|
||||
|
||||
return paths[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@ import { AppModule } from './app.module';
|
||||
import { graphqlUploadExpress } from 'graphql-upload';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { cors: true });
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
cors: true,
|
||||
});
|
||||
|
||||
// Apply validation pipes globally
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
Reference in New Issue
Block a user