mirror of
https://github.com/hexastack/hexabot
synced 2025-04-06 22:14:26 +00:00
412 lines
13 KiB
TypeScript
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);
|
|
}
|
|
}
|