Upload Workspace logo during onboarding (#542)
* Upload image * Upload image * Fix tests * Remove pictures from seeds * Fix storybook * Fix storybook * Fix storybook
This commit is contained in:
@ -55,6 +55,7 @@ export class AbilityFactory {
|
||||
|
||||
// Workspace
|
||||
can(AbilityAction.Read, 'Workspace', { id: workspace.id });
|
||||
can(AbilityAction.Update, 'Workspace', { id: workspace.id });
|
||||
|
||||
// Workspace Member
|
||||
can(AbilityAction.Read, 'WorkspaceMember', { userId: user.id });
|
||||
|
||||
@ -4,6 +4,7 @@ import { AppAbility } from '../ability.factory';
|
||||
import { IAbilityHandler } from '../interfaces/ability-handler.interface';
|
||||
import {
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
@ -11,6 +12,7 @@ import { subject } from '@casl/ability';
|
||||
import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { getRequest } from 'src/utils/extract-request';
|
||||
|
||||
class WorksapceArgs {
|
||||
where?: WorkspaceWhereInput;
|
||||
@ -42,10 +44,11 @@ export class UpdateWorkspaceAbilityHandler implements IAbilityHandler {
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||
const gqlContext = GqlExecutionContext.create(context);
|
||||
const args = gqlContext.getArgs<WorksapceArgs>();
|
||||
const workspace = await this.prismaService.workspace.findFirst({
|
||||
where: args.where,
|
||||
const request = getRequest(context);
|
||||
assert(request.user.workspace.id, '', ForbiddenException);
|
||||
|
||||
const workspace = await this.prismaService.workspace.findUnique({
|
||||
where: { id: request.user.workspace.id },
|
||||
});
|
||||
assert(workspace, '', NotFoundException);
|
||||
|
||||
|
||||
@ -3,7 +3,8 @@ import { Settings } from './interfaces/settings.interface';
|
||||
export const settings: Settings = {
|
||||
storage: {
|
||||
imageCropSizes: {
|
||||
profilePicture: ['original'],
|
||||
'profile-picture': ['original'],
|
||||
'workspace-logo': ['original'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -4,7 +4,6 @@ import { KebabCase } from 'type-fest';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { basename } from 'path';
|
||||
import { settings } from 'src/constants/settings';
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
|
||||
type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
||||
|
||||
@ -20,10 +19,7 @@ export function checkFilePath(filePath: string): string {
|
||||
throw new BadRequestException(`Folder ${folder} is not allowed`);
|
||||
}
|
||||
|
||||
if (
|
||||
size &&
|
||||
!settings.storage.imageCropSizes[camelCase(folder)]?.includes(size)
|
||||
) {
|
||||
if (size && !settings.storage.imageCropSizes[folder]?.includes(size)) {
|
||||
throw new BadRequestException(`Size ${size} is not allowed`);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum FileFolder {
|
||||
ProfilePicture = 'profilePicture',
|
||||
ProfilePicture = 'profile-picture',
|
||||
WorkspaceLogo = 'workspace-logo',
|
||||
}
|
||||
|
||||
registerEnumType(FileFolder, {
|
||||
|
||||
@ -91,7 +91,7 @@ export class FileUploadService {
|
||||
images.map(async (image, index) => {
|
||||
const buffer = await image.toBuffer();
|
||||
|
||||
paths.push(`profile-picture/${cropSizes[index]}/${name}`);
|
||||
paths.push(`${fileFolder}/${cropSizes[index]}/${name}`);
|
||||
|
||||
return this.uploadFile({
|
||||
file: buffer,
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { WorkspaceResolver } from './workspace.resolver';
|
||||
import { WorkspaceService } from '../services/workspace.service';
|
||||
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||
|
||||
describe('WorkspaceMemberResolver', () => {
|
||||
describe('WorkspaceResolver', () => {
|
||||
let resolver: WorkspaceResolver;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -10,6 +12,8 @@ describe('WorkspaceMemberResolver', () => {
|
||||
providers: [
|
||||
WorkspaceResolver,
|
||||
{ provide: WorkspaceService, useValue: {} },
|
||||
{ provide: AbilityFactory, useValue: {} },
|
||||
{ provide: FileUploadService, useValue: {} },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -11,13 +11,25 @@ import { WorkspaceUpdateInput } from 'src/core/@generated/workspace/workspace-up
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
|
||||
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||
import { UpdateWorkspaceAbilityHandler } from 'src/ability/handlers/workspace.ability-handler';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => Workspace)
|
||||
export class WorkspaceResolver {
|
||||
constructor(private readonly workspaceService: WorkspaceService) {}
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => Workspace)
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(UpdateWorkspaceAbilityHandler)
|
||||
async updateWorkspace(
|
||||
@Args('data') data: WorkspaceUpdateInput,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@ -51,4 +63,33 @@ export class WorkspaceResolver {
|
||||
|
||||
return selectedWorkspace;
|
||||
}
|
||||
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(UpdateWorkspaceAbilityHandler)
|
||||
@Mutation(() => String)
|
||||
async uploadWorkspaceLogo(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
): Promise<string> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const fileFolder = FileFolder.WorkspaceLogo;
|
||||
|
||||
const { paths } = await this.fileUploadService.uploadImage({
|
||||
file: buffer,
|
||||
filename,
|
||||
mimeType: mimetype,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
await this.workspaceService.update({
|
||||
where: { id: workspace.id },
|
||||
data: {
|
||||
logo: paths[0],
|
||||
},
|
||||
});
|
||||
|
||||
return paths[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,12 @@ import { WorkspaceService } from './services/workspace.service';
|
||||
import { WorkspaceMemberService } from './services/workspace-member.service';
|
||||
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
||||
import { WorkspaceResolver } from './resolvers/workspace.resolver';
|
||||
import { FileUploadService } from '../file/services/file-upload.service';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
WorkspaceService,
|
||||
FileUploadService,
|
||||
WorkspaceMemberService,
|
||||
WorkspaceMemberResolver,
|
||||
WorkspaceResolver,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,7 +3,6 @@ import { createReadStream, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||
import { Readable } from 'stream';
|
||||
import { kebabCase } from 'src/utils/kebab-case';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
storagePath: string;
|
||||
@ -32,7 +31,7 @@ export class LocalDriver implements StorageDriver {
|
||||
}): Promise<void> {
|
||||
const filePath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
kebabCase(params.folder),
|
||||
params.folder,
|
||||
params.name,
|
||||
);
|
||||
const folderPath = dirname(filePath);
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||
import { Readable } from 'stream';
|
||||
import { kebabCase } from 'src/utils/kebab-case';
|
||||
|
||||
export interface S3DriverOptions extends S3ClientConfig {
|
||||
bucketName: string;
|
||||
@ -42,7 +41,7 @@ export class S3Driver implements StorageDriver {
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const command = new PutObjectCommand({
|
||||
Key: `${kebabCase(params.folder)}/${params.name}`,
|
||||
Key: `${params.folder}/${params.name}`,
|
||||
Body: params.file,
|
||||
ContentType: params.mimeType,
|
||||
Bucket: this.bucketName,
|
||||
|
||||
Reference in New Issue
Block a user