2022-01-06 20:43:57 +01:00
|
|
|
import bcrypt from 'bcryptjs';
|
2021-04-09 13:46:53 +02:00
|
|
|
import owasp from 'owasp-password-strength-test';
|
|
|
|
import Joi from 'joi';
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
import { URL } from 'url';
|
2021-04-09 13:46:53 +02:00
|
|
|
import { Logger } from '../logger';
|
2021-04-22 23:40:52 +02:00
|
|
|
import User, { IUser } from '../types/user';
|
2021-04-09 13:46:53 +02:00
|
|
|
import isEmail from '../util/is-email';
|
2021-08-12 15:04:37 +02:00
|
|
|
import { AccessService } from './access-service';
|
2021-04-16 15:29:23 +02:00
|
|
|
import ResetTokenService from './reset-token-service';
|
|
|
|
import InvalidTokenError from '../error/invalid-token-error';
|
|
|
|
import NotFoundError from '../error/notfound-error';
|
|
|
|
import OwaspValidationError from '../error/owasp-validation-error';
|
|
|
|
import { EmailService } from './email-service';
|
2021-04-22 10:07:10 +02:00
|
|
|
import { IUnleashConfig } from '../types/option';
|
2021-04-27 09:16:44 +02:00
|
|
|
import SessionService from './session-service';
|
|
|
|
import { IUnleashStores } from '../types/stores';
|
2021-04-27 15:35:10 +02:00
|
|
|
import PasswordUndefinedError from '../error/password-undefined';
|
2021-04-29 10:21:29 +02:00
|
|
|
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
|
2021-08-12 15:04:37 +02:00
|
|
|
import { IEventStore } from '../types/stores/event-store';
|
2022-06-22 14:55:43 +02:00
|
|
|
import { IUserStore } from '../types/stores/user-store';
|
2021-08-12 15:04:37 +02:00
|
|
|
import { RoleName } from '../types/model';
|
2021-10-29 10:25:42 +02:00
|
|
|
import SettingService from './setting-service';
|
|
|
|
import { SimpleAuthSettings } from '../server-impl';
|
2022-08-26 09:09:48 +02:00
|
|
|
import { simpleAuthSettingsKey } from '../types/settings/simple-auth-settings';
|
2022-01-26 13:45:22 +01:00
|
|
|
import DisabledError from '../error/disabled-error';
|
|
|
|
import PasswordMismatch from '../error/password-mismatch';
|
2022-06-22 14:55:43 +02:00
|
|
|
import BadDataError from '../error/bad-data-error';
|
|
|
|
import { isDefined } from '../util/isDefined';
|
2022-06-22 15:37:26 +02:00
|
|
|
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
2021-04-27 20:47:11 +02:00
|
|
|
|
|
|
|
const systemUser = new User({ id: -1, username: 'system' });
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
export interface ICreateUser {
|
|
|
|
name?: string;
|
|
|
|
email?: string;
|
|
|
|
username?: string;
|
|
|
|
password?: string;
|
2021-04-30 13:25:24 +02:00
|
|
|
rootRole: number | RoleName;
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface IUpdateUser {
|
|
|
|
id: number;
|
|
|
|
name?: string;
|
|
|
|
email?: string;
|
2021-04-30 13:25:24 +02:00
|
|
|
rootRole?: number | RoleName;
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
2021-08-23 12:11:29 +02:00
|
|
|
export interface ILoginUserRequest {
|
|
|
|
email: string;
|
|
|
|
name?: string;
|
|
|
|
rootRole?: number | RoleName;
|
|
|
|
autoCreate?: boolean;
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
interface IUserWithRole extends IUser {
|
|
|
|
rootRole: number;
|
|
|
|
}
|
2022-06-22 14:55:43 +02:00
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
const saltRounds = 10;
|
|
|
|
|
|
|
|
class UserService {
|
|
|
|
private logger: Logger;
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
private store: IUserStore;
|
2021-04-09 13:46:53 +02:00
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
private eventStore: IEventStore;
|
2021-04-27 20:47:11 +02:00
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
private accessService: AccessService;
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
private resetTokenService: ResetTokenService;
|
|
|
|
|
2021-04-27 09:16:44 +02:00
|
|
|
private sessionService: SessionService;
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
private emailService: EmailService;
|
|
|
|
|
2021-10-29 10:25:42 +02:00
|
|
|
private settingService: SettingService;
|
|
|
|
|
2022-09-28 10:24:43 +02:00
|
|
|
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
constructor(
|
2021-04-27 20:47:11 +02:00
|
|
|
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
|
2021-04-22 10:07:10 +02:00
|
|
|
{
|
|
|
|
getLogger,
|
|
|
|
authentication,
|
2022-12-12 15:32:35 +01:00
|
|
|
}: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
2021-08-12 15:04:37 +02:00
|
|
|
services: {
|
|
|
|
accessService: AccessService;
|
|
|
|
resetTokenService: ResetTokenService;
|
|
|
|
emailService: EmailService;
|
|
|
|
sessionService: SessionService;
|
2021-10-29 10:25:42 +02:00
|
|
|
settingService: SettingService;
|
2021-08-12 15:04:37 +02:00
|
|
|
},
|
2021-04-09 13:46:53 +02:00
|
|
|
) {
|
2021-04-22 10:07:10 +02:00
|
|
|
this.logger = getLogger('service/user-service.js');
|
2021-04-09 13:46:53 +02:00
|
|
|
this.store = stores.userStore;
|
2021-04-27 20:47:11 +02:00
|
|
|
this.eventStore = stores.eventStore;
|
2021-08-12 15:04:37 +02:00
|
|
|
this.accessService = services.accessService;
|
|
|
|
this.resetTokenService = services.resetTokenService;
|
|
|
|
this.emailService = services.emailService;
|
|
|
|
this.sessionService = services.sessionService;
|
2021-10-29 10:25:42 +02:00
|
|
|
this.settingService = services.settingService;
|
2021-04-22 10:07:10 +02:00
|
|
|
if (authentication && authentication.createAdminUser) {
|
2021-04-09 13:46:53 +02:00
|
|
|
process.nextTick(() => this.initAdminUser());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
validatePassword(password: string): boolean {
|
2021-04-27 15:35:10 +02:00
|
|
|
if (password) {
|
|
|
|
const result = owasp.test(password);
|
|
|
|
if (!result.strong) {
|
|
|
|
throw new OwaspValidationError(result);
|
|
|
|
} else return true;
|
|
|
|
} else {
|
|
|
|
throw new PasswordUndefinedError();
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async initAdminUser(): Promise<void> {
|
2021-10-12 21:27:06 +02:00
|
|
|
const userCount = await this.store.count();
|
2021-04-09 13:46:53 +02:00
|
|
|
|
2021-10-12 21:27:06 +02:00
|
|
|
if (userCount === 0) {
|
2021-04-09 13:46:53 +02:00
|
|
|
// create default admin user
|
|
|
|
try {
|
2021-04-26 11:28:51 +02:00
|
|
|
const pwd = 'unleash4all';
|
2021-04-09 13:46:53 +02:00
|
|
|
this.logger.info(
|
2021-04-26 11:28:51 +02:00
|
|
|
`Creating default user "admin" with password "${pwd}"`,
|
2021-04-09 13:46:53 +02:00
|
|
|
);
|
2021-04-22 23:40:52 +02:00
|
|
|
const user = await this.store.insert({
|
|
|
|
username: 'admin',
|
|
|
|
});
|
2021-04-26 11:28:51 +02:00
|
|
|
const passwordHash = await bcrypt.hash(pwd, saltRounds);
|
2021-04-09 13:46:53 +02:00
|
|
|
await this.store.setPasswordHash(user.id, passwordHash);
|
2021-08-12 15:04:37 +02:00
|
|
|
await this.accessService.setUserRootRole(
|
|
|
|
user.id,
|
|
|
|
RoleName.ADMIN,
|
|
|
|
);
|
2021-04-09 13:46:53 +02:00
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('Unable to create default user "admin"');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async getAll(): Promise<IUserWithRole[]> {
|
|
|
|
const users = await this.store.getAll();
|
2021-04-22 10:07:10 +02:00
|
|
|
const defaultRole = await this.accessService.getRootRole(
|
|
|
|
RoleName.VIEWER,
|
|
|
|
);
|
2021-04-09 13:46:53 +02:00
|
|
|
const userRoles = await this.accessService.getRootRoleForAllUsers();
|
2021-08-12 15:04:37 +02:00
|
|
|
const usersWithRootRole = users.map((u) => {
|
|
|
|
const rootRole = userRoles.find((r) => r.userId === u.id);
|
2021-04-09 13:46:53 +02:00
|
|
|
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
|
|
|
return { ...u, rootRole: roleId };
|
2023-01-18 13:12:44 +01:00
|
|
|
});
|
|
|
|
return usersWithRootRole;
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
async getUser(id: number): Promise<IUserWithRole> {
|
|
|
|
const roles = await this.accessService.getUserRootRoles(id);
|
2021-04-22 10:07:10 +02:00
|
|
|
const defaultRole = await this.accessService.getRootRole(
|
|
|
|
RoleName.VIEWER,
|
|
|
|
);
|
2021-04-09 13:46:53 +02:00
|
|
|
const roleId = roles.length > 0 ? roles[0].id : defaultRole.id;
|
2021-08-12 15:04:37 +02:00
|
|
|
const user = await this.store.get(id);
|
2021-04-09 13:46:53 +02:00
|
|
|
return { ...user, rootRole: roleId };
|
|
|
|
}
|
|
|
|
|
2022-06-22 14:55:43 +02:00
|
|
|
async search(query: string): Promise<IUser[]> {
|
2021-04-09 13:46:53 +02:00
|
|
|
return this.store.search(query);
|
|
|
|
}
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
async getByEmail(email: string): Promise<IUser> {
|
|
|
|
return this.store.getByQuery({ email });
|
2021-04-16 15:29:23 +02:00
|
|
|
}
|
|
|
|
|
2021-04-27 20:47:11 +02:00
|
|
|
async createUser(
|
|
|
|
{ username, email, name, password, rootRole }: ICreateUser,
|
|
|
|
updatedBy?: User,
|
2021-08-12 15:04:37 +02:00
|
|
|
): Promise<IUser> {
|
2022-06-22 14:55:43 +02:00
|
|
|
if (!username && !email) {
|
|
|
|
throw new BadDataError('You must specify username or email');
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
if (email) {
|
|
|
|
Joi.assert(email, Joi.string().email(), 'Email');
|
|
|
|
}
|
|
|
|
|
|
|
|
const exists = await this.store.hasUser({ username, email });
|
|
|
|
if (exists) {
|
|
|
|
throw new Error('User already exists');
|
|
|
|
}
|
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
const user = await this.store.insert({
|
|
|
|
username,
|
|
|
|
email,
|
|
|
|
name,
|
|
|
|
});
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
await this.accessService.setUserRootRole(user.id, rootRole);
|
|
|
|
|
|
|
|
if (password) {
|
|
|
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
|
|
|
await this.store.setPasswordHash(user.id, passwordHash);
|
|
|
|
}
|
|
|
|
|
2021-11-12 13:15:51 +01:00
|
|
|
await this.eventStore.store({
|
|
|
|
type: USER_CREATED,
|
|
|
|
createdBy: this.getCreatedBy(updatedBy),
|
|
|
|
data: this.mapUserToData(user),
|
|
|
|
});
|
2021-04-27 20:47:11 +02:00
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
2021-11-12 13:15:51 +01:00
|
|
|
private getCreatedBy(updatedBy: User = systemUser) {
|
|
|
|
return updatedBy.username || updatedBy.email;
|
|
|
|
}
|
|
|
|
|
|
|
|
private mapUserToData(user?: IUser): any {
|
|
|
|
if (!user) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
id: user.id,
|
|
|
|
name: user.name,
|
|
|
|
username: user.username,
|
|
|
|
email: user.email,
|
|
|
|
};
|
2021-04-27 20:47:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async updateUser(
|
|
|
|
{ id, name, email, rootRole }: IUpdateUser,
|
|
|
|
updatedBy?: User,
|
2021-08-12 15:04:37 +02:00
|
|
|
): Promise<IUser> {
|
2021-11-12 13:15:51 +01:00
|
|
|
const preUser = await this.store.get(id);
|
|
|
|
|
2022-06-22 14:55:43 +02:00
|
|
|
if (email) {
|
|
|
|
Joi.assert(email, Joi.string().email(), 'Email');
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
if (rootRole) {
|
|
|
|
await this.accessService.setUserRootRole(id, rootRole);
|
|
|
|
}
|
|
|
|
|
2022-06-22 14:55:43 +02:00
|
|
|
const payload: Partial<IUser> = {
|
|
|
|
name: name || preUser.name,
|
|
|
|
email: email || preUser.email,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Empty updates will throw, so make sure we have something to update.
|
|
|
|
const user = Object.values(payload).some(isDefined)
|
|
|
|
? await this.store.update(id, payload)
|
|
|
|
: preUser;
|
2021-04-27 20:47:11 +02:00
|
|
|
|
2021-11-12 13:15:51 +01:00
|
|
|
await this.eventStore.store({
|
|
|
|
type: USER_UPDATED,
|
|
|
|
createdBy: this.getCreatedBy(updatedBy),
|
|
|
|
data: this.mapUserToData(user),
|
|
|
|
preData: this.mapUserToData(preUser),
|
|
|
|
});
|
2021-04-27 20:47:11 +02:00
|
|
|
|
|
|
|
return user;
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
2021-11-12 13:15:51 +01:00
|
|
|
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
|
|
|
|
const user = await this.store.get(userId);
|
2022-11-23 08:30:54 +01:00
|
|
|
await this.accessService.wipeUserPermissions(userId);
|
2021-11-12 13:15:51 +01:00
|
|
|
await this.sessionService.deleteSessionsForUser(userId);
|
|
|
|
|
|
|
|
await this.store.delete(userId);
|
|
|
|
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: USER_DELETED,
|
|
|
|
createdBy: this.getCreatedBy(updatedBy),
|
|
|
|
preData: this.mapUserToData(user),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
2021-10-29 10:25:42 +02:00
|
|
|
const settings = await this.settingService.get<SimpleAuthSettings>(
|
2022-08-26 09:09:48 +02:00
|
|
|
simpleAuthSettingsKey,
|
2021-10-29 10:25:42 +02:00
|
|
|
);
|
|
|
|
|
2022-01-26 13:45:22 +01:00
|
|
|
if (settings?.disabled) {
|
|
|
|
throw new DisabledError(
|
2021-10-29 10:25:42 +02:00
|
|
|
'Logging in with username/password has been disabled.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
const idQuery = isEmail(usernameOrEmail)
|
|
|
|
? { email: usernameOrEmail }
|
|
|
|
: { username: usernameOrEmail };
|
2021-08-12 15:04:37 +02:00
|
|
|
const user = await this.store.getByQuery(idQuery);
|
2021-04-09 13:46:53 +02:00
|
|
|
const passwordHash = await this.store.getPasswordHash(user.id);
|
|
|
|
|
|
|
|
const match = await bcrypt.compare(password, passwordHash);
|
|
|
|
if (match) {
|
|
|
|
await this.store.successfullyLogin(user);
|
|
|
|
return user;
|
|
|
|
}
|
2022-01-26 13:45:22 +01:00
|
|
|
throw new PasswordMismatch();
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to login users without specifying password. Used when integrating
|
|
|
|
* with external identity providers.
|
|
|
|
*
|
|
|
|
* @param usernameOrEmail
|
|
|
|
* @param autoCreateUser
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
async loginUserWithoutPassword(
|
|
|
|
email: string,
|
|
|
|
autoCreateUser: boolean = false,
|
2021-08-12 15:04:37 +02:00
|
|
|
): Promise<IUser> {
|
2021-08-23 12:11:29 +02:00
|
|
|
return this.loginUserSSO({ email, autoCreate: autoCreateUser });
|
|
|
|
}
|
|
|
|
|
|
|
|
async loginUserSSO({
|
|
|
|
email,
|
|
|
|
name,
|
|
|
|
rootRole,
|
|
|
|
autoCreate = false,
|
|
|
|
}: ILoginUserRequest): Promise<IUser> {
|
2021-08-12 15:04:37 +02:00
|
|
|
let user: IUser;
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
try {
|
2021-08-12 15:04:37 +02:00
|
|
|
user = await this.store.getByQuery({ email });
|
2021-08-23 12:11:29 +02:00
|
|
|
// Update user if autCreate is enabled.
|
2021-08-25 12:43:42 +02:00
|
|
|
if (name && user.name !== name) {
|
2021-08-23 12:11:29 +02:00
|
|
|
user = await this.store.update(user.id, { name, email });
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
} catch (e) {
|
2021-08-23 12:11:29 +02:00
|
|
|
// User does not exists. Create if "autoCreate" is enabled
|
|
|
|
if (autoCreate) {
|
2021-04-09 13:46:53 +02:00
|
|
|
user = await this.createUser({
|
|
|
|
email,
|
2021-08-23 12:11:29 +02:00
|
|
|
name,
|
|
|
|
rootRole: rootRole || RoleName.EDITOR,
|
2021-04-09 13:46:53 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2022-09-23 14:19:17 +02:00
|
|
|
await this.store.successfullyLogin(user);
|
2021-04-09 13:46:53 +02:00
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
|
|
|
async changePassword(userId: number, password: string): Promise<void> {
|
|
|
|
this.validatePassword(password);
|
|
|
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
2022-09-23 14:19:17 +02:00
|
|
|
await this.store.setPasswordHash(userId, passwordHash);
|
|
|
|
await this.sessionService.deleteSessionsForUser(userId);
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
2022-06-22 15:37:26 +02:00
|
|
|
async getUserForToken(token: string): Promise<TokenUserSchema> {
|
2021-04-16 15:29:23 +02:00
|
|
|
const { createdBy, userId } = await this.resetTokenService.isValid(
|
|
|
|
token,
|
|
|
|
);
|
|
|
|
const user = await this.getUser(userId);
|
2022-01-13 11:14:17 +01:00
|
|
|
const role = await this.accessService.getRoleData(user.rootRole);
|
2021-04-16 15:29:23 +02:00
|
|
|
return {
|
|
|
|
token,
|
|
|
|
createdBy,
|
|
|
|
email: user.email,
|
|
|
|
name: user.name,
|
|
|
|
id: user.id,
|
|
|
|
role: {
|
2022-06-22 14:55:43 +02:00
|
|
|
id: user.rootRole,
|
2021-04-16 15:29:23 +02:00
|
|
|
description: role.role.description,
|
|
|
|
type: role.role.type,
|
|
|
|
name: role.role.name,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-27 09:16:44 +02:00
|
|
|
/**
|
|
|
|
* If the password is a strong password will update password and delete all sessions for the user we're changing the password for
|
|
|
|
* @param token - the token authenticating this request
|
|
|
|
* @param password - new password
|
|
|
|
*/
|
2021-04-16 15:29:23 +02:00
|
|
|
async resetPassword(token: string, password: string): Promise<void> {
|
|
|
|
this.validatePassword(password);
|
|
|
|
const user = await this.getUserForToken(token);
|
|
|
|
const allowed = await this.resetTokenService.useAccessToken({
|
|
|
|
userId: user.id,
|
|
|
|
token,
|
|
|
|
});
|
|
|
|
if (allowed) {
|
|
|
|
await this.changePassword(user.id, password);
|
2021-04-27 09:16:44 +02:00
|
|
|
await this.sessionService.deleteSessionsForUser(user.id);
|
2021-04-16 15:29:23 +02:00
|
|
|
} else {
|
|
|
|
throw new InvalidTokenError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async createResetPasswordEmail(
|
|
|
|
receiverEmail: string,
|
2021-04-27 20:47:11 +02:00
|
|
|
user: User = systemUser,
|
2021-04-16 15:29:23 +02:00
|
|
|
): Promise<URL> {
|
|
|
|
const receiver = await this.getByEmail(receiverEmail);
|
|
|
|
if (!receiver) {
|
|
|
|
throw new NotFoundError(`Could not find ${receiverEmail}`);
|
|
|
|
}
|
2022-09-28 10:24:43 +02:00
|
|
|
if (this.passwordResetTimeouts[receiver.id]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
const resetLink = await this.resetTokenService.createResetPasswordUrl(
|
|
|
|
receiver.id,
|
2021-04-27 20:47:11 +02:00
|
|
|
user.username || user.email,
|
2021-04-16 15:29:23 +02:00
|
|
|
);
|
|
|
|
|
2022-09-28 10:24:43 +02:00
|
|
|
this.passwordResetTimeouts[receiver.id] = setTimeout(() => {
|
|
|
|
delete this.passwordResetTimeouts[receiver.id];
|
|
|
|
}, 1000 * 60); // 1 minute
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
await this.emailService.sendResetMail(
|
|
|
|
receiver.name,
|
|
|
|
receiver.email,
|
|
|
|
resetLink.toString(),
|
|
|
|
);
|
|
|
|
return resetLink;
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = UserService;
|
|
|
|
export default UserService;
|