diff --git a/backend/src/module/document-download/document-download.controller.ts b/backend/src/module/document-download/document-download.controller.ts new file mode 100644 index 0000000..e13732c --- /dev/null +++ b/backend/src/module/document-download/document-download.controller.ts @@ -0,0 +1,35 @@ +import { + Controller, Get, Param, Req, Res, UseGuards, + NotFoundException, ForbiddenException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Response } from 'express'; +import { createReadStream, existsSync } from 'fs'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { Document } from '../../documents/entities/document.entity'; +import { User } from '../../users/entities/user.entity'; + +@Controller('module/documents') +@UseGuards(JwtAuthGuard) +export class DocumentDownloadController { + constructor( + @InjectRepository(Document) + private readonly docs: Repository, + ) {} + + @Get(':id/download') + async download( + @Param('id') id: string, + @Req() req: { user: User }, + @Res() res: Response, + ) { + const doc = await this.docs.findOneBy({ id }); + if (!doc) throw new NotFoundException('Document not found'); + if (doc.ownerId !== req.user.id) throw new ForbiddenException(); + if (!existsSync(doc.filePath)) throw new NotFoundException('File not found on disk'); + res.setHeader('Content-Type', doc.mimeType); + res.setHeader('Content-Disposition', `attachment; filename="${doc.title}"`); + createReadStream(doc.filePath).pipe(res); + } +} \ No newline at end of file diff --git a/backend/src/module/document-list/document-list.controller.ts b/backend/src/module/document-list/document-list.controller.ts new file mode 100644 index 0000000..e00aa1e --- /dev/null +++ b/backend/src/module/document-list/document-list.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { Document, DocumentStatus } from '../../documents/entities/document.entity'; +import { User } from '../../users/entities/user.entity'; + +@Controller('module/documents') +@UseGuards(JwtAuthGuard) +export class DocumentListController { + constructor( + @InjectRepository(Document) + private readonly docs: Repository, + ) {} + + @Get() + async list( + @Req() req: { user: User }, + @Query('page') page = '1', + @Query('limit') limit = '20', + @Query('status') status?: DocumentStatus, + ) { + const p = Math.max(1, parseInt(page, 10)); + const l = Math.min(100, Math.max(1, parseInt(limit, 10))); + const where: Record = { ownerId: req.user.id }; + if (status) where['status'] = status; + const [data, total] = await this.docs.findAndCount({ + where, + order: { createdAt: 'DESC' }, + skip: (p - 1) * l, + take: l, + }); + return { data, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; + } +} \ No newline at end of file diff --git a/backend/src/module/user-profile/dto/user-profile.dto.ts b/backend/src/module/user-profile/dto/user-profile.dto.ts new file mode 100644 index 0000000..949b28b --- /dev/null +++ b/backend/src/module/user-profile/dto/user-profile.dto.ts @@ -0,0 +1,24 @@ +import { IsEmail, IsOptional, IsString } from 'class-validator'; +import { Exclude, Expose } from 'class-transformer'; +import { UserRole } from '../../users/entities/user.entity'; + +export class UpdateProfileDto { + @IsOptional() + @IsString() + fullName?: string; + + @IsOptional() + @IsEmail() + email?: string; +} + +@Exclude() +export class UserResponseDto { + @Expose() id: string; + @Expose() email: string; + @Expose() fullName: string; + @Expose() role: UserRole; + @Expose() isVerified: boolean; + @Expose() preferredLanguage: string; + @Expose() createdAt: Date; +} \ No newline at end of file diff --git a/backend/src/module/user-profile/user-profile.controller.ts b/backend/src/module/user-profile/user-profile.controller.ts new file mode 100644 index 0000000..4f81da1 --- /dev/null +++ b/backend/src/module/user-profile/user-profile.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Get, Patch, Req, UseGuards } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { User } from '../../users/entities/user.entity'; + +@Controller('module/users') +@UseGuards(JwtAuthGuard) +export class UserProfileController { + constructor( + @InjectRepository(User) + private readonly users: Repository, + ) {} + + @Get('me') + getProfile(@Req() req: { user: User }) { + const { passwordHash, twoFactorSecret, twoFactorBackupCodes, ...profile } = req.user; + void passwordHash; void twoFactorSecret; void twoFactorBackupCodes; + return profile; + } + + @Patch('me') + async updateProfile( + @Req() req: { user: User }, + @Body() body: { fullName?: string; email?: string }, + ) { + await this.users.update(req.user.id, body); + const updated = await this.users.findOneByOrFail({ id: req.user.id }); + const { passwordHash, twoFactorSecret, twoFactorBackupCodes, ...profile } = updated; + void passwordHash; void twoFactorSecret; void twoFactorBackupCodes; + return profile; + } +} \ No newline at end of file