src/module/user/user.service.ts
Properties |
|
Methods |
|
constructor(tutorialService: TutorialService, entityManager: EntityManager, orm: MikroORM, repository: EntityRepository<User>, em: EntityManager)
|
||||||||||||||||||
Defined in src/module/user/user.service.ts:26
|
||||||||||||||||||
Parameters :
|
Private Async assertUserIsChangeable | ||||||||||||
assertUserIsChangeable(user: User, dto: UserDTO)
|
||||||||||||
Defined in src/module/user/user.service.ts:450
|
||||||||||||
Checks if the user can be safely updated. A user is considered not NOT updatable if
Parameters :
Returns :
any
|
Private Async assertUserIsDeletable | ||||||||
assertUserIsDeletable(user: User)
|
||||||||
Defined in src/module/user/user.service.ts:471
|
||||||||
Checks if the user can be safely deleted. A user is considered NOT deletable if
Parameters :
Returns :
any
|
Private Async checkUserDTO | |||||||||||
checkUserDTO(undefined: UserDTO, user?: User)
|
|||||||||||
Defined in src/module/user/user.service.ts:420
|
|||||||||||
Checks if some more complex conditions apply to the given DTO:
If all conditions apply nothing happens else an exception is thrown.
Parameters :
Returns :
any
|
Async create | ||||||||
create(user: CreateUserDTO)
|
||||||||
Defined in src/module/user/user.service.ts:115
|
||||||||
Creates a new user and saves it to the database.
Parameters :
Returns :
Promise<IUser>
Created user. |
Async createMany | ||||||||
createMany(users: CreateUserDTO[])
|
||||||||
Defined in src/module/user/user.service.ts:128
|
||||||||
Creates multiple new users from the given information and saves them to the database.
Parameters :
Returns :
Promise<IUser[]>
Created users. |
Private Async createUser | ||||||||
createUser(user: CreateUserDTO)
|
||||||||
Defined in src/module/user/user.service.ts:314
|
||||||||
Creates a new user based on the given information. This function also updates all related tutorials which the user is tutor or corrector of.
Parameters :
Returns :
Promise<User>
Created UserDocument. |
Async delete | ||||||||
delete(id: string)
|
||||||||
Defined in src/module/user/user.service.ts:201
|
||||||||
Delete the user with the given ID if one exists.
Parameters :
Returns :
Promise<void>
Deleted document. |
Private Async doesUserWithUsernameExist | ||||||||||||
doesUserWithUsernameExist(username: string, user?: User)
|
||||||||||||
Defined in src/module/user/user.service.ts:343
|
||||||||||||
Checks if there is already a user with the given username saved in the database. If a
Parameters :
Returns :
Promise<boolean>
Is there already a user with that username? |
Async findAll |
findAll()
|
Defined in src/module/user/user.service.ts:68
|
Returns :
Promise<User[]>
All users saved in the database. |
Async findById | ||||||||
findById(id: string)
|
||||||||
Defined in src/module/user/user.service.ts:81
|
||||||||
Searches for a user with the given ID and returns its document if possible.
Parameters :
Returns :
Promise<User>
UserDocument with the given ID. |
Async findWithUsername | ||||||||
findWithUsername(usernameToFind: string)
|
||||||||
Defined in src/module/user/user.service.ts:102
|
||||||||
Returns the credentials of the user with the given username. If no user with that username exists a
Parameters :
Returns :
Promise<UserCredentialsWithPassword>
UserCredentials of the user. |
Private Async getAllTutorials | ||||||||
getAllTutorials(ids: string[])
|
||||||||
Defined in src/module/user/user.service.ts:402
|
||||||||
Helper to retrieve all TutorialDocument of the given IDs. All tutorials will be fetched in parallel with a
Parameters :
Returns :
Promise<Tutorial[]>
Tutorials matching the given IDs. |
Private Async getAllUsersWithUsername | ||||||||
getAllUsersWithUsername(username: string)
|
||||||||
Defined in src/module/user/user.service.ts:388
|
||||||||
Parameters :
Returns :
Promise<User[]>
All users with that username. Array is empty if there are no users with that username. |
Async getLoggedInUserInformation | ||||||||
getLoggedInUserInformation(id: string)
|
||||||||
Defined in src/module/user/user.service.ts:265
|
||||||||
Collects and returns the information about a user which just logged in and needs it's information.
Parameters :
Returns :
Promise<ILoggedInUser>
Information for the user on logging in. |
Async getNamesOfAllTutors |
getNamesOfAllTutors()
|
Defined in src/module/user/user.service.ts:292
|
Returns :
Promise<NamedElement[]>
The name of all users with the |
Private Async getUserWithUsername | ||||||||
getUserWithUsername(username: string)
|
||||||||
Defined in src/module/user/user.service.ts:370
|
||||||||
Returns the user with the given username. If there is no user with that username a
Parameters :
Returns :
Promise<User>
User with that username. |
Async onApplicationBootstrap |
onApplicationBootstrap()
|
Decorators :
@CreateRequestContext()
|
Defined in src/module/user/user.service.ts:43
|
Creates a new administrator on application start if there are no users present in the DB.
Returns :
Promise<void>
|
Async setPassword | ||||||||||||
setPassword(id: string, password: string)
|
||||||||||||
Defined in src/module/user/user.service.ts:224
|
||||||||||||
Sets the password of the given user to the given one. This will remove the If one wants to set the temporary password aswell one should use the
Parameters :
Returns :
Promise<User>
Updated UserDocument |
Async setTemporaryPassword | ||||||||||||
setTemporaryPassword(id: string, password: string)
|
||||||||||||
Defined in src/module/user/user.service.ts:246
|
||||||||||||
Sets the password and the temporary password of the user to the given one. If one only wants to set the password while removing the temporary one, one should use the
Parameters :
Returns :
Promise<User>
Updated User |
Async update | ||||||||||||
update(id: string, dto: UserDTO)
|
||||||||||||
Defined in src/module/user/user.service.ts:168
|
||||||||||||
Updates the user with the given information If necessary this functions updates all related tutorials and saves them afterwards. Related tutorials can be:
Parameters :
Returns :
Promise<IUser>
Updated user. |
Private Readonly logger |
Default value : new Logger(UserService.name)
|
Defined in src/module/user/user.service.ts:26
|
import { Collection, CreateRequestContext, EntityRepository, MikroORM } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/mysql';
import { InjectRepository } from '@mikro-orm/nestjs';
import {
BadRequestException,
forwardRef,
Inject,
Injectable,
Logger,
NotFoundException,
OnApplicationBootstrap,
} from '@nestjs/common';
import { NamedElement } from 'shared/model/Common';
import { Role } from 'shared/model/Role';
import { ILoggedInUser, IUser } from 'shared/model/User';
import { UserCredentialsWithPassword } from '../../auth/auth.model';
import { Tutorial } from '../../database/entities/tutorial.entity';
import { User } from '../../database/entities/user.entity';
import { CRUDService } from '../../helpers/CRUDService';
import { TutorialService } from '../tutorial/tutorial.service';
import { CreateUserDTO, UserDTO } from './user.dto';
import { updateCollection } from '../../helpers/updateCollection';
@Injectable()
export class UserService implements OnApplicationBootstrap, CRUDService<IUser, UserDTO, User> {
private readonly logger = new Logger(UserService.name);
constructor(
@Inject(forwardRef(() => TutorialService))
private readonly tutorialService: TutorialService,
private readonly entityManager: EntityManager,
private readonly orm: MikroORM,
@InjectRepository(User)
private readonly repository: EntityRepository<User>,
@Inject(EntityManager)
private readonly em: EntityManager
) {}
/**
* Creates a new administrator on application start if there are no users present in the DB.
*/
@CreateRequestContext()
async onApplicationBootstrap(): Promise<void> {
const areUsersPresent = (await this.repository.findAll()).length > 0;
if (!areUsersPresent) {
this.logger.log('No admin user found in database. Creating new admin...');
const user = await this.repository.create(
new User({
firstname: 'Created',
lastname: 'Admin',
roles: [Role.ADMIN],
username: 'admin',
password: 'adminPass',
temporaryPassword: 'adminPass',
email: 'admin@email.mail',
})
);
await this.em.persistAndFlush(user);
this.logger.log('Admin user successfully created.');
}
}
/**
* @returns All users saved in the database.
*/
async findAll(): Promise<User[]> {
return this.repository.findAll({ populate: ['*'] });
}
/**
* Searches for a user with the given ID and returns its document if possible.
*
* @param id ID to search for.
*
* @returns UserDocument with the given ID.
*
* @throws `NotFoundException` - If there is no user with the given ID.
*/
async findById(id: string): Promise<User> {
const user = await this.repository.findOne({ id }, { populate: ['*'] });
if (!user) {
throw new NotFoundException(`User with the ID '${id}' could not be found.`);
}
return user;
}
/**
* Returns the credentials of the user with the given username.
*
* If no user with that username exists a `NotFoundException` is thrown.
*
* @param usernameToFind Username to search
*
* @returns UserCredentials of the user.
*
* @throws `NotFoundException` - If no user could be found
*/
async findWithUsername(usernameToFind: string): Promise<UserCredentialsWithPassword> {
const { id, username, password, roles } = await this.getUserWithUsername(usernameToFind);
return { id, username, password, roles };
}
/**
* Creates a new user and saves it to the database.
*
* @param user Information to create the user from.
*
* @returns Created user.
*/
async create(user: CreateUserDTO): Promise<IUser> {
const createdUser = await this.createUser(user);
await this.em.persistAndFlush(createdUser);
return createdUser.toDTO();
}
/**
* Creates multiple new users from the given information and saves them to the database.
*
* @param users Information of all users to create.
*
* @returns Created users.
*/
async createMany(users: CreateUserDTO[]): Promise<IUser[]> {
const errors: string[] = [];
const toCreate: User[] = [];
for (const user of users) {
try {
const createdUser = await this.createUser(user);
toCreate.push(createdUser);
} catch (err) {
const message: string = err instanceof Error ? err.message : 'Unknown error';
errors.push(`[${user.lastname}, ${user.firstname}]: ${message}`);
}
}
if (errors.length === 0) {
await this.em.persistAndFlush(toCreate);
} else {
throw new BadRequestException(errors);
}
return toCreate.map((user) => user.toDTO());
}
/**
* Updates the user with the given information
*
* If necessary this functions updates all related tutorials and saves them afterwards. Related tutorials can be:
* - Tutorials of which the user _was_ the tutor.
* - Tutorials of which the user _will be_ the tutor.
* - Tutorials of which the user _was_ a corrector.
* - Tutorials of which the user _will be_ a corrector.
*
* @param id ID of the user to update.
* @param dto Information to update the user with.
*
* @returns Updated user.
*
* @throws `NotFoundException` - If there is no user with the given `id`.
* @throws `BadRequestException` - {@link UserService#checkUserDTO}
*/
async update(id: string, dto: UserDTO): Promise<IUser> {
const user = await this.findById(id);
await this.checkUserDTO(dto, user);
await this.assertUserIsChangeable(user, dto);
const [tutorials, tutorialsToCorrect] = await Promise.all([
this.getAllTutorials(dto.tutorials),
this.getAllTutorials(dto.tutorialsToCorrect),
]);
user.firstname = dto.firstname;
user.lastname = dto.lastname;
user.username = dto.username;
user.email = dto.email;
user.roles = dto.roles;
updateCollection(user.tutorials, tutorials);
updateCollection(user.tutorialsToCorrect, tutorialsToCorrect);
await this.em.persistAndFlush(user);
return user.toDTO();
}
/**
* Delete the user with the given ID if one exists.
*
* @param id ID of the user to delete.
*
* @returns Deleted document.
*
* @throws `NotFoundException` - If there is no user with such an ID.
* @throws `BadRequestException` - If the deleted user is the last available ADMIN.
*/
async delete(id: string): Promise<void> {
const user = await this.findById(id);
await this.assertUserIsDeletable(user);
user.tutorials.removeAll();
user.tutorialsToCorrect.removeAll();
user.tutorialsToSubstitute.removeAll();
await this.em.removeAndFlush(user);
}
/**
* Sets the password of the given user to the given one. This will remove the `temporaryPassword` from the user.
*
* If one wants to set the temporary password aswell one should use the `setTemporaryPassword()` function.
*
* @param id ID of the user.
* @param password (New) password.
*
* @returns Updated UserDocument
*
* @throws `NotFoundException` - If no user with the given ID could be found.
*/
async setPassword(id: string, password: string): Promise<User> {
const user = await this.findById(id);
user.password = password;
user.temporaryPassword = undefined;
await this.em.persistAndFlush(user);
return user;
}
/**
* Sets the password _and_ the temporary password of the user to the given one.
*
* If one only wants to set the password while removing the temporary one, one should use the `setPassword()` function.
*
* @param id ID of the user.
* @param password (New) password.
*
* @returns Updated User
*
* @throws `NotFoundException` - If no user with the given ID could be found.
*/
async setTemporaryPassword(id: string, password: string): Promise<User> {
const user = await this.findById(id);
user.password = password;
user.temporaryPassword = password;
await this.em.persistAndFlush(user);
return user;
}
/**
* Collects and returns the information about a user which just logged in and needs it's information.
*
* @param id ID of the user to get the information on login for.
*
* @returns Information for the user on logging in.
*
* @throws `NotFoundException` - If no user with the given ID could be found.
*/
async getLoggedInUserInformation(id: string): Promise<ILoggedInUser> {
const user = await this.findById(id);
const {
id: userId,
firstname,
lastname,
roles,
temporaryPassword,
tutorials,
tutorialsToCorrect,
} = user.toDTO();
return {
id: userId,
firstname,
lastname,
roles,
substituteTutorials: user.getSubstituteInformation(),
hasTemporaryPassword: !!temporaryPassword,
tutorials,
tutorialsToCorrect,
};
}
/**
* @returns The name of all users with the `TUTOR` role in the database.
*/
async getNamesOfAllTutors(): Promise<NamedElement[]> {
const users = await this.findAll();
return users
.filter((u) => u.roles.includes(Role.TUTOR))
.map<NamedElement>(({ id, firstname, lastname }) => ({
id,
firstname,
lastname,
}));
}
/**
* Creates a new user based on the given information.
*
* This function also updates all related tutorials which the user is tutor or corrector of.
*
* @param user Information about the user to create.
*
* @returns Created UserDocument.
* @throws `BadRequestException` - If the DTO is invalid a BadRequestException is thrown. More information: See function `checkUserDTO()`.
*/
private async createUser(user: CreateUserDTO): Promise<User> {
await this.checkUserDTO(user);
const {
tutorials: tutorialIds,
tutorialsToCorrect: toCorrectIds,
password,
username,
...dto
} = user;
const [tutorials, tutorialsToCorrect] = await Promise.all([
this.getAllTutorials(tutorialIds),
this.getAllTutorials(toCorrectIds),
]);
const userEntity: User = new User({ ...dto, username, password });
userEntity.temporaryPassword = password;
userEntity.tutorials.add(tutorials);
userEntity.tutorialsToCorrect = new Collection<Tutorial>(userEntity, tutorialsToCorrect);
return userEntity;
}
/**
* Checks if there is already a user with the given username saved in the database. If a `user` is provided that user is ignored during the check.
*
* @param username Username to check
* @param user (optional) User object which is allowed to have that username.
* @returns Is there already a user with that username?
*/
private async doesUserWithUsernameExist(username: string, user?: User): Promise<boolean> {
const usersWithUsername: User[] = await this.getAllUsersWithUsername(username);
if (!user) {
return usersWithUsername.length > 0;
}
for (const sameUsernameUser of usersWithUsername) {
if (sameUsernameUser.id !== user.id) {
return true;
}
}
return false;
}
/**
* Returns the user with the given username.
*
* If there is no user with that username a `NotFoundException` is thrown.
*
* @param username Username to search for.
*
* @returns User with that username.
*
* @throws `NotFoundException` - If there is no user with that username.
*/
private async getUserWithUsername(username: string): Promise<User> {
const usersWithUserName = await this.getAllUsersWithUsername(username);
const user = usersWithUserName[0];
if (!user) {
throw new NotFoundException(`User with username "${username}" was not found`);
}
return user;
}
/**
*
* @param username Username to get users for.
* @returns All users with that username. Array is empty if there are no users with that username.
*
* @private
*/
private async getAllUsersWithUsername(username: string): Promise<User[]> {
const allUsers = await this.repository.findAll({ populate: false });
return allUsers.filter((u) => u.username === username);
}
/**
* Helper to retrieve all TutorialDocument of the given IDs.
*
* All tutorials will be fetched in parallel with a `Promise.all` collecting them all. The tutorials retrieved will match the order of their provided IDs.
*
* @param ids IDs of the tutorials.
*
* @returns Tutorials matching the given IDs.
*/
private async getAllTutorials(ids: string[]): Promise<Tutorial[]> {
return this.tutorialService.findMultiple(ids);
}
/**
* Checks if some more complex conditions apply to the given DTO:
*
* - There is no _other_ user with the `username`.
* - If the user has `tutorials` he/she needs the TUTOR role aswell.
* - If the user has `tutorialsToCorrect` he/she needs the CORRECTOR role aswell.
*
* If all conditions apply nothing happens else an exception is thrown.
*
* @param dto DTO with information to create / update a user
* @param user (optional) User with the same username as the one to check. Should be provided to prevent false positives on updating an already existing user.
*
* @throws `BadRequestException` - If _any_ of the above conditions is violated a `BadRequestException` is thrown.
*/
private async checkUserDTO(
{ tutorials, tutorialsToCorrect, username, roles }: UserDTO,
user?: User
) {
if (await this.doesUserWithUsernameExist(username, user)) {
throw new BadRequestException(`A user with the username '${username}' already exists.`);
}
if (tutorials.length > 0 && !roles.includes(Role.TUTOR)) {
throw new BadRequestException(`A user with tutorials needs to have the 'TUTOR' role`);
}
if (tutorialsToCorrect.length > 0 && !roles.includes(Role.CORRECTOR)) {
throw new BadRequestException(
`A user with tutorials to correct needs to have the 'CORRECTOR' role`
);
}
}
/**
* Checks if the user can be safely updated.
*
* A user is considered not NOT updatable if
* - It is the last user holding the ADMIN role and that role would get removed during the update.
*
* @param user User which should get updated.
* @param dto New data of the user if it would get updated.
*
* @throws `BadRequestException` - If the user is considered _NOT_ updatable.
*/
private async assertUserIsChangeable(user: User, dto: UserDTO) {
if (user.roles.includes(Role.ADMIN) && !dto.roles.includes(Role.ADMIN)) {
const allUsers = await this.findAll();
const adminUsers = allUsers.filter((u) => u.roles.includes(Role.ADMIN));
if (adminUsers.length <= 1) {
throw new BadRequestException('ERR_REMOVE_LAST_ADMIN_ROLE');
}
}
}
/**
* Checks if the user can be safely deleted.
*
* A user is considered NOT deletable if
* - It is the last user holding the ADMIN role.
*
* @param user User to check.
*
* @throws `BadRequestException` - If the user is the last admin.
*/
private async assertUserIsDeletable(user: User) {
if (user.roles.includes(Role.ADMIN)) {
const allUsers = await this.findAll();
const adminUsers = allUsers.filter((u) => u.roles.includes(Role.ADMIN));
// Do NOT allow deleting the last admin.
if (adminUsers.length <= 1) {
throw new BadRequestException('ERR_DELETE_LAST_ADMIN');
}
}
}
}