hexabot/api/src/user/controllers/user.controller.ts
Mohamed Marrouchi 8ec4a33d94 fix: pagination
2024-11-22 16:05:30 +01:00

412 lines
13 KiB
TypeScript

/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
NotFoundException,
Param,
Patch,
Post,
Query,
Req,
Session,
UnauthorizedException,
UseInterceptors,
} from '@nestjs/common';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
import { Request } from 'express';
import { Session as ExpressSession } from 'express-session';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { Roles } from '@/utils/decorators/roles.decorator';
import { BaseController } from '@/utils/generics/base-controller';
import { generateInitialsAvatar, getBotAvatar } from '@/utils/helpers/avatar';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { TFilterQuery } from '@/utils/types/filter.types';
import { InvitationCreateDto } from '../dto/invitation.dto';
import {
UserCreateDto,
UserEditProfileDto,
UserRequestResetDto,
UserResetPasswordDto,
UserUpdateStateAndRolesDto,
} from '../dto/user.dto';
import { User, UserFull, UserPopulate, UserStub } from '../schemas/user.schema';
import { InvitationService } from '../services/invitation.service';
import { PasswordResetService } from '../services/passwordReset.service';
import { PermissionService } from '../services/permission.service';
import { RoleService } from '../services/role.service';
import { UserService } from '../services/user.service';
import { ValidateAccountService } from '../services/validate-account.service';
@UseInterceptors(CsrfInterceptor)
@Controller('user')
export class ReadOnlyUserController extends BaseController<
User,
UserStub,
UserPopulate,
UserFull
> {
constructor(
protected readonly userService: UserService,
protected readonly roleService: RoleService,
protected readonly invitationService: InvitationService,
protected readonly permissionService: PermissionService,
protected readonly attachmentService: AttachmentService,
protected readonly logger: LoggerService,
protected readonly passwordResetService: PasswordResetService,
protected readonly validateAccountService: ValidateAccountService,
) {
super(userService);
}
/**
* Retrieves the bot's profile picture.
*
* @returns A promise that resolves to the bot's avatar URL.
*/
@Roles('public')
@Get('bot/profile_pic')
async botProfilePic(@Query('color') color: string) {
return getBotAvatar(color);
}
/**
* Retrieves the user's profile picture.
*
* @param id - The ID of the user.
*
* @returns A promise that resolves to the user's avatar or an avatar generated from initials if not found.
*/
@Roles('public')
@Get(':id/profile_pic')
async UserProfilePic(@Param('id') id: string) {
try {
const res = await this.userService.userProfilePic(id);
return res;
} catch (e) {
const user = await this.userService.findOne(id);
if (user) {
return generateInitialsAvatar(user);
} else {
throw new NotFoundException(`user with ID ${id} not found`);
}
}
}
/**
* Retrieves the current user's roles and permissions.
*
* @param req - The request object containing the authenticated user.
*
* @returns A promise that resolves to the user's roles and associated permissions.
*/
@Roles('public')
@Get('permissions/:id?')
async permissions(@Req() req: Request) {
if (!req.user || !('id' in req.user && req.user.id)) {
throw new UnauthorizedException();
}
const currentUser = await this.userService.findOneAndPopulate(
req.user.id as string,
);
const currentPermissions = await this.permissionService.findAndPopulate({
role: {
$in: currentUser.roles.map(({ id }) => id),
},
});
return {
roles: currentUser.roles,
permissions: currentPermissions.map((permission) => {
if (permission.model) {
return {
model: permission.model.name,
action: permission.action,
relation: permission.relation,
};
}
}),
};
}
/**
* Retrieves a paginated list of users based on filters.
*
* @param pageQuery - The pagination query object.
* @param populate - An array of fields to populate.
* @param filters - Filters applied to the query.
*
* @returns A promise that resolves to a paginated list of users.
*/
@Get()
async findPage(
@Query(PageQueryPipe) pageQuery: PageQueryDto<User>,
@Query(PopulatePipe)
populate: string[],
@Query(
new SearchFilterPipe<User>({
allowedFields: ['first_name', 'last_name'],
}),
)
filters: TFilterQuery<User>,
) {
if (pageQuery.limit) {
return this.canPopulate(populate)
? await this.userService.findPageAndPopulate(filters, pageQuery)
: await this.userService.findPage(filters, pageQuery);
}
return this.canPopulate(populate)
? await this.userService.findAndPopulate(filters, pageQuery.sort)
: await this.userService.find(filters, pageQuery.sort);
}
/**
* Counts the number of users that match the provided filters.
*
* @returns A promise that resolves to the count of filtered users.
*/
@Get('count')
async filterCount(
@Query(
new SearchFilterPipe<User>({
allowedFields: ['first_name', 'last_name'],
}),
)
filters?: TFilterQuery<User>,
) {
return await this.count(filters);
}
/**
* Retrieves a single user by ID.
*
* @param id - The ID of the user to retrieve.
* @param populate - An array of fields to populate.
*
* @returns A promise that resolves to the user document.
*/
@Get(':id')
async findOne(
@Param('id') id: string,
@Query(PopulatePipe)
populate: string[],
) {
const doc = this.canPopulate(populate)
? await this.userService.findOneAndPopulate(id)
: await this.userService.findOne(id);
if (!doc) {
this.logger.warn(`Unable to find User by id ${id}`);
throw new NotFoundException(`User with ID ${id} not found`);
}
return doc;
}
}
@UseInterceptors(CsrfInterceptor)
@Controller('user')
export class ReadWriteUserController extends ReadOnlyUserController {
/**
* Creates a new user.
*
* @param user - The user object to create.
*
* @returns A promise that resolves to the created user.
*/
@CsrfCheck(true)
@Post()
async create(@Body() user: UserCreateDto) {
this.validate({
dto: user,
allowedIds: {
roles: (await this.roleService.findAll())
.filter((role) => user.roles.includes(role.id))
.map((role) => role.id),
avatar: (await this.attachmentService.findOne(user.avatar))?.id,
},
});
return await this.userService.create(user);
}
/**
* Updates an existing user profile.
*
* @param req - The request object containing the authenticated user.
* @param id - The ID of the user to update.
* @param userUpdate - The user update object.
*
* @returns A promise that resolves to the updated user.
*/
@CsrfCheck(true)
@Patch('edit/:id')
async updateOne(
@Req() req: Request,
@Param('id') id: string,
@Body() userUpdate: UserEditProfileDto,
) {
if (!('id' in req.user && req.user.id) || req.user.id !== id) {
throw new UnauthorizedException();
}
const result = await this.userService.updateOne(req.user.id, userUpdate);
if (!result) {
this.logger.warn(`Unable to update User by id ${id}`);
throw new NotFoundException(`User with ID ${id} not found`);
}
return result;
}
/**
* Updates the state and roles of a user.
*
* This method allows updating the state and roles of a user. It ensures that
* the current user cannot disable their own account or revoke their admin
* privileges. If an update attempt fails, it throws a `NotFoundException`.
*
* @param id - The ID of the user to update.
* @param body - The new state and roles of the user.
* @param session - The current session of the user performing the update.
*
* @returns The updated user data.
*/
@CsrfCheck(true)
@Patch(':id')
async updateStateAndRoles(
@Param('id') id: string,
@Body() body: UserUpdateStateAndRolesDto,
@Session() session: ExpressSession,
) {
const oldRoles = (await this.userService.findOne(id)).roles;
const newRoles = body.roles;
const { id: adminRoleId } = await this.roleService.findOne({
name: 'admin',
});
if (id === session.passport?.user?.id && body.state === false) {
throw new ForbiddenException('Your account state is protected');
}
if (
session?.passport?.user?.id === id &&
oldRoles.includes(adminRoleId) &&
!newRoles.includes(adminRoleId)
) {
throw new ForbiddenException('Admin privileges are protected');
}
const result = await this.userService.updateOne(id, body);
if (!result) {
this.logger.warn(`Unable to update User by id ${id}`);
throw new NotFoundException(`User with ID ${id} not found`);
}
return result;
}
/**
* Deletes a user by ID.
*
* This method deletes a user from the system. If no user with the given ID is found,
* it throws a `NotFoundException`. A successful deletion returns a `204 No Content` status.
*
* @param id - The ID of the user to delete.
*
* @returns Nothing (HTTP 204 on success).
*/
@CsrfCheck(true)
@Delete(':id')
@HttpCode(204)
async deleteOne(@Param('id') id: string) {
const result = await this.userService.deleteOne(id);
if (result.deletedCount === 0) {
this.logger.warn(`Unable to delete User by id ${id}`);
throw new NotFoundException(`User with ID ${id} not found`);
}
return result;
}
/**
* Sends an invitation to a user.
*
* This method allows an administrator or authorized user to invite someone by
* creating an invitation entry in the system.
*
* @param invitationCreateDto - The invitation details, including recipient information.
*
* @returns The created invitation record.
*/
@CsrfCheck(true)
@Post('invite')
async invite(@Body() invitationCreateDto: InvitationCreateDto) {
return await this.invitationService.create(invitationCreateDto);
}
/**
* Requests a password reset.
*
* This method initiates the password reset process for a user. It sends an
* email or other communication to the user with instructions on how to reset
* their password.
*
* @param body - The email or identifier of the user requesting the password reset.
*
* @returns A success message indicating the reset request has been processed.
*/
@Roles('public')
@Post('reset')
async requestReset(@Body() body: UserRequestResetDto) {
return await this.passwordResetService.requestReset(body);
}
/**
* Resets the password for a user.
*
* This method allows a user to reset their password using a provided token. The token
* must be valid and correspond to a valid reset request.
*
* @param body - The new password and any other necessary information.
* @param token - The reset token provided to the user.
*
* @returns A success message indicating the password has been reset.
*/
@Roles('public')
@Post('reset/:token')
async reset(
@Body() body: UserResetPasswordDto,
@Param('token') token: string,
) {
return await this.passwordResetService.reset(body, token);
}
/**
* Confirms a user's account.
*
* This method verifies a user's account by validating a confirmation token. It marks
* the account as confirmed and activates it if the token is valid.
*
* @param body - The confirmation token to verify the user's account.
*
* @returns A success message indicating the account has been confirmed.
*/
@Roles('public')
@Post('confirm')
async confirmAccount(@Body() body: { token: string }) {
return await this.validateAccountService.confirmAccount(body);
}
}