mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 22:54:42 -04:00
216 lines
7.1 KiB
TypeScript
216 lines
7.1 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import {
|
|
OptionsGraph,
|
|
OptionsType,
|
|
zxcvbnAsync,
|
|
zxcvbnOptions,
|
|
} from '@zxcvbn-ts/core';
|
|
import {
|
|
adjacencyGraphs,
|
|
dictionary as zxcvbnCommonDictionary,
|
|
} from '@zxcvbn-ts/language-common';
|
|
import {
|
|
dictionary as zxcvbnEnDictionary,
|
|
translations as zxcvbnEnTranslations,
|
|
} from '@zxcvbn-ts/language-en';
|
|
import { Repository } from 'typeorm';
|
|
|
|
import authConfiguration, { AuthConfig } from '../config/auth.config';
|
|
import {
|
|
InvalidCredentialsError,
|
|
NoLocalIdentityError,
|
|
NotInDBError,
|
|
PasswordTooWeakError,
|
|
} from '../errors/errors';
|
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
|
import { User } from '../users/user.entity';
|
|
import { checkPassword, hashPassword } from '../utils/password';
|
|
import { Identity } from './identity.entity';
|
|
import { ProviderType } from './provider-type.enum';
|
|
import { getFirstIdentityFromUser } from './utils';
|
|
|
|
@Injectable()
|
|
export class IdentityService {
|
|
constructor(
|
|
private readonly logger: ConsoleLoggerService,
|
|
@InjectRepository(Identity)
|
|
private identityRepository: Repository<Identity>,
|
|
@Inject(authConfiguration.KEY)
|
|
private authConfig: AuthConfig,
|
|
) {
|
|
this.logger.setContext(IdentityService.name);
|
|
const options: OptionsType = {
|
|
dictionary: {
|
|
...zxcvbnCommonDictionary,
|
|
...zxcvbnEnDictionary,
|
|
},
|
|
graphs: adjacencyGraphs as OptionsGraph,
|
|
translations: zxcvbnEnTranslations,
|
|
};
|
|
zxcvbnOptions.setOptions(options);
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Retrieve an identity by userId and providerType.
|
|
* @param {string} userId - the userId of the wanted identity
|
|
* @param {ProviderType} providerType - the providerType of the wanted identity
|
|
*/
|
|
async getIdentityFromUserIdAndProviderType(
|
|
userId: string,
|
|
providerType: ProviderType,
|
|
): Promise<Identity> {
|
|
const identity = await this.identityRepository.findOne({
|
|
where: {
|
|
providerUserId: userId,
|
|
providerType: providerType,
|
|
},
|
|
relations: ['user'],
|
|
});
|
|
if (identity === null) {
|
|
throw new NotInDBError(`Identity for user id '${userId}' not found`);
|
|
}
|
|
return identity;
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Update the given Identity with the given information
|
|
* @param {Identity} identity - the identity to update
|
|
* @param {string | undefined} displayName - the displayName to update the user with
|
|
* @param {string | undefined} email - the email to update the user with
|
|
* @param {string | undefined} profilePicture - the profilePicture to update the user with
|
|
*/
|
|
async updateIdentity(
|
|
identity: Identity,
|
|
displayName?: string,
|
|
email?: string,
|
|
profilePicture?: string,
|
|
): Promise<Identity> {
|
|
if (identity.syncSource) {
|
|
// The identity is the syncSource and the user should be changed accordingly
|
|
const user = await identity.user;
|
|
let shouldSave = false;
|
|
if (displayName) {
|
|
user.displayName = displayName;
|
|
shouldSave = true;
|
|
}
|
|
if (email) {
|
|
user.email = email;
|
|
shouldSave = true;
|
|
}
|
|
if (profilePicture) {
|
|
// ToDo: sync image (https://github.com/hedgedoc/hedgedoc/issues/5032)
|
|
}
|
|
if (shouldSave) {
|
|
identity.user = Promise.resolve(user);
|
|
return await this.identityRepository.save(identity);
|
|
}
|
|
}
|
|
return identity;
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Create a new generic identity.
|
|
* @param {User} user - the user the identity should be added to
|
|
* @param {ProviderType} providerType - the providerType of the identity
|
|
* @param {string} userId - the userId the identity should have
|
|
* @return {Identity} the new local identity
|
|
*/
|
|
async createIdentity(
|
|
user: User,
|
|
providerType: ProviderType,
|
|
userId: string,
|
|
): Promise<Identity> {
|
|
const identity = Identity.create(user, providerType, false);
|
|
identity.providerUserId = userId;
|
|
return await this.identityRepository.save(identity);
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Create a new identity for internal auth
|
|
* @param {User} user - the user the identity should be added to
|
|
* @param {string} password - the password the identity should have
|
|
* @return {Identity} the new local identity
|
|
*/
|
|
async createLocalIdentity(user: User, password: string): Promise<Identity> {
|
|
const identity = Identity.create(user, ProviderType.LOCAL, false);
|
|
identity.passwordHash = await hashPassword(password);
|
|
return await this.identityRepository.save(identity);
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Update the internal password of the specified the user
|
|
* @param {User} user - the user, which identity should be updated
|
|
* @param {string} newPassword - the new password
|
|
* @throws {NoLocalIdentityError} the specified user has no internal identity
|
|
* @return {Identity} the changed identity
|
|
*/
|
|
async updateLocalPassword(
|
|
user: User,
|
|
newPassword: string,
|
|
): Promise<Identity> {
|
|
const internalIdentity: Identity | undefined =
|
|
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
|
|
if (internalIdentity === undefined) {
|
|
this.logger.debug(
|
|
`The user with the username ${user.username} does not have a internal identity.`,
|
|
'updateLocalPassword',
|
|
);
|
|
throw new NoLocalIdentityError('This user has no internal identity.');
|
|
}
|
|
await this.checkPasswordStrength(newPassword);
|
|
internalIdentity.passwordHash = await hashPassword(newPassword);
|
|
return await this.identityRepository.save(internalIdentity);
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Checks if the user and password combination matches
|
|
* @param {User} user - the user to use
|
|
* @param {string} password - the password to use
|
|
* @throws {InvalidCredentialsError} the password and user do not match
|
|
* @throws {NoLocalIdentityError} the specified user has no internal identity
|
|
*/
|
|
async checkLocalPassword(user: User, password: string): Promise<void> {
|
|
const internalIdentity: Identity | undefined =
|
|
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
|
|
if (internalIdentity === undefined) {
|
|
this.logger.debug(
|
|
`The user with the username ${user.username} does not have a internal identity.`,
|
|
'checkLocalPassword',
|
|
);
|
|
throw new NoLocalIdentityError('This user has no internal identity.');
|
|
}
|
|
if (!(await checkPassword(password, internalIdentity.passwordHash ?? ''))) {
|
|
this.logger.debug(
|
|
`Password check for ${user.username} did not succeed.`,
|
|
'checkLocalPassword',
|
|
);
|
|
throw new InvalidCredentialsError('Password is not correct');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @async
|
|
* Check if the password is strong enough.
|
|
* This check is performed against the minimalPasswordStrength of the {@link AuthConfig}.
|
|
* @param {string} password - the password to check
|
|
* @throws {PasswordTooWeakError} the password is too weak
|
|
*/
|
|
async checkPasswordStrength(password: string): Promise<void> {
|
|
const result = await zxcvbnAsync(password);
|
|
if (result.score < this.authConfig.local.minimalPasswordStrength) {
|
|
throw new PasswordTooWeakError();
|
|
}
|
|
}
|
|
}
|