diff --git a/client/src/components/ui/user-avatar.tsx b/client/src/components/ui/user-avatar.tsx index 8a9f5c2..a7e5c79 100644 --- a/client/src/components/ui/user-avatar.tsx +++ b/client/src/components/ui/user-avatar.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { Avatar } from '@mantine/core'; -interface UserAvatarProps extends React.ComponentProps { +interface UserAvatarProps { avatarUrl: string; name: string; color?: string; size?: string; + radius?: string; + style?: any; + component?: any; } export const UserAvatar = React.forwardRef( diff --git a/client/src/features/home/components/recent-changes.tsx b/client/src/features/home/components/recent-changes.tsx index 0d51a24..17c3ec4 100644 --- a/client/src/features/home/components/recent-changes.tsx +++ b/client/src/features/home/components/recent-changes.tsx @@ -18,20 +18,21 @@ function RecentChanges() { return (
- {data.map((page) => ( + {data + .map((page) => (
- + {page.title || 'Untitled'} - - {format(new Date(page.createdAt), 'PPP')} + + {format(new Date(page.updatedAt), 'PP')} diff --git a/client/src/features/settings/account/settings/account-settings.tsx b/client/src/features/settings/account/settings/account-settings.tsx index f10b742..fd133d1 100644 --- a/client/src/features/settings/account/settings/account-settings.tsx +++ b/client/src/features/settings/account/settings/account-settings.tsx @@ -3,11 +3,15 @@ import AccountNameForm from '@/features/settings/account/settings/components/acc import ChangeEmail from '@/features/settings/account/settings/components/change-email'; import ChangePassword from '@/features/settings/account/settings/components/change-password'; import { Divider } from '@mantine/core'; +import AccountAvatar from '@/features/settings/account/settings/components/account-avatar'; export default function AccountSettings() { return ( <> + + + diff --git a/client/src/features/settings/account/settings/components/account-avatar.tsx b/client/src/features/settings/account/settings/components/account-avatar.tsx new file mode 100644 index 0000000..89e8087 --- /dev/null +++ b/client/src/features/settings/account/settings/components/account-avatar.tsx @@ -0,0 +1,64 @@ +import { focusAtom } from 'jotai-optics'; +import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; +import { useState } from 'react'; +import { useAtom } from 'jotai'; +import { UserAvatar } from '@/components/ui/user-avatar'; +import { FileButton, Button, Text, Popover, Tooltip } from '@mantine/core'; +import { uploadAvatar } from '@/features/user/services/user-service'; + +const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop('user')); + +export default function AccountAvatar() { + const [isLoading, setIsLoading] = useState(false); + const [currentUser] = useAtom(currentUserAtom); + const [, setUser] = useAtom(userAtom); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + + const handleFileChange = async (selectedFile: File) => { + if (!selectedFile) { + return; + } + + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + + setFile(selectedFile); + setPreviewUrl(URL.createObjectURL(selectedFile)); + + try { + setIsLoading(true); + const upload = await uploadAvatar(selectedFile); + console.log(upload); + } catch (err) { + console.log(err); + } finally { + setIsLoading(false); + } + }; + + + return ( + <> + + {(props) => ( + + + + )} + + + + ); +} diff --git a/client/src/features/settings/account/settings/components/change-password.tsx b/client/src/features/settings/account/settings/components/change-password.tsx index 3272f42..30534f6 100644 --- a/client/src/features/settings/account/settings/components/change-password.tsx +++ b/client/src/features/settings/account/settings/components/change-password.tsx @@ -63,15 +63,15 @@ function ChangePasswordForm() { { return req.data as ICurrentUserResponse; } -export async function updateUser(data: Partial) { +export async function updateUser(data: Partial): Promise { const req = await api.post('/user/update', data); return req.data as IUser; } + +export async function uploadAvatar(file: File) { + const formData = new FormData(); + formData.append('avatar', file); + const req = await api.post('/attachments/upload/avatar', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + } + }); + return req.data; +} + diff --git a/server/.local/avatars/60a2ccc3-edfd-4053-a2c0-765d40a2c49f.png b/server/.local/avatars/60a2ccc3-edfd-4053-a2c0-765d40a2c49f.png new file mode 100644 index 0000000..eb21b36 Binary files /dev/null and b/server/.local/avatars/60a2ccc3-edfd-4053-a2c0-765d40a2c49f.png differ diff --git a/server/package.json b/server/package.json index 61ff7b0..d35bb69 100644 --- a/server/package.json +++ b/server/package.json @@ -43,6 +43,7 @@ "@nestjs/typeorm": "^10.0.1", "@nestjs/websockets": "^10.2.10", "bcrypt": "^5.1.1", + "bytes": "^3.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "fastify": "^4.24.3", @@ -63,6 +64,7 @@ "@nestjs/schematics": "^10.0.3", "@nestjs/testing": "^10.2.10", "@types/bcrypt": "^5.0.2", + "@types/bytes": "^3.1.4", "@types/debounce": "^1.2.4", "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.10", diff --git a/server/src/core/attachment/attachment.controller.ts b/server/src/core/attachment/attachment.controller.ts new file mode 100644 index 0000000..a2c5f80 --- /dev/null +++ b/server/src/core/attachment/attachment.controller.ts @@ -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.'); + } + } +} diff --git a/server/src/core/attachment/attachment.interceptor.ts b/server/src/core/attachment/attachment.interceptor.ts new file mode 100644 index 0000000..548c083 --- /dev/null +++ b/server/src/core/attachment/attachment.interceptor.ts @@ -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 { + const req: FastifyRequest = context.switchToHttp().getRequest(); + + if (!req.isMultipart() || !req.file) { + throw new BadRequestException('Invalid multipart content type'); + } + + return next.handle(); + } +} diff --git a/server/src/core/attachment/attachment.module.ts b/server/src/core/attachment/attachment.module.ts index 856f906..8a7906c 100644 --- a/server/src/core/attachment/attachment.module.ts +++ b/server/src/core/attachment/attachment.module.ts @@ -1,7 +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({ - providers: [AttachmentService], + imports: [ + TypeOrmModule.forFeature([Attachment]), + StorageModule, + AuthModule, + UserModule, + WorkspaceModule, + ], + controllers: [AttachmentController], + providers: [AttachmentService, AttachmentRepository], }) export class AttachmentModule {} diff --git a/server/src/core/attachment/attachment.service.ts b/server/src/core/attachment/attachment.service.ts index 7834c7b..b50c16f 100644 --- a/server/src/core/attachment/attachment.service.ts +++ b/server/src/core/attachment/attachment.service.ts @@ -1,4 +1,164 @@ -import { Injectable } from '@nestjs/common'; +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 {} +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, 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, + 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, + 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); + } + } +} diff --git a/server/src/core/attachment/attachment.utils.ts b/server/src/core/attachment/attachment.utils.ts new file mode 100644 index 0000000..a768aaa --- /dev/null +++ b/server/src/core/attachment/attachment.utils.ts @@ -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, +): Promise { + 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`; + } +} diff --git a/server/src/core/attachment/dto/avatar-upload.dto.ts b/server/src/core/attachment/dto/avatar-upload.dto.ts new file mode 100644 index 0000000..40f3609 --- /dev/null +++ b/server/src/core/attachment/dto/avatar-upload.dto.ts @@ -0,0 +1,5 @@ +import { IsOptional, IsString, IsUUID } from 'class-validator'; + +export class AvatarUploadDto { + +} diff --git a/server/src/core/attachment/dto/page-attachment-upload.dto.ts b/server/src/core/attachment/dto/page-attachment-upload.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/src/core/attachment/entities/attachment.entity.ts b/server/src/core/attachment/entities/attachment.entity.ts new file mode 100644 index 0000000..0a26b59 --- /dev/null +++ b/server/src/core/attachment/entities/attachment.entity.ts @@ -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; +} diff --git a/server/src/core/attachment/repositories/attachment.repository.ts b/server/src/core/attachment/repositories/attachment.repository.ts new file mode 100644 index 0000000..0f3a4b3 --- /dev/null +++ b/server/src/core/attachment/repositories/attachment.repository.ts @@ -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 { + constructor(private dataSource: DataSource) { + super(Attachment, dataSource.createEntityManager()); + } + + async findById(id: string) { + return this.findOneBy({ id: id }); + } +} diff --git a/server/src/core/auth/services/token.service.ts b/server/src/core/auth/services/token.service.ts index 8e0dc3e..aeb1b13 100644 --- a/server/src/core/auth/services/token.service.ts +++ b/server/src/core/auth/services/token.service.ts @@ -5,6 +5,8 @@ import { User } from '../../user/entities/user.entity'; import { FastifyRequest } from 'fastify'; import { TokensDto } from '../dto/tokens.dto'; +export type JwtPayload = { sub: string; email: string }; + @Injectable() export class TokenService { constructor( @@ -12,7 +14,7 @@ export class TokenService { private environmentService: EnvironmentService, ) {} async generateJwt(user: User): Promise { - const payload = { + const payload: JwtPayload = { sub: user.id, email: user.email, }; diff --git a/server/src/core/page/page.controller.ts b/server/src/core/page/page.controller.ts index 815a096..5e810c5 100644 --- a/server/src/core/page/page.controller.ts +++ b/server/src/core/page/page.controller.ts @@ -2,7 +2,6 @@ import { Controller, Post, Body, - Req, HttpCode, HttpStatus, UseGuards, @@ -10,7 +9,6 @@ import { import { PageService } from './services/page.service'; import { CreatePageDto } from './dto/create-page.dto'; import { UpdatePageDto } from './dto/update-page.dto'; -import { FastifyRequest } from 'fastify'; import { JwtGuard } from '../auth/guards/JwtGuard'; import { WorkspaceService } from '../workspace/services/workspace.service'; import { MovePageDto } from './dto/move-page.dto'; @@ -20,6 +18,7 @@ import { PageOrderingService } from './services/page-ordering.service'; import { PageHistoryService } from './services/page-history.service'; import { HistoryDetailsDto } from './dto/history-details.dto'; import { PageHistoryDto } from './dto/page-history.dto'; +import { JwtUser } from '../../decorators/jwt-user.decorator'; @UseGuards(JwtGuard) @Controller('pages') @@ -39,29 +38,17 @@ export class PageController { @HttpCode(HttpStatus.CREATED) @Post('create') - async create( - @Req() req: FastifyRequest, - @Body() createPageDto: CreatePageDto, - ) { - const jwtPayload = req['user']; - const userId = jwtPayload.sub; - + async create(@JwtUser() jwtUser, @Body() createPageDto: CreatePageDto) { const workspaceId = ( - await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + await this.workspaceService.getUserCurrentWorkspace(jwtUser.id) ).id; - return this.pageService.create(userId, workspaceId, createPageDto); + return this.pageService.create(jwtUser.id, workspaceId, createPageDto); } @HttpCode(HttpStatus.OK) @Post('update') - async update( - @Req() req: FastifyRequest, - @Body() updatePageDto: UpdatePageDto, - ) { - const jwtPayload = req['user']; - const userId = jwtPayload.sub; - - return this.pageService.update(updatePageDto.id, updatePageDto, userId); + async update(@JwtUser() jwtUser, @Body() updatePageDto: UpdatePageDto) { + return this.pageService.update(updatePageDto.id, updatePageDto, jwtUser.id); } @HttpCode(HttpStatus.OK) @@ -84,40 +71,36 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('recent') - async getRecentWorkspacePages(@Req() req: FastifyRequest) { - const jwtPayload = req['user']; + async getRecentWorkspacePages(@JwtUser() jwtUser) { const workspaceId = ( - await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + await this.workspaceService.getUserCurrentWorkspace(jwtUser.id) ).id; return this.pageService.getRecentWorkspacePages(workspaceId); } @HttpCode(HttpStatus.OK) @Post() - async getWorkspacePages(@Req() req: FastifyRequest) { - const jwtPayload = req['user']; + async getWorkspacePages(@JwtUser() jwtUser) { const workspaceId = ( - await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + await this.workspaceService.getUserCurrentWorkspace(jwtUser.id) ).id; return this.pageService.getSidebarPagesByWorkspaceId(workspaceId); } @HttpCode(HttpStatus.OK) @Post('ordering') - async getWorkspacePageOrder(@Req() req: FastifyRequest) { - const jwtPayload = req['user']; + async getWorkspacePageOrder(@JwtUser() jwtUser) { const workspaceId = ( - await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + await this.workspaceService.getUserCurrentWorkspace(jwtUser.id) ).id; return this.pageOrderService.getWorkspacePageOrder(workspaceId); } @HttpCode(HttpStatus.OK) @Post('tree') - async workspacePageTree(@Req() req: FastifyRequest) { - const jwtPayload = req['user']; + async workspacePageTree(@JwtUser() jwtUser) { const workspaceId = ( - await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + await this.workspaceService.getUserCurrentWorkspace(jwtUser.id) ).id; return this.pageOrderService.convertToTree(workspaceId); diff --git a/server/src/core/page/services/page-ordering.service.ts b/server/src/core/page/services/page-ordering.service.ts index 5bed4e3..ad85482 100644 --- a/server/src/core/page/services/page-ordering.service.ts +++ b/server/src/core/page/services/page-ordering.service.ts @@ -40,7 +40,6 @@ export class PageOrderingService { if (!movedPage) throw new BadRequestException('Moved page not found'); if (!dto.parentId) { - console.log('no parent'); if (movedPage.parentPageId) { await this.removeFromParent(movedPage.parentPageId, dto.id, manager); } diff --git a/server/src/core/storage/drivers/s3.driver.ts b/server/src/core/storage/drivers/s3.driver.ts index 06a2878..baf82fc 100644 --- a/server/src/core/storage/drivers/s3.driver.ts +++ b/server/src/core/storage/drivers/s3.driver.ts @@ -18,7 +18,7 @@ export class S3Driver implements StorageDriver { constructor(config: S3StorageConfig) { this.config = config; - this.s3Client = new S3Client(config); + this.s3Client = new S3Client(config as any); } async upload(filePath: string, file: Buffer): Promise { diff --git a/server/src/core/user/dto/update-user.dto.ts b/server/src/core/user/dto/update-user.dto.ts index dfd37fb..fe40a07 100644 --- a/server/src/core/user/dto/update-user.dto.ts +++ b/server/src/core/user/dto/update-user.dto.ts @@ -1,4 +1,9 @@ import { PartialType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; +import { IsOptional, IsString } from 'class-validator'; -export class UpdateUserDto extends PartialType(CreateUserDto) {} +export class UpdateUserDto extends PartialType(CreateUserDto) { + @IsOptional() + @IsString() + avatarUrl: string; +} diff --git a/server/src/core/user/user.service.ts b/server/src/core/user/user.service.ts index 0c2bb06..439f9cd 100644 --- a/server/src/core/user/user.service.ts +++ b/server/src/core/user/user.service.ts @@ -69,6 +69,10 @@ export class UserService { user.email = updateUserDto.email; } + if (updateUserDto.avatarUrl) { + user.avatarUrl = updateUserDto.avatarUrl; + } + return this.userRepository.save(user); } diff --git a/server/src/core/workspace/dto/update-workspace.dto.ts b/server/src/core/workspace/dto/update-workspace.dto.ts index 3a76ba0..fc12a62 100644 --- a/server/src/core/workspace/dto/update-workspace.dto.ts +++ b/server/src/core/workspace/dto/update-workspace.dto.ts @@ -1,4 +1,9 @@ import { PartialType } from '@nestjs/mapped-types'; import { CreateWorkspaceDto } from './create-workspace.dto'; +import { IsOptional, IsString } from 'class-validator'; -export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {} +export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) { + @IsOptional() + @IsString() + logo: string; +} diff --git a/server/src/core/workspace/services/workspace.service.ts b/server/src/core/workspace/services/workspace.service.ts index 820329b..5e8b028 100644 --- a/server/src/core/workspace/services/workspace.service.ts +++ b/server/src/core/workspace/services/workspace.service.ts @@ -68,6 +68,10 @@ export class WorkspaceService { workspace.name = updateWorkspaceDto.name; } + if (updateWorkspaceDto.logo) { + workspace.logo = updateWorkspaceDto.logo; + } + return this.workspaceRepository.save(workspace); } diff --git a/server/src/decorators/jwt-user.decorator.ts b/server/src/decorators/jwt-user.decorator.ts new file mode 100644 index 0000000..a01f0f4 --- /dev/null +++ b/server/src/decorators/jwt-user.decorator.ts @@ -0,0 +1,9 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const JwtUser = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request['user']; + return { id: user.sub, email: user.email }; + }, +); diff --git a/server/src/main.ts b/server/src/main.ts index d888840..7119cf4 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -32,7 +32,7 @@ async function bootstrap() { app.useGlobalInterceptors(new TransformHttpResponseInterceptor()); app.enableShutdownHooks(); - await app.listen(process.env.PORT || 3001); + await app.listen(process.env.PORT || 3000); } bootstrap();