switch to nx monorepo

This commit is contained in:
Philipinho
2024-01-09 18:58:26 +01:00
parent e1bb2632b8
commit 093e634c0b
273 changed files with 11419 additions and 31 deletions

View File

@ -0,0 +1,110 @@
import {
BadRequestException,
Controller,
HttpCode,
HttpStatus,
Post,
Req,
Res,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AttachmentService } from './attachment.service';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AttachmentInterceptor } from './attachment.interceptor';
import { JwtUser } from '../../decorators/jwt-user.decorator';
import { JwtGuard } from '../auth/guards/JwtGuard';
import * as bytes from 'bytes';
@Controller('attachments')
export class AttachmentController {
constructor(private readonly attachmentService: AttachmentService) {}
@UseGuards(JwtGuard)
@HttpCode(HttpStatus.CREATED)
@Post('upload/avatar')
@UseInterceptors(AttachmentInterceptor)
async uploadAvatar(
@JwtUser() jwtUser,
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
) {
const maxFileSize = bytes('5MB');
try {
const file = req.file({
limits: { fileSize: maxFileSize, fields: 1, files: 1 },
});
const fileResponse = await this.attachmentService.uploadAvatar(
file,
jwtUser.id,
);
return res.send(fileResponse);
} catch (err) {
throw new BadRequestException('Error processing file upload.');
}
}
@UseGuards(JwtGuard)
@HttpCode(HttpStatus.CREATED)
@Post('upload/workspace-logo')
@UseInterceptors(AttachmentInterceptor)
async uploadWorkspaceLogo(
@JwtUser() jwtUser,
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
) {
const maxFileSize = bytes('5MB');
try {
const file = req.file({
limits: { fileSize: maxFileSize, fields: 1, files: 1 },
});
// TODO FIX
const workspaceId = '123';
const fileResponse = await this.attachmentService.uploadWorkspaceLogo(
file,
workspaceId,
jwtUser.id,
);
return res.send(fileResponse);
} catch (err) {
throw new BadRequestException('Error processing file upload.');
}
}
@UseGuards(JwtGuard)
@HttpCode(HttpStatus.CREATED)
@Post('upload/file')
@UseInterceptors(AttachmentInterceptor)
async uploadFile(
@JwtUser() jwtUser,
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
) {
const maxFileSize = bytes('20MB');
try {
const file = req.file({
limits: { fileSize: maxFileSize, fields: 1, files: 1 },
});
const workspaceId = '123';
const fileResponse = await this.attachmentService.uploadWorkspaceLogo(
file,
workspaceId,
jwtUser.id,
);
return res.send(fileResponse);
} catch (err) {
throw new BadRequestException('Error processing file upload.');
}
}
}

View File

@ -0,0 +1,25 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
BadRequestException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { FastifyRequest } from 'fastify';
@Injectable()
export class AttachmentInterceptor implements NestInterceptor {
public intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<any> {
const req: FastifyRequest = context.switchToHttp().getRequest();
if (!req.isMultipart() || !req.file) {
throw new BadRequestException('Invalid multipart content type');
}
return next.handle();
}
}

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { AttachmentService } from './attachment.service';
import { AttachmentController } from './attachment.controller';
import { StorageModule } from '../storage/storage.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attachment } from './entities/attachment.entity';
import { AttachmentRepository } from './repositories/attachment.repository';
import { AuthModule } from '../auth/auth.module';
import { UserModule } from '../user/user.module';
import { WorkspaceModule } from '../workspace/workspace.module';
@Module({
imports: [
TypeOrmModule.forFeature([Attachment]),
StorageModule,
AuthModule,
UserModule,
WorkspaceModule,
],
controllers: [AttachmentController],
providers: [AttachmentService, AttachmentRepository],
})
export class AttachmentModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AttachmentService } from './attachment.service';
describe('AttachmentService', () => {
let service: AttachmentService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AttachmentService],
}).compile();
service = module.get<AttachmentService>(AttachmentService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,164 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { StorageService } from '../storage/storage.service';
import { MultipartFile } from '@fastify/multipart';
import { AttachmentRepository } from './repositories/attachment.repository';
import { Attachment } from './entities/attachment.entity';
import { UserService } from '../user/user.service';
import { UpdateUserDto } from '../user/dto/update-user.dto';
import {
AttachmentType,
getAttachmentPath,
PreparedFile,
prepareFile,
validateFileType,
} from './attachment.utils';
import { v4 as uuid4 } from 'uuid';
import { WorkspaceService } from '../workspace/services/workspace.service';
import { UpdateWorkspaceDto } from '../workspace/dto/update-workspace.dto';
@Injectable()
export class AttachmentService {
constructor(
private readonly storageService: StorageService,
private readonly attachmentRepo: AttachmentRepository,
private readonly workspaceService: WorkspaceService,
private readonly userService: UserService,
) {}
async uploadToDrive(preparedFile: PreparedFile, filePath: string) {
try {
await this.storageService.upload(filePath, preparedFile.buffer);
} catch (err) {
console.error('Error uploading file to drive:', err);
throw new BadRequestException('Error uploading file to drive');
}
}
async updateUserAvatar(userId: string, avatarUrl: string) {
const updateUserDto = new UpdateUserDto();
updateUserDto.avatarUrl = avatarUrl;
await this.userService.update(userId, updateUserDto);
}
async updateWorkspaceLogo(workspaceId: string, logoUrl: string) {
const updateWorkspaceDto = new UpdateWorkspaceDto();
updateWorkspaceDto.logo = logoUrl;
await this.workspaceService.update(workspaceId, updateWorkspaceDto);
}
async uploadAvatar(filePromise: Promise<MultipartFile>, userId: string) {
try {
const preparedFile: PreparedFile = await prepareFile(filePromise);
const allowedImageTypes = ['.jpg', '.jpeg', '.png'];
validateFileType(preparedFile.fileExtension, allowedImageTypes);
preparedFile.fileName = uuid4() + preparedFile.fileExtension;
const attachmentPath = getAttachmentPath(AttachmentType.Avatar);
const filePath = `${attachmentPath}/${preparedFile.fileName}`;
await this.uploadToDrive(preparedFile, filePath);
const attachment = new Attachment();
attachment.creatorId = userId;
attachment.pageId = null;
attachment.workspaceId = null;
attachment.type = AttachmentType.Avatar;
attachment.filePath = filePath;
attachment.fileName = preparedFile.fileName;
attachment.fileSize = preparedFile.fileSize;
attachment.mimeType = preparedFile.mimeType;
attachment.fileExt = preparedFile.fileExtension;
await this.updateUserAvatar(userId, filePath);
return attachment;
} catch (err) {
console.log(err);
throw new BadRequestException(err.message);
}
}
async uploadWorkspaceLogo(
filePromise: Promise<MultipartFile>,
workspaceId: string,
userId: string,
) {
try {
const preparedFile: PreparedFile = await prepareFile(filePromise);
const allowedImageTypes = ['.jpg', '.jpeg', '.png'];
validateFileType(preparedFile.fileExtension, allowedImageTypes);
preparedFile.fileName = uuid4() + preparedFile.fileExtension;
const attachmentPath = getAttachmentPath(
AttachmentType.WorkspaceLogo,
workspaceId,
);
const filePath = `${attachmentPath}/${preparedFile.fileName}`;
await this.uploadToDrive(preparedFile, filePath);
const attachment = new Attachment();
attachment.creatorId = userId;
attachment.pageId = null;
attachment.workspaceId = workspaceId;
attachment.type = AttachmentType.WorkspaceLogo;
attachment.filePath = filePath;
attachment.fileName = preparedFile.fileName;
attachment.fileSize = preparedFile.fileSize;
attachment.mimeType = preparedFile.mimeType;
attachment.fileExt = preparedFile.fileExtension;
await this.updateWorkspaceLogo(workspaceId, filePath);
return attachment;
} catch (err) {
console.log(err);
throw new BadRequestException(err.message);
}
}
async uploadFile(
filePromise: Promise<MultipartFile>,
pageId: string,
workspaceId: string,
userId: string,
) {
try {
const preparedFile: PreparedFile = await prepareFile(filePromise);
const allowedImageTypes = ['.jpg', '.jpeg', '.png', '.pdf'];
validateFileType(preparedFile.fileExtension, allowedImageTypes);
const attachmentPath = getAttachmentPath(
AttachmentType.WorkspaceLogo,
workspaceId,
);
const filePath = `${attachmentPath}/${preparedFile.fileName}`;
await this.uploadToDrive(preparedFile, filePath);
const attachment = new Attachment();
attachment.creatorId = userId;
attachment.pageId = pageId;
attachment.workspaceId = workspaceId;
attachment.type = AttachmentType.WorkspaceLogo;
attachment.filePath = filePath;
attachment.fileName = preparedFile.fileName;
attachment.fileSize = preparedFile.fileSize;
attachment.mimeType = preparedFile.mimeType;
attachment.fileExt = preparedFile.fileExtension;
return attachment;
} catch (err) {
console.log(err);
throw new BadRequestException(err.message);
}
}
}

View File

@ -0,0 +1,75 @@
import { MultipartFile } from '@fastify/multipart';
import { randomBytes } from 'crypto';
import { sanitize } from 'sanitize-filename-ts';
import * as path from 'path';
export interface PreparedFile {
buffer: Buffer;
fileName: string;
fileSize: number;
fileExtension: string;
mimeType: string;
}
export async function prepareFile(
filePromise: Promise<MultipartFile>,
): Promise<PreparedFile> {
try {
const rand = randomBytes(4).toString('hex');
const file = await filePromise;
if (!file) {
throw new Error('No file provided');
}
const buffer = await file.toBuffer();
const sanitizedFilename = sanitize(file.filename).replace(/ /g, '_');
const fileName = `${rand}_${sanitizedFilename}`;
const fileSize = buffer.length;
const fileExtension = path.extname(file.filename).toLowerCase();
return {
buffer,
fileName,
fileSize,
fileExtension,
mimeType: file.mimetype,
};
} catch (error) {
console.error('Error in file preparation:', error);
throw error;
}
}
export function validateFileType(
fileExtension: string,
allowedTypes: string[],
) {
if (!allowedTypes.includes(fileExtension)) {
throw new Error('Invalid file type');
}
}
export enum AttachmentType {
Avatar = 'Avatar',
WorkspaceLogo = 'WorkspaceLogo',
File = 'file',
}
export function getAttachmentPath(
type: AttachmentType,
workspaceId?: string,
): string {
if (!workspaceId && type != AttachmentType.Avatar) {
throw new Error('Workspace ID is required for this attachment type');
}
switch (type) {
case AttachmentType.Avatar:
return 'avatars';
case AttachmentType.WorkspaceLogo:
return `${workspaceId}/logo`;
default:
return `${workspaceId}/files`;
}
}

View File

@ -0,0 +1,5 @@
import { IsOptional, IsString, IsUUID } from 'class-validator';
export class AvatarUploadDto {
}

View File

@ -0,0 +1,65 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
DeleteDateColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Page } from '../../page/entities/page.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
@Entity('attachments')
export class Attachment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255 })
fileName: string;
@Column({ type: 'varchar' })
filePath: string;
@Column({ type: 'bigint' })
fileSize: number;
@Column({ type: 'varchar', length: 55 })
fileExt: string;
@Column({ type: 'varchar', length: 255 })
mimeType: string;
@Column({ type: 'varchar', length: 55 })
type: string; // e.g. page / workspace / avatar
@Column()
creatorId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' })
creator: User;
@Column({ nullable: true })
pageId: string;
@ManyToOne(() => Page)
@JoinColumn({ name: 'pageId' })
page: Page;
@Column({ nullable: true })
workspaceId: string;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@CreateDateColumn()
createdAt: Date;
@DeleteDateColumn({ nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,14 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { Attachment } from '../entities/attachment.entity';
@Injectable()
export class AttachmentRepository extends Repository<Attachment> {
constructor(private dataSource: DataSource) {
super(Attachment, dataSource.createEntityManager());
}
async findById(id: string) {
return this.findOneBy({ id: id });
}
}