Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
} from '@nestjs/common';
import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/guards/roles.decorator';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
Expand All @@ -29,4 +39,31 @@ export class AdminController {
async getQueueStats() {
return this.adminService.getQueueStats();
}

@Get('users')
async listUsers(
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
return this.adminService.listUsers(page, limit);
}

@Get('users/:id')
async getUser(@Param('id') id: string) {
return this.adminService.getUser(id);
}

@Patch('users/:id/role')
async changeUserRole(
@Param('id') id: string,
@Body() dto: UpdateUserRoleDto,
) {
return this.adminService.changeUserRole(id, dto.role);
}

@Delete('users/:id')
async deleteUser(@Param('id') id: string) {
await this.adminService.deleteUser(id);
return { message: 'User deleted successfully' };
}
}
30 changes: 28 additions & 2 deletions backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { User } from '../users/entities/user.entity';
import { User, UserRole } from '../users/entities/user.entity';
import { Document, DocumentStatus } from '../documents/entities/document.entity';
import { QueueService } from '../queue/queue.service';

Expand Down Expand Up @@ -62,4 +62,30 @@ export class AdminService {
async getQueueStats() {
return this.queueService.getQueueStats();
}

async listUsers(page = 1, limit = 20) {
const [data, total] = await this.userRepository.findAndCount({
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { data, total, page, limit, totalPages: Math.ceil(total / limit) };
}

async getUser(id: string) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) throw new NotFoundException('User not found');
return user;
}

async changeUserRole(id: string, role: UserRole) {
const user = await this.getUser(id);
user.role = role;
return this.userRepository.save(user);
}

async deleteUser(id: string) {
const user = await this.getUser(id);
await this.userRepository.softDelete(user.id);
}
}
8 changes: 8 additions & 0 deletions backend/src/admin/dto/update-user-role.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsEnum, IsNotEmpty } from 'class-validator';
import { UserRole } from '../../users/entities/user.entity';

export class UpdateUserRoleDto {
@IsNotEmpty()
@IsEnum(UserRole)
role: UserRole;
}
24 changes: 24 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { AuthService } from './auth.service';
import { RegisterAuthDto } from './dto/register-auth.dto';
import { LoginAuthDto } from './dto/login-auth.dto';
import { RefreshAuthDto } from './dto/refresh-auth.dto';
import { SendVerificationDto } from './dto/send-verification.dto';
import { VerifyEmailDto } from './dto/verify-email.dto';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';

@Controller('auth')
Expand All @@ -43,6 +47,26 @@ export class AuthController {
return this.authService.refreshToken(dto);
}

@Post('send-verification')
sendVerification(@Body() dto: SendVerificationDto) {
return this.authService.sendVerification(dto.email);
}

@Post('verify-email')
verifyEmail(@Body() dto: VerifyEmailDto) {
return this.authService.verifyEmail(dto.email, dto.token);
}

@Post('forgot-password')
forgotPassword(@Body() dto: ForgotPasswordDto) {
return this.authService.forgotPassword(dto.email);
}

@Post('reset-password')
resetPassword(@Body() dto: ResetPasswordDto) {
return this.authService.resetPassword(dto.email, dto.token, dto.newPassword);
}

@Post('logout')
@UseGuards(JwtAuthGuard)
async logout(
Expand Down
52 changes: 52 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Logger,
UnauthorizedException,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
Expand Down Expand Up @@ -146,6 +147,57 @@ export class AuthService {
return { message: 'Successfully logged out' };
}

private readonly verificationTokens = new Map<string, { token: string; expires: Date }>();
private readonly resetTokens = new Map<string, { token: string; expires: Date }>();

async sendVerification(email: string) {
const user = await this.usersService.findByEmail(email);
if (!user) throw new NotFoundException('User not found');
if (user.isVerified) throw new BadRequestException('Email already verified');

const token = crypto.randomBytes(32).toString('hex');
this.verificationTokens.set(email, { token, expires: new Date(Date.now() + 3600_000) });

this.logger.log(`Verification token for ${email}: ${token}`);
return { message: 'Verification email sent' };
}

async verifyEmail(email: string, token: string) {
const stored = this.verificationTokens.get(email);
if (!stored || stored.token !== token) throw new BadRequestException('Invalid token');
if (stored.expires < new Date()) throw new BadRequestException('Token expired');

const user = await this.usersService.findByEmail(email);
if (!user) throw new NotFoundException('User not found');
await this.usersService.update(user.id, { isVerified: true } as any);
this.verificationTokens.delete(email);
return { message: 'Email verified successfully' };
}

async forgotPassword(email: string) {
const user = await this.usersService.findByEmail(email);
if (!user) throw new NotFoundException('User not found');

const token = crypto.randomBytes(32).toString('hex');
this.resetTokens.set(email, { token, expires: new Date(Date.now() + 3600_000) });

this.logger.log(`Password reset token for ${email}: ${token}`);
return { message: 'Password reset email sent' };
}

async resetPassword(email: string, token: string, newPassword: string) {
const stored = this.resetTokens.get(email);
if (!stored || stored.token !== token) throw new BadRequestException('Invalid token');
if (stored.expires < new Date()) throw new BadRequestException('Token expired');

const user = await this.usersService.findByEmail(email);
if (!user) throw new NotFoundException('User not found');
const passwordHash = await bcrypt.hash(newPassword, 12);
await this.usersService.update(user.id, { passwordHash } as any);
this.resetTokens.delete(email);
return { message: 'Password reset successfully' };
}

private hashToken(token: string): string {
return crypto.createHash('sha256').update(token).digest('hex');
}
Expand Down
6 changes: 6 additions & 0 deletions backend/src/auth/dto/forgot-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsEmail } from 'class-validator';

export class ForgotPasswordDto {
@IsEmail()
email: string;
}
13 changes: 13 additions & 0 deletions backend/src/auth/dto/reset-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

export class ResetPasswordDto {
@IsEmail()
email: string;

@IsNotEmpty()
token: string;

@IsNotEmpty()
@MinLength(6)
newPassword: string;
}
6 changes: 6 additions & 0 deletions backend/src/auth/dto/send-verification.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsEmail } from 'class-validator';

export class SendVerificationDto {
@IsEmail()
email: string;
}
9 changes: 9 additions & 0 deletions backend/src/auth/dto/verify-email.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsEmail, IsNotEmpty } from 'class-validator';

export class VerifyEmailDto {
@IsEmail()
email: string;

@IsNotEmpty()
token: string;
}
11 changes: 11 additions & 0 deletions backend/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {
@IsOptional()
@IsString()
fullName?: string;

@IsOptional()
@IsString()
preferredLanguage?: string;
}
19 changes: 18 additions & 1 deletion backend/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Controller, Get, Delete, Req, UseGuards } from '@nestjs/common';
import { Controller, Get, Patch, Delete, Body, Req, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { Request } from 'express';
import { User } from './entities/user.entity';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
Expand All @@ -26,4 +27,20 @@ export class UsersController {
await this.usersService.eraseUserData(req.user!.id);
return { message: 'Your personal data has been erased.' };
}

@Patch('me')
@UseGuards(JwtAuthGuard)
async updateProfile(
@Req() req: Request & { user?: User },
@Body() dto: UpdateUserDto,
) {
return this.usersService.update(req.user!.id, dto);
}

@Delete('me')
@UseGuards(JwtAuthGuard)
async deleteAccount(@Req() req: Request & { user?: User }) {
await this.usersService.softDelete(req.user!.id);
return { message: 'Account deleted successfully' };
}
}
Loading