import { BadRequestException, Body, Controller, Delete, Get, NotFoundException, Param, Patch, Post, Put, Query, UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOAuth2, ApiTags } from '@nestjs/swagger'; import { Privileges } from 'src/decorators/privileges.decorator'; import { Scopes } from 'src/decorators/scopes.decorator'; import { OAuth2Guard } from 'src/guards/oauth2.guard'; import { PrivilegesGuard } from 'src/guards/privileges.guard'; import { ScopesGuard } from 'src/guards/scopes.guard'; import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service'; import { User } from 'src/modules/objects/user/user.entity'; import { UserService } from 'src/modules/objects/user/user.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { PaginationService } from 'src/modules/utility/services/paginate.service'; import { PageOptions } from 'src/types/pagination.interfaces'; const RELATIONS = ['picture', 'privileges']; @ApiBearerAuth() @ApiTags('admin') @ApiOAuth2(['management']) @Controller('/api/admin/users') @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) export class UserAdminController { constructor( private _user: UserService, private _privilege: PrivilegeService, private _paginate: PaginationService, private _form: FormUtilityService, ) {} /** * Get a list of all users or search for a specific user * @param options Search and pagination options * @returns Paginated user list */ @Get('') @Scopes('management') @Privileges('admin', 'admin:user') async userList(@Query() options: { q?: string } & PageOptions) { const search = options.q ? decodeURIComponent(options.q) : null; const resultCount = await this._user.searchUsersCount(search, RELATIONS); const pagination = this._paginate.paginate(options, resultCount); const [list] = await this._user.searchUsers( pagination.pageSize, pagination.offset, search, RELATIONS, ); return { pagination, list: this._form.stripObjectArray(list, ['password']), }; } /** * Get a single user by ID * @param id User ID * @returns User */ @Get(':id') @Scopes('management') @Privileges('admin', 'admin:user') async user(@Param('id') id: string) { const user = await this._user.getById(parseInt(id, 10), RELATIONS); if (!user) { throw new NotFoundException('User not found'); } return this._form.stripObject(user, ['password']); } @Patch(':id') @Scopes('management') @Privileges('admin', 'admin:user') async updateUser(@Param('id') id: string, @Body() body: Partial) { const user = await this._user.getById(parseInt(id, 10)); if (!user) { throw new NotFoundException('User not found'); } const allowedFieldsOnly = this._form.pluckObject(body, [ 'display_name', 'username', 'email', 'activated', ]); if (!Object.keys(allowedFieldsOnly).length) { throw new BadRequestException('No data was updated.'); } Object.assign(user, allowedFieldsOnly); await this._user.updateUser(user); return user; } /** * Delete a user avatar from the server * @param id User ID * @returns Success */ @Delete(':id/avatar') @Scopes('management') @Privileges('admin', 'admin:user') async deleteUserAvatar(@Param('id') id: string) { const user = await this._user.getById(parseInt(id, 10), ['picture']); if (!user) { throw new NotFoundException('User not found'); } if (user.picture) { await this._user.deleteAvatar(user); user.picture = null; await this._user.updateUser(user); } return { success: true }; } /** * Unpaginated list of all privileges for user * @param id User ID * @returns Privilege list */ @Get(':id/privileges') @Scopes('management') @Privileges('admin', 'admin:user') async userPrivileges(@Param('id') id: string) { const user = await this._user.getById(parseInt(id, 10), ['privileges']); if (!user) { throw new NotFoundException('User not found'); } return user.privileges; } /** * Replace user's privileges with the new list * @param id User ID * @param body With `privileges`, list of privilege IDs the user should have * @returns New privileges array */ @Put(':id/privileges') @Scopes('management') @Privileges('admin', 'admin:user') async setUserPrivileges( @Param('id') id: string, @Body() body: { privileges: number[] }, ) { const user = await this._user.getById(parseInt(id, 10), ['privileges']); if (!user) { throw new NotFoundException('User not found'); } if (!body.privileges) { throw new BadRequestException('Privileges are required.'); } if (body.privileges.length) { const privileges = await this._privilege.getByIDs(body.privileges); if (!privileges?.length) { throw new BadRequestException('Privileges not found.'); } user.privileges = privileges; } else { user.privileges.length = 0; } await this._user.updateUser(user); return user.privileges; } /** * Resend activation email to a user * @param id User ID * @returns Success or error */ @Post(':id/activation') @Scopes('management') @Privileges('admin', 'admin:user') async activateUserEmail(@Param('id') id: string) { const user = await this._user.getById(parseInt(id, 10)); if (!user) { throw new NotFoundException('User not found'); } let error: Error; try { await this._user.sendActivationEmail(user); } catch (e: any) { error = e as Error; } return { success: !error, error: error?.name, message: error?.message }; } /** * Send password reset email to a user * @param id User ID * @returns Success or error */ @Post(':id/password') @Scopes('management') @Privileges('admin', 'admin:user') async resetPasswordEmail(@Param('id') id: string) { const user = await this._user.getById(parseInt(id, 10)); if (!user) { throw new NotFoundException('User not found'); } let error: Error; try { await this._user.sendPasswordEmail(user); } catch (e: any) { error = e as Error; } return { success: !error, error: error?.name, message: error?.message }; } }