file attachments - WIP

This commit is contained in:
Philipinho 2024-01-08 09:56:10 +01:00
parent e594074fda
commit e1bb2632b8
27 changed files with 616 additions and 49 deletions

View File

@ -1,11 +1,14 @@
import React from 'react'; import React from 'react';
import { Avatar } from '@mantine/core'; import { Avatar } from '@mantine/core';
interface UserAvatarProps extends React.ComponentProps<typeof Avatar> { interface UserAvatarProps {
avatarUrl: string; avatarUrl: string;
name: string; name: string;
color?: string; color?: string;
size?: string; size?: string;
radius?: string;
style?: any;
component?: any;
} }
export const UserAvatar = React.forwardRef<HTMLInputElement, UserAvatarProps>( export const UserAvatar = React.forwardRef<HTMLInputElement, UserAvatarProps>(

View File

@ -18,20 +18,21 @@ function RecentChanges() {
return ( return (
<div> <div>
{data.map((page) => ( {data
.map((page) => (
<div key={page.id}> <div key={page.id}>
<UnstyledButton component={Link} to={`/p/${page.id}`} <UnstyledButton component={Link} to={`/p/${page.id}`}
className={classes.page} p="xs"> className={classes.page} p="xs">
<Group wrap="nowrap"> <Group wrap="nowrap">
<Stack gap="xs" style={{ flex: 1 }}> <Stack gap="xs" style={{ flex: 1 }}>
<Text fw={500} size="sm"> <Text fw={500} size="md" lineClamp={1}>
{page.title || 'Untitled'} {page.title || 'Untitled'}
</Text> </Text>
</Stack> </Stack>
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs" fw={500}>
{format(new Date(page.createdAt), 'PPP')} {format(new Date(page.updatedAt), 'PP')}
</Text> </Text>
</Group> </Group>
</UnstyledButton> </UnstyledButton>

View File

@ -3,11 +3,15 @@ import AccountNameForm from '@/features/settings/account/settings/components/acc
import ChangeEmail from '@/features/settings/account/settings/components/change-email'; import ChangeEmail from '@/features/settings/account/settings/components/change-email';
import ChangePassword from '@/features/settings/account/settings/components/change-password'; import ChangePassword from '@/features/settings/account/settings/components/change-password';
import { Divider } from '@mantine/core'; import { Divider } from '@mantine/core';
import AccountAvatar from '@/features/settings/account/settings/components/account-avatar';
export default function AccountSettings() { export default function AccountSettings() {
return ( return (
<> <>
<AccountAvatar />
<AccountNameForm /> <AccountNameForm />
<Divider my="lg" /> <Divider my="lg" />

View File

@ -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<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(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 (
<>
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
{(props) => (
<Tooltip label="Change photo"
position="bottom"
>
<UserAvatar
{...props}
component="button"
radius="xl"
size="60px"
avatarUrl={previewUrl || currentUser.user.avatarUrl}
name={currentUser.user.name}
style={{ cursor: 'pointer' }}
/>
</Tooltip>
)}
</FileButton>
</>
);
}

View File

@ -63,15 +63,15 @@ function ChangePasswordForm() {
<PasswordInput <PasswordInput
label="Current password" label="Current password"
name="current" name="current"
placeholder="Enter your password" placeholder="Enter your current password"
variant="filled" variant="filled"
mb="md" mb="md"
{...form.getInputProps('password')} {...form.getInputProps('current')}
/> />
<PasswordInput <PasswordInput
label="New password" label="New password"
placeholder="Enter your password" placeholder="Enter your new password"
variant="filled" variant="filled"
mb="md" mb="md"
{...form.getInputProps('password')} {...form.getInputProps('password')}

View File

@ -11,8 +11,20 @@ export async function getUserInfo(): Promise<ICurrentUserResponse> {
return req.data as ICurrentUserResponse; return req.data as ICurrentUserResponse;
} }
export async function updateUser(data: Partial<IUser>) { export async function updateUser(data: Partial<IUser>): Promise<IUser> {
const req = await api.post<IUser>('/user/update', data); const req = await api.post<IUser>('/user/update', data);
return req.data as IUser; 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

View File

@ -43,6 +43,7 @@
"@nestjs/typeorm": "^10.0.1", "@nestjs/typeorm": "^10.0.1",
"@nestjs/websockets": "^10.2.10", "@nestjs/websockets": "^10.2.10",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bytes": "^3.1.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"fastify": "^4.24.3", "fastify": "^4.24.3",
@ -63,6 +64,7 @@
"@nestjs/schematics": "^10.0.3", "@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.10", "@nestjs/testing": "^10.2.10",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/bytes": "^3.1.4",
"@types/debounce": "^1.2.4", "@types/debounce": "^1.2.4",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.10", "@types/jest": "^29.5.10",

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

@ -1,7 +1,23 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AttachmentService } from './attachment.service'; 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({ @Module({
providers: [AttachmentService], imports: [
TypeOrmModule.forFeature([Attachment]),
StorageModule,
AuthModule,
UserModule,
WorkspaceModule,
],
controllers: [AttachmentController],
providers: [AttachmentService, AttachmentRepository],
}) })
export class AttachmentModule {} export class AttachmentModule {}

View File

@ -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() @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<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 });
}
}

View File

@ -5,6 +5,8 @@ import { User } from '../../user/entities/user.entity';
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { TokensDto } from '../dto/tokens.dto'; import { TokensDto } from '../dto/tokens.dto';
export type JwtPayload = { sub: string; email: string };
@Injectable() @Injectable()
export class TokenService { export class TokenService {
constructor( constructor(
@ -12,7 +14,7 @@ export class TokenService {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
) {} ) {}
async generateJwt(user: User): Promise<string> { async generateJwt(user: User): Promise<string> {
const payload = { const payload: JwtPayload = {
sub: user.id, sub: user.id,
email: user.email, email: user.email,
}; };

View File

@ -2,7 +2,6 @@ import {
Controller, Controller,
Post, Post,
Body, Body,
Req,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
UseGuards, UseGuards,
@ -10,7 +9,6 @@ import {
import { PageService } from './services/page.service'; import { PageService } from './services/page.service';
import { CreatePageDto } from './dto/create-page.dto'; import { CreatePageDto } from './dto/create-page.dto';
import { UpdatePageDto } from './dto/update-page.dto'; import { UpdatePageDto } from './dto/update-page.dto';
import { FastifyRequest } from 'fastify';
import { JwtGuard } from '../auth/guards/JwtGuard'; import { JwtGuard } from '../auth/guards/JwtGuard';
import { WorkspaceService } from '../workspace/services/workspace.service'; import { WorkspaceService } from '../workspace/services/workspace.service';
import { MovePageDto } from './dto/move-page.dto'; 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 { PageHistoryService } from './services/page-history.service';
import { HistoryDetailsDto } from './dto/history-details.dto'; import { HistoryDetailsDto } from './dto/history-details.dto';
import { PageHistoryDto } from './dto/page-history.dto'; import { PageHistoryDto } from './dto/page-history.dto';
import { JwtUser } from '../../decorators/jwt-user.decorator';
@UseGuards(JwtGuard) @UseGuards(JwtGuard)
@Controller('pages') @Controller('pages')
@ -39,29 +38,17 @@ export class PageController {
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@Post('create') @Post('create')
async create( async create(@JwtUser() jwtUser, @Body() createPageDto: CreatePageDto) {
@Req() req: FastifyRequest,
@Body() createPageDto: CreatePageDto,
) {
const jwtPayload = req['user'];
const userId = jwtPayload.sub;
const workspaceId = ( const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id; ).id;
return this.pageService.create(userId, workspaceId, createPageDto); return this.pageService.create(jwtUser.id, workspaceId, createPageDto);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('update') @Post('update')
async update( async update(@JwtUser() jwtUser, @Body() updatePageDto: UpdatePageDto) {
@Req() req: FastifyRequest, return this.pageService.update(updatePageDto.id, updatePageDto, jwtUser.id);
@Body() updatePageDto: UpdatePageDto,
) {
const jwtPayload = req['user'];
const userId = jwtPayload.sub;
return this.pageService.update(updatePageDto.id, updatePageDto, userId);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ -84,40 +71,36 @@ export class PageController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('recent') @Post('recent')
async getRecentWorkspacePages(@Req() req: FastifyRequest) { async getRecentWorkspacePages(@JwtUser() jwtUser) {
const jwtPayload = req['user'];
const workspaceId = ( const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id; ).id;
return this.pageService.getRecentWorkspacePages(workspaceId); return this.pageService.getRecentWorkspacePages(workspaceId);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post() @Post()
async getWorkspacePages(@Req() req: FastifyRequest) { async getWorkspacePages(@JwtUser() jwtUser) {
const jwtPayload = req['user'];
const workspaceId = ( const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id; ).id;
return this.pageService.getSidebarPagesByWorkspaceId(workspaceId); return this.pageService.getSidebarPagesByWorkspaceId(workspaceId);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('ordering') @Post('ordering')
async getWorkspacePageOrder(@Req() req: FastifyRequest) { async getWorkspacePageOrder(@JwtUser() jwtUser) {
const jwtPayload = req['user'];
const workspaceId = ( const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id; ).id;
return this.pageOrderService.getWorkspacePageOrder(workspaceId); return this.pageOrderService.getWorkspacePageOrder(workspaceId);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('tree') @Post('tree')
async workspacePageTree(@Req() req: FastifyRequest) { async workspacePageTree(@JwtUser() jwtUser) {
const jwtPayload = req['user'];
const workspaceId = ( const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) await this.workspaceService.getUserCurrentWorkspace(jwtUser.id)
).id; ).id;
return this.pageOrderService.convertToTree(workspaceId); return this.pageOrderService.convertToTree(workspaceId);

View File

@ -40,7 +40,6 @@ export class PageOrderingService {
if (!movedPage) throw new BadRequestException('Moved page not found'); if (!movedPage) throw new BadRequestException('Moved page not found');
if (!dto.parentId) { if (!dto.parentId) {
console.log('no parent');
if (movedPage.parentPageId) { if (movedPage.parentPageId) {
await this.removeFromParent(movedPage.parentPageId, dto.id, manager); await this.removeFromParent(movedPage.parentPageId, dto.id, manager);
} }

View File

@ -18,7 +18,7 @@ export class S3Driver implements StorageDriver {
constructor(config: S3StorageConfig) { constructor(config: S3StorageConfig) {
this.config = config; this.config = config;
this.s3Client = new S3Client(config); this.s3Client = new S3Client(config as any);
} }
async upload(filePath: string, file: Buffer): Promise<void> { async upload(filePath: string, file: Buffer): Promise<void> {

View File

@ -1,4 +1,9 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto'; 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;
}

View File

@ -69,6 +69,10 @@ export class UserService {
user.email = updateUserDto.email; user.email = updateUserDto.email;
} }
if (updateUserDto.avatarUrl) {
user.avatarUrl = updateUserDto.avatarUrl;
}
return this.userRepository.save(user); return this.userRepository.save(user);
} }

View File

@ -1,4 +1,9 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { CreateWorkspaceDto } from './create-workspace.dto'; 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;
}

View File

@ -68,6 +68,10 @@ export class WorkspaceService {
workspace.name = updateWorkspaceDto.name; workspace.name = updateWorkspaceDto.name;
} }
if (updateWorkspaceDto.logo) {
workspace.logo = updateWorkspaceDto.logo;
}
return this.workspaceRepository.save(workspace); return this.workspaceRepository.save(workspace);
} }

View File

@ -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 };
},
);

View File

@ -32,7 +32,7 @@ async function bootstrap() {
app.useGlobalInterceptors(new TransformHttpResponseInterceptor()); app.useGlobalInterceptors(new TransformHttpResponseInterceptor());
app.enableShutdownHooks(); app.enableShutdownHooks();
await app.listen(process.env.PORT || 3001); await app.listen(process.env.PORT || 3000);
} }
bootstrap(); bootstrap();