diff --git a/backend/src/alias/alias.service.ts b/backend/src/alias/alias.service.ts index 1bdd67a67..cb94b3b7d 100644 --- a/backend/src/alias/alias.service.ts +++ b/backend/src/alias/alias.service.ts @@ -7,8 +7,6 @@ import { AliasDto } from '@hedgedoc/commons'; import { Alias, FieldNameAlias, - FieldNameNote, - Note, TableAlias, TypeInsertAlias, } from '@hedgedoc/database'; @@ -60,12 +58,12 @@ export class AliasService { * @param noteId The id of the note to add the aliases to * @param alias The alias to add to the note * @param transaction The optional transaction to access the db - * @throws {AlreadyInDBError} The alias is already in use. - * @throws {ForbiddenIdError} The requested alias is forbidden + * @throws AlreadyInDBError The alias is already in use. + * @throws ForbiddenIdError The requested alias is forbidden */ async addAlias( - noteId: Note[FieldNameNote.id], - alias: Alias[FieldNameAlias.alias], + noteId: number, + alias: string, transaction?: Knex, ): Promise { const dbActor: Knex = transaction ? transaction : this.knex; @@ -89,14 +87,11 @@ export class AliasService { * * @param noteId The id of the note to change the primary alias * @param alias The alias to be the new primary alias of the note - * @throws {ForbiddenIdError} when the requested alias is forbidden - * @throws {NotInDBError} when the alias is not assigned to this note - * @throws {GenericDBError} when the database has an inconsistent state + * @throws ForbiddenIdError when the requested alias is forbidden + * @throws NotInDBError when the alias is not assigned to this note + * @throws GenericDBError when the database has an inconsistent state */ - async makeAliasPrimary( - noteId: Note[FieldNameNote.id], - alias: Alias[FieldNameAlias.alias], - ): Promise { + async makeAliasPrimary(noteId: number, alias: string): Promise { await this.knex.transaction(async (transaction) => { // First, set all existing aliases to not primary const numberOfUpdatedEntries = await transaction(TableAlias) @@ -106,7 +101,7 @@ export class AliasService { .where(FieldNameAlias.noteId, noteId); if (numberOfUpdatedEntries === 0) { throw new GenericDBError( - `The note does not exist or has no primary alias. This should never happen`, + 'The note does not exist or has no primary alias. This should never happen', this.logger.getContext(), 'makeAliasPrimary', ); @@ -130,12 +125,12 @@ export class AliasService { /** * Removes the specified alias from the note - * This method only does not require the noteId since it can be obtained from the alias prior to deletion + * This method only requires the alias since it can obtain the noteId from the alias prior to deletion * * @param alias The alias to remove from the note - * @throws {ForbiddenIdError} The requested alias is forbidden - * @throws {NotInDBError} The alias is not assigned to this note - * @throws {PrimaryAliasDeletionForbiddenError} The primary alias cannot be deleted + * @throws ForbiddenIdError The requested alias is forbidden + * @throws NotInDBError The alias is not assigned to this note + * @throws PrimaryAliasDeletionForbiddenError The primary alias cannot be deleted */ async removeAlias(alias: string): Promise { await this.knex.transaction(async (transaction) => { @@ -172,13 +167,14 @@ export class AliasService { * Gets the primary alias of the note specified by the noteId * * @param noteId The id of the note to get the primary alias of + * @param transaction The optional transaction to access the db * @returns The primary alias of the note - * @throws {NotInDBError} The note has no primary alias which should mean that the note does not exist + * @throws NotInDBError The note has no primary alias which should mean that the note does not exist */ async getPrimaryAliasByNoteId( noteId: number, transaction?: Knex, - ): Promise { + ): Promise { const dbActor = transaction ?? this.knex; const primaryAlias = await dbActor(TableAlias) .select(FieldNameAlias.alias) @@ -187,7 +183,7 @@ export class AliasService { .first(); if (primaryAlias === undefined) { throw new NotInDBError( - `The noteId '${noteId}' has no primary alias.`, + 'The note does not exist or has no primary alias. This should never happen', this.logger.getContext(), 'getPrimaryAliasByNoteId', ); @@ -198,9 +194,10 @@ export class AliasService { /** * Gets all aliases of the note specified by the noteId * - * @param noteId The id of the note to get the primary alias of - * @returns The primary alias of the note - * @throws {NotInDBError} The note has no primary alias which should mean that the note does not exist + * @param noteId The id of the note to get the list of aliases for + * @param transaction The optional transaction to access the db + * @returns The list of aliases for the note + * @throws NotInDBError The note with the specified id does not exist */ async getAllAliases( noteId: number, @@ -212,7 +209,7 @@ export class AliasService { .where(FieldNameAlias.noteId, noteId); if (aliases.length === 0) { throw new NotInDBError( - `The noteId '${noteId}' has no aliases. This should never happen.`, + 'The note does not exist or has no aliases. This should never happen', this.logger.getContext(), 'getAllAliases', ); @@ -226,11 +223,11 @@ export class AliasService { * * @param alias The alias to check * @param transaction The optional transaction to access the db - * @throws {ForbiddenIdError} The requested alias is not available - * @throws {AlreadyInDBError} The requested alias already exists + * @throws ForbiddenIdError The requested alias is not available + * @throws AlreadyInDBError The requested alias already exists */ async ensureAliasIsAvailable( - alias: Alias[FieldNameAlias.alias], + alias: string, transaction?: Knex, ): Promise { if (this.isAliasForbidden(alias)) { @@ -243,7 +240,7 @@ export class AliasService { const isUsed = await this.isAliasUsed(alias, transaction); if (isUsed) { throw new AlreadyInDBError( - `A note with the id or alias '${alias}' already exists.`, + `A note with the alias '${alias}' already exists.`, this.logger.getContext(), 'ensureAliasIsAvailable', ); @@ -254,17 +251,10 @@ export class AliasService { * Checks if the provided alias is forbidden by configuration * * @param alias The alias to check - * @return {boolean} true if the alias is forbidden, false otherwise + * @returns true if the alias is forbidden, false otherwise */ - isAliasForbidden(alias: Alias[FieldNameAlias.alias]): boolean { - const forbidden = this.noteConfig.forbiddenNoteIds.includes(alias); - if (forbidden) { - this.logger.warn( - `A note with the alias '${alias}' is forbidden by the administrator.`, - 'isAliasForbidden', - ); - } - return forbidden; + isAliasForbidden(alias: string): boolean { + return this.noteConfig.forbiddenNoteIds.includes(alias); } /** @@ -272,19 +262,16 @@ export class AliasService { * * @param alias The alias to check * @param transaction The optional transaction to access the db - * @return {boolean} true if the id or alias is already used, false otherwise + * @returns true if the alias is already used, false otherwise */ - async isAliasUsed( - alias: Alias[FieldNameAlias.alias], - transaction?: Knex, - ): Promise { + async isAliasUsed(alias: string, transaction?: Knex): Promise { const dbActor = transaction ? transaction : this.knex; const result = await dbActor(TableAlias) .select(FieldNameAlias.alias) .where(FieldNameAlias.alias, alias); if (result.length === 1) { this.logger.log( - `A note with the id or alias '${alias}' already exists.`, + `A note with the alias '${alias}' already exists.`, 'isAliasUsed', ); return true; @@ -293,11 +280,11 @@ export class AliasService { } /** - * Build the AliasDto from a note. + * Returns alias information in the AliasDto format + * * @param alias The alias to use - * @param isPrimaryAlias If the alias is the primary alias. - * @throws {NotInDBError} The specified alias does not exist - * @return {AliasDto} The built AliasDto + * @param isPrimaryAlias Whether the alias is the primary alias. + * @returns The built AliasDto */ toAliasDto(alias: string, isPrimaryAlias: boolean): AliasDto { return { diff --git a/backend/src/api-token/api-token.service.ts b/backend/src/api-token/api-token.service.ts index 87b2c99ca..de4a01bcd 100644 --- a/backend/src/api-token/api-token.service.ts +++ b/backend/src/api-token/api-token.service.ts @@ -4,12 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons'; -import { - ApiToken, - FieldNameApiToken, - TableApiToken, - TypeInsertApiToken, -} from '@hedgedoc/database'; +import { ApiToken, FieldNameApiToken, TableApiToken } from '@hedgedoc/database'; import { Injectable } from '@nestjs/common'; import { Cron, Timeout } from '@nestjs/schedule'; import { randomBytes } from 'crypto'; @@ -28,7 +23,8 @@ import { hashApiToken, } from '../utils/password'; -export const AUTH_TOKEN_PREFIX = 'hd2'; +const AUTH_TOKEN_PREFIX = 'hd2'; +const MESSAGE_TOKEN_INVALID = 'API token is invalid, expired or not found'; @Injectable() export class ApiTokenService { @@ -46,21 +42,22 @@ export class ApiTokenService { * The usage of this token is tracked in the database * * @param tokenString The token string to validate and parse - * @return The userId associated with the token + * @returns The userId associated with the token * @throws TokenNotValidError if the token is not valid */ async getUserIdForToken(tokenString: string): Promise { const [prefix, keyId, secret, ...rest] = tokenString.split('.'); - if (!keyId || !secret || prefix !== AUTH_TOKEN_PREFIX || rest.length > 0) { + // We always expect 86 characters for the secret and 11 characters for the keyId + // as they are generated with 64 bytes and 8 bytes respectively and then converted to a base64url string + if ( + keyId.length !== 11 || + !secret || + secret.length !== 86 || + prefix !== AUTH_TOKEN_PREFIX || + rest.length > 0 + ) { throw new TokenNotValidError('Invalid API token format'); } - if (secret.length != 86) { - // We always expect 86 characters, as the secret is generated with 64 bytes - // and then converted to a base64url string - throw new TokenNotValidError( - `API token '${tokenString}' has incorrect length`, - ); - } return await this.knex.transaction(async (transaction) => { const token = await transaction(TableApiToken) .select( @@ -71,7 +68,7 @@ export class ApiTokenService { .where(FieldNameApiToken.id, keyId) .first(); if (token === undefined) { - throw new TokenNotValidError('Token not found'); + throw new TokenNotValidError(MESSAGE_TOKEN_INVALID); } const tokenHash = token[FieldNameApiToken.secretHash]; @@ -88,16 +85,20 @@ export class ApiTokenService { /** * Creates a new API token for the given user + * We limit the number of tokens to 200 per user to avoid users losing track over their tokens. + * There is no technical limit to this. + * + * The returned secret is stored hashed in the database and therefore cannot be retrieved again. * * @param userId The id of the user to create the token for - * @param tokenLabel The label of the token + * @param label The label of the token * @param userDefinedValidUntil Maximum date until the token is valid, will be truncated to 2 years - * @throws TooManyTokensError if the user already has 200 tokens * @returns The created token together with the secret + * @throws TooManyTokensError if the user already has 200 tokens */ async createToken( userId: number, - tokenLabel: string, + label: string, userDefinedValidUntil?: Date, ): Promise { return await this.knex.transaction(async (transaction) => { @@ -105,16 +106,15 @@ export class ApiTokenService { .select(FieldNameApiToken.id) .where(FieldNameApiToken.userId, userId); if (existingTokensForUser.length >= 200) { - // This is a very high ceiling unlikely to hinder legitimate usage, - // but should prevent possible attack vectors throw new TooManyTokensError( - `User '${userId}' has already 200 API tokens and can't have more`, + 'There is a maximum of 200 API tokens per user', ); } const secret = bufferToBase64Url(randomBytes(64)); const keyId = bufferToBase64Url(randomBytes(8)); - const accessTokenHash = hashApiToken(secret); + const secretHash = hashApiToken(secret); + const fullToken = `${AUTH_TOKEN_PREFIX}.${keyId}.${secret}`; // Tokens can only be valid for a maximum of 2 years const maximumTokenValidity = new Date(); maximumTokenValidity.setTime( @@ -125,31 +125,28 @@ export class ApiTokenService { const validUntil = isTokenLimitedToMaximumValidity ? maximumTokenValidity : userDefinedValidUntil; - const token: TypeInsertApiToken = { + const createdAt = new Date(); + await this.knex(TableApiToken).insert({ [FieldNameApiToken.id]: keyId, - [FieldNameApiToken.label]: tokenLabel, + [FieldNameApiToken.label]: label, [FieldNameApiToken.userId]: userId, - [FieldNameApiToken.secretHash]: accessTokenHash, + [FieldNameApiToken.secretHash]: secretHash, [FieldNameApiToken.validUntil]: validUntil, - [FieldNameApiToken.createdAt]: new Date(), + [FieldNameApiToken.createdAt]: createdAt, + }); + return { + label, + keyId, + createdAt: createdAt.toISOString(), + validUntil: validUntil.toISOString(), + lastUsedAt: null, + secret: fullToken, }; - await this.knex(TableApiToken).insert(token); - return this.toAuthTokenWithSecretDto( - { - ...token, - [FieldNameApiToken.validUntil]: - token[FieldNameApiToken.validUntil].toISOString(), - [FieldNameApiToken.createdAt]: - token[FieldNameApiToken.createdAt].toISOString(), - [FieldNameApiToken.lastUsedAt]: null, - }, - secret, - ); }); } /** - * Ensures that the given token secret is valid for the given token + * Ensures that a token is valid by evaluating the expiry date as well as comparing secret and stored hash * This method does not return any value but throws an error if the token is not valid * * @param secret The secret to compare against the hash from the database @@ -164,14 +161,12 @@ export class ApiTokenService { ): void { // First, verify token expiry is not in the past (cheap operation) if (validUntil.getTime() < new Date().getTime()) { - throw new TokenNotValidError( - `Auth token is not valid since ${validUntil.toISOString()}`, - ); + throw new TokenNotValidError(MESSAGE_TOKEN_INVALID); } // Second, verify the secret (costly operation) if (!checkTokenEquality(secret, tokenHash)) { - throw new TokenNotValidError(`Secret does not match token hash`); + throw new TokenNotValidError(MESSAGE_TOKEN_INVALID); } } @@ -179,7 +174,7 @@ export class ApiTokenService { * Returns all tokens of a user * * @param userId The id of the user to get the tokens for - * @return The tokens of the user + * @returns A list of the user's tokens as ApiToken objects */ getTokensOfUserById(userId: number): Promise { return this.knex(TableApiToken) @@ -205,10 +200,10 @@ export class ApiTokenService { } /** - * Converts an ApiToken to an ApiTokenDto + * Formats an ApiToken object from the database to an ApiTokenDto * - * @param apiToken The token to convert - * @return The converted token + * @param apiToken The token object to convert + * @returns The built ApiTokenDto */ toAuthTokenDto(apiToken: ApiToken): ApiTokenDto { return { @@ -224,25 +219,6 @@ export class ApiTokenService { }; } - /** - * Converts an ApiToken to an ApiTokenWithSecretDto - * - * @param apiToken The token to convert - * @param secret The secret of the token - * @return The converted token - */ - toAuthTokenWithSecretDto( - apiToken: ApiToken, - secret: string, - ): ApiTokenWithSecretDto { - const tokenDto = this.toAuthTokenDto(apiToken); - const fullToken = `${AUTH_TOKEN_PREFIX}.${tokenDto.keyId}.${secret}`; - return { - ...tokenDto, - secret: fullToken, - }; - } - // Deletes all invalid tokens every sunday on 3:00 AM @Cron('0 0 3 * * 0') async handleCron(): Promise { @@ -264,7 +240,7 @@ export class ApiTokenService { .where(FieldNameApiToken.validUntil, '<', new Date()) .delete(); this.logger.log( - `${numberOfDeletedTokens} invalid AuthTokens were purged from the DB.`, + `${numberOfDeletedTokens} expired API tokens were purged from the DB`, 'removeInvalidTokens', ); } diff --git a/backend/src/api/private/auth/local/local.controller.ts b/backend/src/api/private/auth/local/local.controller.ts index 5a3ae5fd5..97d901b7c 100644 --- a/backend/src/api/private/auth/local/local.controller.ts +++ b/backend/src/api/private/auth/local/local.controller.ts @@ -51,7 +51,7 @@ export class LocalController { @Body() registerDto: RegisterDto, ): Promise { await this.localIdentityService.checkPasswordStrength(registerDto.password); - const userId = await this.localIdentityService.createLocalIdentity( + const userId = await this.localIdentityService.createUserWithLocalIdentity( registerDto.username, registerDto.password, registerDto.displayName, diff --git a/backend/src/auth/identity.service.ts b/backend/src/auth/identity.service.ts index 2d058dc55..118e6f2d5 100644 --- a/backend/src/auth/identity.service.ts +++ b/backend/src/auth/identity.service.ts @@ -8,14 +8,7 @@ import { PendingUserConfirmationDto, PendingUserInfoDto, } from '@hedgedoc/commons'; -import { - FieldNameIdentity, - FieldNameUser, - Identity, - TableIdentity, - TypeInsertIdentity, - User, -} from '@hedgedoc/database'; +import { FieldNameIdentity, Identity, TableIdentity } from '@hedgedoc/database'; import { Inject, Injectable } from '@nestjs/common'; import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; @@ -44,7 +37,7 @@ export class IdentityService { * Determines if the identity should be updated * * @param authProviderIdentifier The identifier of the auth source - * @return true if the authProviderIdentifier is the sync source, false otherwise + * @returns true if the authProviderIdentifier is the sync source, false otherwise */ mayUpdateIdentity(authProviderIdentifier: string): boolean { return this.authConfig.common.syncSource === authProviderIdentifier; @@ -53,10 +46,11 @@ export class IdentityService { /** * Retrieve an identity from the information received from an auth provider. * - * @param authProviderUserId - the userId of the wanted identity - * @param authProviderType - the providerType of the wanted identity - * @param authProviderIdentifier - optional name of the provider if multiple exist - * @return + * @param authProviderUserId the userId of the wanted identity + * @param authProviderType the providerType of the wanted identity + * @param authProviderIdentifier optional name of the provider if multiple exist + * @returns The found identity + * @throws NotInDBError if the identity is not found */ async getIdentityFromUserIdAndProviderType( authProviderUserId: string, @@ -78,15 +72,14 @@ export class IdentityService { } /** - * Creates a new generic identity. + * Creates a new generic identity * - * @param userId - the user the identity should be added to - * @param authProviderType - the providerType of the identity - * @param authProviderIdentifier - the providerIdentifier of the identity - * @param authProviderUserId - the userId the identity should have - * @param passwordHash - the password hash if the identiy uses that. - * @param transaction - the database transaction to use if any - * @return the new local identity + * @param userId the user the identity should be added to + * @param authProviderType the providerType of the identity + * @param authProviderIdentifier the providerIdentifier of the identity + * @param authProviderUserId the userId the identity should have + * @param passwordHash the password hash if the identity uses that + * @param transaction the database transaction to use if any */ async createIdentity( userId: number, @@ -97,28 +90,27 @@ export class IdentityService { transaction?: Knex, ): Promise { const dbActor = transaction ?? this.knex; - const identity: TypeInsertIdentity = { + await dbActor(TableIdentity).insert({ [FieldNameIdentity.userId]: userId, [FieldNameIdentity.providerType]: authProviderType, [FieldNameIdentity.providerIdentifier]: authProviderIdentifier, [FieldNameIdentity.providerUserId]: authProviderUserId, [FieldNameIdentity.passwordHash]: passwordHash ?? null, - }; - await dbActor(TableIdentity).insert(identity); + }); } /** - * Creates a new user with the given user data. + * Creates a new user with the given user data * * @param authProviderType The type of the auth provider * @param authProviderIdentifier The identifier of the auth provider * @param authProviderUserId The id of the user in the auth system * @param username The new username - * @param displayName The dispay name of the new user + * @param displayName The display name of the new user * @param email The email address of the new user * @param photoUrl The URL to the new user's profile picture * @param passwordHash The optional password hash, only required for local identities - * @return The id of the newly created user + * @returns The id of the newly created user */ async createUserWithIdentity( authProviderType: AuthProviderType, @@ -129,7 +121,7 @@ export class IdentityService { email: string | null, photoUrl: string | null, passwordHash?: string, - ): Promise { + ): Promise { return await this.knex.transaction(async (transaction) => { const userId = await this.usersService.createUser( username, @@ -151,14 +143,14 @@ export class IdentityService { } /** - * Create a user with identity from pending user confirmation data. + * Create a user with identity from pending user confirmation data * * @param sessionUserData The data we got from the authProvider itself * @param pendingUserConfirmationData The data the user entered while confirming their account * @param authProviderType The type of the auth provider * @param authProviderIdentifier The identifier of the auth provider * @param authProviderUserId The id of the user in the auth system - * @return The id of the newly created user + * @returns The id of the newly created user */ async createUserWithIdentityFromPendingUserConfirmation( sessionUserData: PendingUserInfoDto, @@ -166,7 +158,7 @@ export class IdentityService { authProviderType: AuthProviderType, authProviderIdentifier: string, authProviderUserId: string, - ): Promise { + ): Promise { const profileEditsAllowed = this.authConfig.common.allowProfileEdits; const chooseUsernameAllowed = this.authConfig.common.allowChooseUsername; diff --git a/backend/src/auth/ldap/ldap.service.ts b/backend/src/auth/ldap/ldap.service.ts index f4df0d84e..298cb07b9 100644 --- a/backend/src/auth/ldap/ldap.service.ts +++ b/backend/src/auth/ldap/ldap.service.ts @@ -32,7 +32,7 @@ const LDAP_ERROR_MAP: Record = { '775': 'User account locked', default: 'Invalid username/password', /* eslint-enable @typescript-eslint/naming-convention */ -}; +} as const; @Injectable() export class LdapService { @@ -45,15 +45,14 @@ export class LdapService { } /** - * Try to log in the user with the given credentials. + * Tries to log in the user with the given credentials and returns the user info on success * - * @param ldapConfig {LdapConfig} - the ldap config to use - * @param username {string} - the username to log in with - * @param password {string} - the password to log in with + * @param ldapConfig The ldap config to use + * @param username The user-provided username + * @param password The user-provided password * @returns The user info of the user that logged in - * @throws {UnauthorizedException} - the user has given us incorrect credentials - * @throws {InternalServerErrorException} - if there are errors that we can't assign to wrong credentials - * @private + * @throws UnauthorizedException if the user has given us incorrect credentials + * @throws InternalServerErrorException if there are errors that we can't assign to wrong credentials */ getUserInfoFromLdap( ldapConfig: LdapConfig, @@ -119,11 +118,11 @@ export class LdapService { } /** - * Get and return the correct ldap config from the list of available configs. - * @param {string} ldapIdentifier the identifier for the ldap config to be used - * @returns {LdapConfig} - the ldap config with the given identifier - * @throws {NotFoundException} - there is no ldap config with the given identifier - * @private + * Fetches the correct LDAP config from the list of available configs + * + * @param ldapIdentifier The identifier for the LDAP config to be used + * @returns The LDAP config with the given identifier + * @throws NotFoundException if there is no LDAP config with the given identifier */ getLdapConfig(ldapIdentifier: string): LdapConfig { const ldapConfig = this.authConfig.ldap.find( @@ -131,19 +130,22 @@ export class LdapService { ); if (!ldapConfig) { this.logger.warn( - `The LDAP Config '${ldapIdentifier}' was requested, but doesn't exist`, + `The LDAP config '${ldapIdentifier}' was requested, but doesn't exist`, + ); + throw new NotFoundException( + `There is no LDAP config '${ldapIdentifier}'`, ); - throw new NotFoundException(`There is no ldapConfig '${ldapIdentifier}'`); } return ldapConfig; } /** - * This method transforms the ldap error codes we receive into correct errors. + * This method transforms the LDAP error codes we receive into correct errors. * It's very much inspired by https://github.com/vesse/passport-ldapauth/blob/b58c60000a7cc62165b112274b80c654adf59fff/lib/passport-ldapauth/strategy.js#L261 - * @returns {HttpException} - the matching HTTP exception to throw to the client - * @throws {UnauthorizedException} if error indicates that the user is not allowed to log in - * @throws {InternalServerErrorException} in every other case + * + * @returns The matching HTTP exception to throw to the client + * @throws UnauthorizedException if the error indicates that the user is not allowed to log in + * @throws InternalServerErrorException in every other case */ private getLdapException( username: string, diff --git a/backend/src/auth/local/local.service.ts b/backend/src/auth/local/local.service.ts index 79dfe3063..6d4a7ea27 100644 --- a/backend/src/auth/local/local.service.ts +++ b/backend/src/auth/local/local.service.ts @@ -4,13 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '@hedgedoc/commons'; -import { - FieldNameIdentity, - FieldNameUser, - Identity, - TableIdentity, - User, -} from '@hedgedoc/database'; +import { FieldNameIdentity, Identity, TableIdentity } from '@hedgedoc/database'; import { Inject, Injectable } from '@nestjs/common'; import { OptionsGraph, @@ -63,18 +57,18 @@ export class LocalService { } /** - * Create a new identity for internal auth + * Creates a new user with an identity for internal auth and returns the id of the newly created user * * @param username The username of the new identity * @param password The password the identity should have * @param displayName The display name of the new identity - * @returns {Identity} the new local identity + * @returns The id of the newly created user */ - async createLocalIdentity( + async createUserWithLocalIdentity( username: string, password: string, displayName: string, - ): Promise { + ): Promise { const passwordHash = await hashPassword(password); return await this.identityService.createUserWithIdentity( AuthProviderType.LOCAL, @@ -89,12 +83,12 @@ export class LocalService { } /** - * @async - * Update the internal password of the specified the user - * @param {User} userId - 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 + * Updates the password hash for the local identity of the specified the user + * + * @param userId The user, whose local identity should be updated + * @param newPassword The new password + * @throws NoLocalIdentityError if the specified user has no local identity + * @throws PasswordTooWeakError if the password is too weak */ async updateLocalPassword( userId: number, @@ -112,12 +106,12 @@ export class LocalService { } /** - * @async - * Checks if the user and password combination matches - * @param {string} username - 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 + * Checks if the user and password combination matches for the local identity and returns the local identity on success + * + * @param username The user to use + * @param password The password to use + * @returns The identity of the user if the credentials are valid + * @throws InvalidCredentialsError if the credentials are invalid */ async checkLocalPassword( username: string, @@ -129,12 +123,11 @@ export class LocalService { AuthProviderType.LOCAL, null, ); - if ( - !(await checkPassword( - password, - identity[FieldNameIdentity.passwordHash] ?? '', - )) - ) { + const passwordValid = await checkPassword( + password, + identity[FieldNameIdentity.passwordHash] ?? '', + ); + if (!passwordValid) { throw new InvalidCredentialsError( 'Username or password is not correct', this.logger.getContext(), @@ -145,11 +138,12 @@ export class LocalService { } /** - * @async - * Check if the password is strong and long enough. + * Checks if the password is strong and long 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 + * The method acts as a guard and therefore throws an error on failure instead of returning a boolean. + * + * @param password The password to check + * @throws PasswordTooWeakError if the password is too weak */ async checkPasswordStrength(password: string): Promise { if (password.length < 6) { diff --git a/backend/src/auth/oidc/oidc.service.ts b/backend/src/auth/oidc/oidc.service.ts index fce041c3c..30419ecad 100644 --- a/backend/src/auth/oidc/oidc.service.ts +++ b/backend/src/auth/oidc/oidc.service.ts @@ -265,6 +265,7 @@ export class OidcService { oidcIdentifier, ); } catch (e) { + // Catch not-found errors when registration via OIDC is enabled and return null instead if (e instanceof NotInDBError) { if (!clientConfig.config.enableRegistration) { throw new ForbiddenException( diff --git a/backend/src/auth/session.guard.ts b/backend/src/auth/session.guard.ts index 9bdde4084..1a79f8b46 100644 --- a/backend/src/auth/session.guard.ts +++ b/backend/src/auth/session.guard.ts @@ -16,10 +16,10 @@ import { ConsoleLoggerService } from '../logger/console-logger.service'; /** * This guard checks if a session is present. * - * If there is a username in `request.session.username` it will try to get this user from the database and put it into `request.user`. See {@link RequestUser}. - * If there is no `request.session.username`, but any PermissionLevel is configured, `request.session.authProvider` is set to `guest` to indicate a guest user. + * It checks if the session contains a `userId` and an `authProviderType`. If both are present, they are added to the request object. + * Otherwise, an `UnauthorizedException` is thrown. * - * @throws UnauthorizedException + * @throws UnauthorizedException if the session is not present or does not contain a `userId` or `authProviderType`. */ @Injectable() export class SessionGuard implements CanActivate { @@ -27,13 +27,20 @@ export class SessionGuard implements CanActivate { this.logger.setContext(SessionGuard.name); } + /** + * Checks if the request has a valid session. + * + * @param context The execution context containing the request. + * @returns true if the session is valid + * @throws UnauthorizedException when the session is invalid, and therefore stops further execution + */ canActivate(context: ExecutionContext): boolean { const request: CompleteRequest = context.switchToHttp().getRequest(); const userId = request.session?.userId; const authProviderType = request.session?.authProviderType; if (!userId || !authProviderType) { this.logger.debug('The user has no session.'); - throw new UnauthorizedException("You're not logged in"); + throw new UnauthorizedException('You have no active session'); } request.userId = userId; request.authProviderType = authProviderType; diff --git a/backend/src/config/app.config.ts b/backend/src/config/app.config.ts index 8dddb2fdf..f775a8188 100644 --- a/backend/src/config/app.config.ts +++ b/backend/src/config/app.config.ts @@ -18,6 +18,12 @@ import { extractDescriptionFromZodIssue, } from './zod-error-message'; +/** + * Validates that a given URL is valid, uses the HTTP or HTTPS protocol, and does not end with a slash + * + * @param value The URL string to validate. + * @param ctx The Zod refinement context. + */ function validateUrl(value: string | undefined, ctx: RefinementCtx): void { if (!value) { return z.NEVER; diff --git a/backend/src/config/gitlab.enum.ts b/backend/src/config/gitlab.enum.ts deleted file mode 100644 index 2bb470c68..000000000 --- a/backend/src/config/gitlab.enum.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export enum GitlabScope { - READ_USER = 'read_user', - API = 'api', -} diff --git a/backend/src/config/note.config.ts b/backend/src/config/note.config.ts index ff9e79fe3..ad0f53dff 100644 --- a/backend/src/config/note.config.ts +++ b/backend/src/config/note.config.ts @@ -60,6 +60,16 @@ const schema = z.object({ export type NoteConfig = z.infer; +/** + * Checks if the configuration for guest access is consistent with the environment variable + * HD_PERMISSIONS_DEFAULT_EVERYONE. + * + * If HD_PERMISSIONS_DEFAULT_EVERYONE is set, it should not conflict with the guestAccess setting. + * If guestAccess is DENY, then HD_PERMISSIONS_DEFAULT_EVERYONE should not be set. + * + * @param config The NoteConfig to check. + * @throws Error if the configuration is inconsistent. + */ function checkEveryoneConfigIsConsistent(config: NoteConfig): void { const everyoneDefaultSet = process.env.HD_PERMISSIONS_DEFAULT_EVERYONE !== undefined; @@ -70,6 +80,15 @@ function checkEveryoneConfigIsConsistent(config: NoteConfig): void { } } +/** + * Checks if the default permissions for logged-in users are higher than those for guests. + * + * If the default permissions for 'everyone' are set to a level that is higher than + * the default permissions for 'loggedIn', it throws an error. + * + * @param config The NoteConfig to check. + * @throws Error if the default permissions for 'everyone' are higher than those for 'loggedIn'. + */ function checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests( config: NoteConfig, ): void { @@ -80,7 +99,7 @@ function checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests( getDefaultAccessLevelOrdinal(loggedIn) ) { throw new Error( - `'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${everyone}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${loggedIn}'. This gives everyone greater permissions than logged-in users which is not allowed.`, + `'HD_PERMISSIONS_DEFAULT_EVERYONE' is set to '${everyone}', but 'HD_PERMISSIONS_DEFAULT_LOGGED_IN' is set to '${loggedIn}'. This would give everyone greater permissions than logged-in users, and is not allowed since it doesn't make sense.`, ); } } diff --git a/backend/src/config/utils.ts b/backend/src/config/utils.ts index f0c44d72d..ffeca8e0c 100644 --- a/backend/src/config/utils.ts +++ b/backend/src/config/utils.ts @@ -5,14 +5,27 @@ */ import { Loglevel } from './loglevel.enum'; +/** + * Finds duplicates in an array + * This function uses the conversion of the array to a Set to find duplicates even if an item is present three or more times + * + * @param array The array to search for duplicates + * @returns An array containing the duplicate items + */ export function findDuplicatesInArray(array: T[]): T[] { - // This uses the Array-Set conversion to remove duplicates in the finding. - // This can happen if an entry is present three or more times return Array.from( new Set(array.filter((item, index) => array.indexOf(item) !== index)), ); } +/** + * Ensures that no duplicates exist in the provided array of names + * If duplicates are found, an error is thrown with a message containing the duplicate names + * + * @param authName The name of the authentication method + * @param names The array of names to check for duplicates + * @throws Error if duplicates are found in the names array + */ export function ensureNoDuplicatesExist( authName: string, names: string[], @@ -26,6 +39,14 @@ export function ensureNoDuplicatesExist( ); } } + +/** + * Converts a (comma-)separated configuration value to an array of strings or undefined if it is undefined + * + * @param configValue The configuration value to convert + * @param separator The separator to use for splitting the value (default is ',') + * @returns An array of strings or undefined if configValue is undefined + */ export function toArrayConfig( configValue?: string, separator = ',', @@ -39,6 +60,13 @@ export function toArrayConfig( return configValue.split(separator).map((arrayItem) => arrayItem.trim()); } +/** + * Checks if the current log level is sufficient to log a message at the requested log level + * + * @param currentLoglevel The current log level + * @param requestedLoglevel The requested log level + * @returns true if the current log level is sufficient to log the requested log level, false otherwise + */ export function needToLog( currentLoglevel: Loglevel, requestedLoglevel: Loglevel, @@ -48,6 +76,12 @@ export function needToLog( return current >= requested; } +/** + * Transforms a Loglevel value to an integer representation where a higher number means more log output + * + * @param loglevel The Loglevel to transform + * @returns The integer representation of the log level + */ function transformLoglevelToInt(loglevel: Loglevel): number { switch (loglevel) { case Loglevel.TRACE: @@ -63,6 +97,12 @@ function transformLoglevelToInt(loglevel: Loglevel): number { } } +/** + * Parses a string to a number. If the value is undefined, it returns undefined. + * + * @param value The value to parse + * @returns The parsed number or undefined if the value is undefined + */ export function parseOptionalNumber(value?: string): number | undefined { if (value === undefined) { return undefined; @@ -81,5 +121,10 @@ export function parseOptionalBoolean(value?: string): boolean | undefined { if (value === undefined) { return undefined; } - return value === 'true' || value === '1' || value === 'y'; + return ( + value === '1' || + value.toLowerCase() === 'y' || + value.toLowerCase() === 'yes' || + value.toLowerCase() === 'true' + ); } diff --git a/backend/src/config/zod-error-message.ts b/backend/src/config/zod-error-message.ts index 78c69e0fa..f6e49a632 100644 --- a/backend/src/config/zod-error-message.ts +++ b/backend/src/config/zod-error-message.ts @@ -5,10 +5,25 @@ */ import { ZodIssue } from 'zod'; +/** + * Converts a camelCase string to snake_case. + * + * @param str The camelCase string to convert. + * @returns The converted snake_case string. + */ function camelToSnakeCase(str: string): string { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } +/** + * Extracts a descriptive error message from a Zod issue by traversing its path to rebuild the identifier + * This is required because Zod does not provide a way to extract the identifier from the issue directly + * + * @param issue The Zod issue to extract the description from. + * @param prefix A prefix to prepend to the identifier. + * @param allArrays An optional record mapping array names to their string representations + * @returns A formatted error message string + */ export function extractDescriptionFromZodIssue( issue: ZodIssue, prefix: string, @@ -31,6 +46,12 @@ export function extractDescriptionFromZodIssue( return `${identifier}: ${issue.message}`; } +/** + * Builds a formatted error message from an array of zod error messages + * + * @param errorMessages An array of error messages to include in the formatted message + * @returns A string containing the formatted error message + */ export function buildErrorMessage(errorMessages: string[]): string { let totalErrorMessage = 'There were some errors with your configuration:'; for (const message of errorMessages) { diff --git a/backend/src/database/migrations/README.md b/backend/src/database/migrations/README.md new file mode 100644 index 000000000..2791dff07 --- /dev/null +++ b/backend/src/database/migrations/README.md @@ -0,0 +1,6 @@ +# Migrations + +The migrations are loaded and executed by Knex itself. It seems Knex expects CommonJS modules and does not support +TypeScript. Additionally, there were problems when importing types from TypeScript from the backend, or from the +commons package due to other (ESM-only) dependencies. Therefore, the migrations and database types use their own +CommonJS package. diff --git a/backend/src/database/seeds/01_user.ts b/backend/src/database/seeds/01_user.ts index 8e8b0b6ac..50aa3bce7 100644 --- a/backend/src/database/seeds/01_user.ts +++ b/backend/src/database/seeds/01_user.ts @@ -24,7 +24,7 @@ export async function seed(knex: Knex): Promise { { [FieldNameUser.username]: null, [FieldNameUser.guestUuid]: '55b4618a-d5f3-4320-93d3-f3501c73d72b', - [FieldNameUser.displayName]: 'Gast 1', + [FieldNameUser.displayName]: 'Guest 1', [FieldNameUser.photoUrl]: null, [FieldNameUser.email]: null, [FieldNameUser.authorStyle]: 1, diff --git a/backend/src/errors/error-mapping.ts b/backend/src/errors/error-mapping.ts index 89abf84d1..43ec0df80 100644 --- a/backend/src/errors/error-mapping.ts +++ b/backend/src/errors/error-mapping.ts @@ -86,6 +86,9 @@ const mapOfHedgeDocErrorsToHttpErrors: Map = ]); @Catch() +/** + * Filters all errors that are not instances of HttpException and maps them to the appropriate HTTP error + */ export class ErrorExceptionMapping extends BaseExceptionFilter { private readonly loggerService: ConsoleLoggerService; constructor(logger: ConsoleLoggerService, applicationRef?: HttpServer) { @@ -97,6 +100,14 @@ export class ErrorExceptionMapping extends BaseExceptionFilter { super.catch(this.transformError(error), host); } + /** + * Transforms an error into an HttpException if it is a HedgeDoc error. + * Logs the error message to the console if it is an ErrorWithContextDetails. + * If the error is not a HedgeDoc error, it returns the original error. + * + * @param error The error to transform + * @returns An HttpException if the error is a HedgeDoc error, otherwise the original error + */ private transformError(error: Error): Error { const httpExceptionConstructor = mapOfHedgeDocErrorsToHttpErrors.get( error.name, diff --git a/backend/src/frontend-config/frontend-config.service.ts b/backend/src/frontend-config/frontend-config.service.ts index 44dbfc296..9361c4241 100644 --- a/backend/src/frontend-config/frontend-config.service.ts +++ b/backend/src/frontend-config/frontend-config.service.ts @@ -40,6 +40,11 @@ export class FrontendConfigService { this.logger.setContext(FrontendConfigService.name); } + /** + * Returns the config options for the frontend + * + * @returns A frontend config DTO + */ async getFrontendConfig(): Promise { return { guestAccess: this.noteConfig.guestAccess, @@ -58,6 +63,11 @@ export class FrontendConfigService { }; } + /** + * Reads the auth providers from the config and returns them + * + * @returns An array of auth provider DTOs + */ private getAuthProviders(): AuthProviderDto[] { const providers: AuthProviderDto[] = []; if (this.authConfig.local.enableLogin) { @@ -84,6 +94,11 @@ export class FrontendConfigService { return providers; } + /** + * Reads the branding from the config and returns it + * + * @returns A branding DTO + */ private getBranding(): BrandingDto { return { logo: this.customizationConfig.branding.customLogo @@ -93,6 +108,11 @@ export class FrontendConfigService { }; } + /** + * Reads the special URLs like imprint or privacy policy from the config and returns them + * + * @returns A special URL DTO + */ private getSpecialUrls(): SpecialUrlDto { return { imprint: this.customizationConfig.specialUrls.imprint diff --git a/backend/src/groups/groups.service.ts b/backend/src/groups/groups.service.ts index 826b894dd..1b245d99b 100644 --- a/backend/src/groups/groups.service.ts +++ b/backend/src/groups/groups.service.ts @@ -4,11 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { GroupInfoDto } from '@hedgedoc/commons'; -import { - FieldNameGroup, - TableGroup, - TypeInsertGroup, -} from '@hedgedoc/database'; +import { FieldNameGroup, TableGroup } from '@hedgedoc/database'; import { Injectable } from '@nestjs/common'; import { Knex } from 'knex'; import { InjectConnection } from 'nest-knexjs'; @@ -32,16 +28,15 @@ export class GroupsService { * * @param name The group name as identifier the new group shall have * @param displayName The display name the new group shall have - * @throws {AlreadyInDBError} The group name is already taken + * @throws AlreadyInDBError if the group name is already taken */ async createGroup(name: string, displayName: string): Promise { - const group: TypeInsertGroup = { - [FieldNameGroup.name]: name, - [FieldNameGroup.displayName]: displayName, - [FieldNameGroup.isSpecial]: false, - }; try { - await this.knex(TableGroup).insert(group); + await this.knex(TableGroup).insert({ + [FieldNameGroup.name]: name, + [FieldNameGroup.displayName]: displayName, + [FieldNameGroup.isSpecial]: false, + }); } catch { const message = `A group with the name '${name}' already exists.`; this.logger.debug(message, 'createGroup'); @@ -53,8 +48,8 @@ export class GroupsService { * Fetches a group by its identifier name * * @param name Name of the group to query - * @return The group - * @throws {NotInDBError} if there is no group with this name + * @returns The group's metadata + * @throws NotInDBError if there is no group with this name */ async getGroupInfoDtoByName(name: string): Promise { const group = await this.knex(TableGroup) @@ -76,8 +71,8 @@ export class GroupsService { * * @param name Name of the group to query * @param transaction The optional database transaction to use - * @return The groupId - * @throws {NotInDBError} if there is no group with this name + * @returns The groupId + * @throws NotInDBError if there is no group with this name */ async getGroupIdByName(name: string, transaction?: Knex): Promise { const dbActor = transaction ?? this.knex; diff --git a/backend/src/media/media-backend.interface.ts b/backend/src/media/media-backend.interface.ts index def764ca4..5d0ee77d0 100644 --- a/backend/src/media/media-backend.interface.ts +++ b/backend/src/media/media-backend.interface.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,12 +7,13 @@ import { FileTypeResult } from 'file-type'; export interface MediaBackend { /** - * Saves a file according to backend internals. + * Saves a file according to backend internals + * * @param uuid Unique identifier of the uploaded file * @param buffer File data * @param fileType File type result - * @throws {MediaBackendError} - there was an error saving the file - * @return The internal backend data, which should be saved + * @returns The internal backend data, which should be saved + * @throws MediaBackendError - there was an error saving the file */ saveFile( uuid: string, @@ -21,19 +22,21 @@ export interface MediaBackend { ): Promise; /** - * Delete a file from the backend + * Deletes a file from the backend + * * @param uuid Unique identifier of the uploaded file * @param backendData Internal backend data - * @throws {MediaBackendError} - there was an error deleting the file + * @throws MediaBackendError if there was an error deleting the file */ deleteFile(uuid: string, backendData: string | null): Promise; /** - * Get a publicly accessible URL of a file from the backend + * Gets a publicly accessible URL of a file from the backend + * * @param uuid Unique identifier of the uploaded file * @param backendData Internal backend data - * @throws {MediaBackendError} - there was an error getting the file - * @return Public accessible URL of the file + * @returns Public accessible URL of the file + * @throws MediaBackendError if there was an error getting the file */ getFileUrl(uuid: string, backendData: string | null): Promise; } diff --git a/backend/src/media/media.service.ts b/backend/src/media/media.service.ts index b442cd2ef..c1e574e0a 100644 --- a/backend/src/media/media.service.ts +++ b/backend/src/media/media.service.ts @@ -55,6 +55,12 @@ export class MediaService { this.mediaBackend = this.getBackendFromType(this.mediaBackendType); } + /** + * Checks if the given MIME type is allowed for media uploads + * + * @param mimeType The MIME type to check + * @returns true if the MIME type is allowed, false otherwise + */ private static isAllowedMimeType(mimeType: string): boolean { const allowedTypes = [ 'image/apng', @@ -81,10 +87,10 @@ export class MediaService { * @param fileBuffer The buffer with the file contents to save * @param userId Id of the user who uploaded this file * @param noteId Id of the note which will be associated with the new file - * @return The created MediaUpload entity - * @throws {ClientError} if the MIME type of the file is not supported - * @throws {NotInDBError} if the note or user is not in the database - * @throws {MediaBackendError} if there was an error saving the file + * @returns The created MediaUpload entity + * @throws ClientError if the MIME type of the file is not supported + * @throws NotInDBError if the note or user is not in the database + * @throws MediaBackendError if there was an error saving the file */ async saveFile( fileName: string, @@ -123,10 +129,11 @@ export class MediaService { } /** - * @async - * Try to delete the specified file. - * @param {uuid} uuid - the name of the file to delete. - * @throws {MediaBackendError} - there was an error deleting the file + * Tries to delete the specified file + * + * @param uuid the uuid of the file to delete + * @throws NotInDBError if the file with the given uuid is not found in the database + * @throws MediaBackendError if there was an error deleting the file at the backend */ async deleteFile(uuid: string): Promise { const backendData = await this.knex(TableMediaUpload) @@ -150,11 +157,11 @@ export class MediaService { } /** - * @async - * Get the URL of the file. - * @param {string} uuid - the uuid of the file to get the URL for. - * @return {string} the URL of the file. - * @throws {MediaBackendError} - there was an error retrieving the url + * Retrieves the URL to a media upload file + * + * @param uuid the uuid of the file to get the URL for + * @returns the URL of the file + * @throws MediaBackendError if there was an error retrieving the url */ async getFileUrl(uuid: string): Promise { const mediaUpload = await this.knex(TableMediaUpload) @@ -178,11 +185,11 @@ export class MediaService { } /** - * @async - * Find a file entry by its UUID. - * @param {string} uuid - The UUID of the MediaUpload entity to find. - * @returns {MediaUpload} - the MediaUpload entity if found. - * @throws {NotInDBError} - the MediaUpload entity with the provided UUID is not found in the database. + * Finds a file entry by its UUID + * + * @param uuid The UUID of the MediaUpload entity to find + * @returns The MediaUpload entity if found + * @throws NotInDBError if the MediaUpload entity with the provided UUID is not found in the database */ async findUploadByUuid(uuid: string): Promise { const mediaUpload = await this.knex(TableMediaUpload) @@ -196,10 +203,10 @@ export class MediaService { } /** - * @async - * List all uploads by a specific user - * @param {number} userId - the specific user - * @return {MediaUpload[]} arary of media uploads owned by the user + * Lists all uploads by a specific user + * + * @param userId the id of the user + * @returns An array of media uploads owned by the user */ async getMediaUploadUuidsByUserId( userId: number, @@ -211,10 +218,10 @@ export class MediaService { } /** - * @async - * List all uploads to a specific note - * @param {number} noteId - the specific user - * @return {MediaUpload[]} array of media uploads owned by the user + * Lists all uploads to a specific note + * + * @param noteId the specific user + * @returns An array of media uploads owned by the user */ async getMediaUploadUuidsByNoteId( noteId: number, @@ -228,9 +235,9 @@ export class MediaService { } /** - * @async - * Set the note of a mediaUpload to null - * @param {string} uuid - the media upload to be changed + * Sets the note of a mediaUpload to null + * + * @param uuid the media upload to be changed */ async removeNoteFromMediaUpload(uuid: string): Promise { this.logger.debug( @@ -244,6 +251,9 @@ export class MediaService { .where(FieldNameMediaUpload.uuid, uuid); } + /** + * Returns the backend type that is configured in the media configuration + */ private chooseBackendType(): MediaBackendType { switch (this.mediaConfig.backend.use as string) { case 'filesystem': @@ -263,6 +273,12 @@ export class MediaService { } } + /** + * Returns the MediaBackend instance for the given MediaBackendType + * + * @param type The MediaBackendType to get the backend for + * @returns The MediaBackend instance + */ private getBackendFromType(type: MediaBackendType): MediaBackend { switch (type) { case MediaBackendType.FILESYSTEM: @@ -278,6 +294,12 @@ export class MediaService { } } + /** + * Retrieves media upload DTOs by a list of their UUIDs + * + * @param uuids The UUIDs of the media uploads to retrieve + * @returns An array of MediaUploadDto objects containing the details of the media uploads + */ async getMediaUploadDtosByUuids(uuids: string[]): Promise { const mediaUploads = await this.knex(TableMediaUpload) .select< diff --git a/backend/src/notes/note.module.ts b/backend/src/notes/note.module.ts index c1301148d..dae11e9b4 100644 --- a/backend/src/notes/note.module.ts +++ b/backend/src/notes/note.module.ts @@ -25,7 +25,6 @@ import { NoteService } from './note.service'; LoggerModule, forwardRef(() => PermissionsModule), ConfigModule, - RealtimeNoteModule, KnexModule, ], controllers: [], diff --git a/backend/src/notes/note.service.ts b/backend/src/notes/note.service.ts index a3f4c2f9c..31cd5a452 100644 --- a/backend/src/notes/note.service.ts +++ b/backend/src/notes/note.service.ts @@ -48,7 +48,6 @@ import { GroupsService } from '../groups/groups.service'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { PermissionService } from '../permissions/permission.service'; import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store'; -import { RealtimeNoteService } from '../realtime/realtime-note/realtime-note.service'; import { RevisionsService } from '../revisions/revisions.service'; import { UsersService } from '../users/users.service'; @@ -68,7 +67,6 @@ export class NoteService { private aliasService: AliasService, @Inject(forwardRef(() => PermissionService)) private permissionService: PermissionService, - private realtimeNoteService: RealtimeNoteService, private realtimeNoteStore: RealtimeNoteStore, private eventEmitter: EventEmitter2, ) { @@ -79,7 +77,7 @@ export class NoteService { * Get all notes owned by a user * * @param userId The id of the user who owns the notes - * @return Array of notes owned by the user + * @returns Array of notes owned by the user */ async getUserNoteIds(userId: number): Promise { const result = await this.knex(TableNote) @@ -94,18 +92,18 @@ export class NoteService { * @param noteContent The content of the new note, in most cases an empty string * @param givenAlias An optional alias the note should have * @param ownerUserId The owner of the note - * @return The newly created note - * @throws {AlreadyInDBError} a note with the requested id or aliases already exists - * @throws {ForbiddenIdError} the requested id or aliases is forbidden - * @throws {MaximumDocumentLengthExceededError} the noteContent is longer than the maxDocumentLength - * @thorws {GenericDBError} the database returned a non-expected value + * @returns The newly created note + * @throws AlreadyInDBError if a note with the requested id or aliases already exists + * @throws ForbiddenIdError if the requested id or aliases is forbidden + * @throws MaximumDocumentLengthExceededError if the noteContent is longer than the maxDocumentLength + * @throws GenericDBError if the database returned a non-expected value */ async createNote( noteContent: string, ownerUserId: number, givenAlias?: string, ): Promise { - // Check if new note doesn't violate application constraints + // Ensures that a new note doesn't violate application constraints if (noteContent.length > this.noteConfig.maxDocumentLength) { throw new MaximumDocumentLengthExceededError(); } @@ -192,12 +190,12 @@ export class NoteService { } /** - * Get the current content of the note + * Gets the current content of the note * * @param noteId the note to use * @param transaction The optional database transaction to use - * @throws {NotInDBError} the note is not in the DB - * @return {string} the content of the note + * @returns the content of the note + * @throws NotInDBError the note is not found in the database */ async getNoteContent(noteId: number, transaction?: Knex): Promise { const realtimeContent = this.realtimeNoteStore @@ -216,13 +214,13 @@ export class NoteService { } /** - * Get a note by either their id or aliases + * Gets a note's id by their aliases * - * @param alias the notes id or aliases + * @param alias the alias * @param transaction The optional database transaction to use - * @return the note id - * @throws {NotInDBError} there is no note with this id or aliases - * @throws {ForbiddenIdError} the requested id or aliases is forbidden + * @returns the note id + * @throws NotInDBError if there is no note with this alias + * @throws ForbiddenIdError if the requested note with the alias is forbidden */ async getNoteIdByAlias(alias: string, transaction?: Knex): Promise { const dbActor = transaction ?? this.knex; @@ -233,12 +231,6 @@ export class NoteService { ); } - this.logger.debug(`Trying to find note '${alias}'`, 'getNoteIdByAlias'); - - /* - * This query gets the note's aliases, owner, groupPermissions (and the groups), userPermissions (and the users) and tags and - * then only selects the note, that has a alias with this name. - */ const note = await dbActor(TableAlias) .select>(`${TableNote}.${FieldNameNote.id}`) .where(FieldNameAlias.alias, alias) @@ -261,8 +253,8 @@ export class NoteService { /** * Deletes a note * - * @param noteId If of the note to delete - * @throws {NotInDBError} if there is no note with this id + * @param noteId Id of the note to delete + * @throws NotInDBError if there is no note with this id */ async deleteNote(noteId: Note[FieldNameNote.id]): Promise { this.eventEmitter.emit(NoteEvent.DELETION, noteId); @@ -275,13 +267,12 @@ export class NoteService { } /** + * Updates the content of a note + * The realtime connection is closed in beforehand to ensure that realtime editing does not interfere with the update * - * Update the content of a note - * - * @param noteId - the note - * @param noteContent - the new content - * @return the note with a new revision and new content - * @throws {NotInDBError} there is no note with this id or aliases + * @param noteId the note id + * @param noteContent the new content + * @throws NotInDBError if there is no note with this id or aliases */ async updateNote(noteId: number, noteContent: string): Promise { this.eventEmitter.emit(NoteEvent.CLOSE_REALTIME, noteId); @@ -289,10 +280,12 @@ export class NoteService { } /** - * Build NotePermissionsDto from a note. - * @param noteId The id of the ntoe to get the permissions for + * Builds a NotePermissionsDto for a note + * This method is a wrapper around the innerToNotePermissionsDto method to ensure a single transaction is used + * + * @param noteId The id of the note to get the permissions for * @param transaction The optional database transaction to use - * @return The built NotePermissionDto + * @returns The built NotePermissionDto */ async toNotePermissionsDto( noteId: number, @@ -306,7 +299,15 @@ export class NoteService { return await this.innerToNotePermissionsDto(noteId, transaction); } - async innerToNotePermissionsDto( + /** + * Builds a NotePermissionsDto for a note + * + * @param noteId The id of the note to get the permissions for + * @param transaction The database transaction to use + * @returns The built NotePermissionDto + * @throws NotInDBError if the note does not exist + */ + private async innerToNotePermissionsDto( noteId: number, transaction: Knex, ): Promise { @@ -374,11 +375,12 @@ export class NoteService { } /** - * @async - * Build NoteMetadataDto from a note. - * @param noteId The if of the note to get the metadata for + * Builds a NoteMetadataDto for a note + * This method is a wrapper around the innerToNoteMetadataDto method to ensure a single transaction is used + * + * @param noteId The id of the note to get the metadata for * @param transaction The optional database transaction to use - * @return The built NoteMetadataDto + * @returns The built NoteMetadataDto */ async toNoteMetadataDto( noteId: number, @@ -392,6 +394,14 @@ export class NoteService { return await this.innerToNoteMetadataDto(noteId, transaction); } + /** + * Builds a NoteMetadataDto for a note + * + * @param noteId The id of the note to get the metadata for + * @param transaction The database transaction to use + * @returns The built NoteMetadataDto + * @throws NotInDBError if the note does not exist or has no primary alias + */ private async innerToNoteMetadataDto( noteId: number, transaction: Knex, @@ -473,7 +483,7 @@ export class NoteService { * Gets the note data for the note DTO * * @param noteId The id of the note to transform - * @return {NoteDto} the built NoteDto + * @returns The built NoteDto */ async toNoteDto(noteId: number): Promise { return await this.knex.transaction(async (transaction) => { diff --git a/backend/src/permissions/note-permission.enum.ts b/backend/src/permissions/note-permission.enum.ts index 7165c499d..79da497ac 100644 --- a/backend/src/permissions/note-permission.enum.ts +++ b/backend/src/permissions/note-permission.enum.ts @@ -18,7 +18,7 @@ export enum NotePermissionLevel { * Returns the display name for the given {@link NotePermissionLevel}. * * @param {NotePermissionLevel} value the note permission to display - * @return {string} The display name + * @returns The display name */ export function getNotePermissionLevelDisplayName( value: NotePermissionLevel, diff --git a/backend/src/permissions/permission.service.ts b/backend/src/permissions/permission.service.ts index 202bd368e..a1247b088 100644 --- a/backend/src/permissions/permission.service.ts +++ b/backend/src/permissions/permission.service.ts @@ -90,7 +90,7 @@ export class PermissionService { * Checks if the given {@link User} is allowed to create notes. * * @param username - The user whose permission should be checked. Value is null if guest access should be checked - * @return if the user is allowed to create notes + * @returns if the user is allowed to create notes */ public mayCreate(username: string | null): boolean { return ( @@ -105,7 +105,7 @@ export class PermissionService { * @param userId The id of the user * @param noteId The id of the note * @param transaction Optional transaction to use - * @return true if the user is the owner of the note + * @returns true if the user is the owner of the note */ async isOwner( userId: number | null, @@ -135,7 +135,7 @@ export class PermissionService { * * @param {number | null} userId The user whose permission should be checked * @param {number} noteId The note that is accessed by the given user - * @return {Promise} The determined permission + * @returns {Promise} The determined permission */ public async determinePermission( userId: number, @@ -284,7 +284,7 @@ export class PermissionService { * @param noteId the note * @param userId the user for which the permission should be set * @param canEdit specifies if the user can edit the note - * @return the note with the new permission + * @returns the note with the new permission */ async setUserPermission( noteId: number, @@ -361,7 +361,7 @@ export class PermissionService { * Remove permission for a specific group on a note. * @param noteId - the note * @param groupId - the group for which the permission should be set - * @return the note with the new permission + * @returns the note with the new permission */ async removeGroupPermission(noteId: number, groupId: number): Promise { const result = await this.knex(TableNoteGroupPermission) @@ -382,7 +382,7 @@ export class PermissionService { * Updates the owner of a note. * @param noteId - the note to use * @param newOwnerId - the new owner - * @return the updated note + * @returns the updated note */ async changeOwner(noteId: number, newOwnerId: number): Promise { const result = await this.knex(TableNote) diff --git a/backend/src/permissions/utils/convert-guest-access-to-note-permission-level.ts b/backend/src/permissions/utils/convert-guest-access-to-note-permission-level.ts index 55cbdd1d9..13c851e0e 100644 --- a/backend/src/permissions/utils/convert-guest-access-to-note-permission-level.ts +++ b/backend/src/permissions/utils/convert-guest-access-to-note-permission-level.ts @@ -11,7 +11,7 @@ import { NotePermissionLevel } from '../note-permission.enum'; * Converts the given guest access level to the highest possible {@link NotePermissionLevel}. * * @param guestAccess the guest access level to should be converted - * @return the {@link NotePermissionLevel} representation + * @returns the {@link NotePermissionLevel} representation */ export function convertPermissionLevelToNotePermissionLevel( guestAccess: PermissionLevel, diff --git a/backend/src/realtime/realtime-note/random-word-lists/name-randomizer.ts b/backend/src/realtime/realtime-note/random-word-lists/name-randomizer.ts index 5f597d433..ef1b38ec0 100644 --- a/backend/src/realtime/realtime-note/random-word-lists/name-randomizer.ts +++ b/backend/src/realtime/realtime-note/random-word-lists/name-randomizer.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,7 +8,7 @@ import { adjectives, items } from './random-words'; /** * Generates a random names based on an adjective and a noun. * - * @return the generated name + * @returns the generated name */ export function generateRandomName(): string { const adjective = generateRandomWord(adjectives); diff --git a/backend/src/realtime/realtime-note/realtime-note-store.ts b/backend/src/realtime/realtime-note/realtime-note-store.ts index 0503d8f70..abc97e51b 100644 --- a/backend/src/realtime/realtime-note/realtime-note-store.ts +++ b/backend/src/realtime/realtime-note/realtime-note-store.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -18,7 +18,7 @@ export class RealtimeNoteStore { * @param initialTextContent the initial text content of realtime doc * @param initialYjsState the initial yjs state. If provided this will be used instead of the text content * @throws Error if there is already an realtime note for the given note. - * @return The created realtime note + * @returns The created realtime note */ public create( noteId: number, @@ -43,7 +43,7 @@ export class RealtimeNoteStore { /** * Retrieves a {@link RealtimeNote} that is linked to the given {@link Note} id. * @param noteId The id of the {@link Note} - * @return A {@link RealtimeNote} or {@code undefined} if no instance is existing. + * @returns A {@link RealtimeNote} or {@code undefined} if no instance is existing. */ public find(noteId: number): RealtimeNote | undefined { return this.noteIdToRealtimeNote.get(noteId); diff --git a/backend/src/realtime/realtime-note/realtime-note.service.ts b/backend/src/realtime/realtime-note/realtime-note.service.ts index 588cbf02e..78dde5d53 100644 --- a/backend/src/realtime/realtime-note/realtime-note.service.ts +++ b/backend/src/realtime/realtime-note/realtime-note.service.ts @@ -61,7 +61,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown { * Creates or reuses a {@link RealtimeNote} that is handling the real time editing of the {@link Note} which is identified by the given note id. * @param noteId The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved. * @throws NotInDBError if note doesn't exist or has no revisions. - * @return A {@link RealtimeNote} that is linked to the given note. + * @returns A {@link RealtimeNote} that is linked to the given note. */ public async getOrCreateRealtimeNote(noteId: number): Promise { return ( @@ -75,7 +75,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown { * * @param noteId The note for which the realtime note should be created * @throws NotInDBError if note doesn't exist or has no revisions. - * @return The created realtime note + * @returns The created realtime note */ private async createNewRealtimeNote(noteId: number): Promise { const lastRevision = await this.revisionsService.getLatestRevision(noteId); diff --git a/backend/src/realtime/realtime-note/realtime-note.ts b/backend/src/realtime/realtime-note/realtime-note.ts index ad6d003c5..77c758e8d 100644 --- a/backend/src/realtime/realtime-note/realtime-note.ts +++ b/backend/src/realtime/realtime-note/realtime-note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -114,7 +114,7 @@ export class RealtimeNote extends EventEmitter2 { /** * Checks if there's still clients connected to this note. * - * @return {@code true} if there a still clinets connected, otherwise {@code false} + * @returns {@code true} if there a still clinets connected, otherwise {@code false} */ public hasConnections(): boolean { return this.clients.size !== 0; @@ -123,7 +123,7 @@ export class RealtimeNote extends EventEmitter2 { /** * Returns all {@link RealtimeConnection WebsocketConnections} currently hold by this note. * - * @return an array of {@link RealtimeConnection WebsocketConnections} + * @returns an array of {@link RealtimeConnection WebsocketConnections} */ public getConnections(): RealtimeConnection[] { return [...this.clients]; @@ -132,7 +132,7 @@ export class RealtimeNote extends EventEmitter2 { /** * Get the {@link RealtimeDoc realtime note} of the note. * - * @return the {@link RealtimeDoc realtime note} of the note + * @returns the {@link RealtimeDoc realtime note} of the note */ public getRealtimeDoc(): RealtimeDoc { return this.doc; @@ -141,7 +141,7 @@ export class RealtimeNote extends EventEmitter2 { /** * Get the {@link Note note} that is edited. * - * @return the {@link Note note} + * @returns the {@link Note note} */ public getNoteId(): number { return this.noteId; diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts index 5a6ac797e..10dc8238b 100644 --- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts +++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts @@ -79,7 +79,7 @@ export class MockConnectionBuilder { /** * Creates a new connection based on the given configuration. * - * @return {RealtimeConnection} The constructed mocked connection + * @returns {RealtimeConnection} The constructed mocked connection * @throws Error if neither withGuestUser nor withLoggedInUser has been called. */ public build(): RealtimeConnection { diff --git a/backend/src/realtime/websocket/utils/extract-note-id-from-request-url.ts b/backend/src/realtime/websocket/utils/extract-note-id-from-request-url.ts index 85df61dc8..63a3e3f98 100644 --- a/backend/src/realtime/websocket/utils/extract-note-id-from-request-url.ts +++ b/backend/src/realtime/websocket/utils/extract-note-id-from-request-url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,7 +9,7 @@ import { IncomingMessage } from 'http'; * Extracts the note id from the url of the given request. * * @param request The request whose URL should be extracted - * @return The extracted note id + * @returns The extracted note id * @throws Error if the given string isn't a valid realtime URL path */ export function extractNoteAliasFromRequestUrl( diff --git a/backend/src/realtime/websocket/websocket.gateway.ts b/backend/src/realtime/websocket/websocket.gateway.ts index 97a8cdff3..523396c80 100644 --- a/backend/src/realtime/websocket/websocket.gateway.ts +++ b/backend/src/realtime/websocket/websocket.gateway.ts @@ -118,7 +118,7 @@ export class WebsocketGateway implements OnGatewayConnection { * Finds the user id whose session cookie is saved in the given {@link IncomingMessage}. * * @param request The request that contains the session cookie - * @return The found user id + * @returns The found user id */ private async findUserIdByRequestSession( request: IncomingMessage, diff --git a/backend/src/revisions/revisions.service.ts b/backend/src/revisions/revisions.service.ts index a853ce466..9f46b127b 100644 --- a/backend/src/revisions/revisions.service.ts +++ b/backend/src/revisions/revisions.service.ts @@ -59,7 +59,7 @@ export class RevisionsService { * Returns all revisions of a note * * @param noteId The id of the note - * @return The list of revisions + * @returns The list of revisions */ async getAllRevisionMetadataDto( noteId: number, @@ -294,14 +294,14 @@ export class RevisionsService { * Creates (but does not persist(!)) a new {@link Revision} for the given {@link Note}. * Useful if the revision is saved together with the note in one action. * - * @async + * * @param noteId The note for which the revision should be created * @param newContent The new note content * @param firstRevision Whether this is called for the first revision of a note * @param transaction The optional pre-existing database transaction to use * @param yjsStateVector The yjs state vector that describes the new content - * @return {Revision} the created revision - * @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed + * @returns {Revision} the created revision + * @returns {undefined} if the revision couldn't be created because e.g. the content hasn't changed */ async createRevision( noteId: number, diff --git a/backend/src/revisions/utils/extract-revision-metadata-from-content.ts b/backend/src/revisions/utils/extract-revision-metadata-from-content.ts index 2e0b89809..51978a2fe 100644 --- a/backend/src/revisions/utils/extract-revision-metadata-from-content.ts +++ b/backend/src/revisions/utils/extract-revision-metadata-from-content.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -31,7 +31,7 @@ interface FrontmatterParserResult { /** * Parses the frontmatter of the given content and extracts the metadata that are necessary to create a new revision.. * - * @param {string} content the revision content that contains the frontmatter. + * @param content the revision content that contains the frontmatter. */ export function extractRevisionMetadataFromContent( content: string, diff --git a/backend/src/sessions/session.service.ts b/backend/src/sessions/session.service.ts index 7639ae202..dff6b42c0 100644 --- a/backend/src/sessions/session.service.ts +++ b/backend/src/sessions/session.service.ts @@ -40,7 +40,7 @@ export class SessionService { * Returns the currently used session store for usage outside of the HTTP session context * Note that this method is also used for connecting the session store with NestJS initially * - * @return The used session store + * @returns The used session store */ getSessionStore(): KeyvSessionStore { return this.sessionStore; @@ -50,7 +50,7 @@ export class SessionService { * Finds the username of the user that has the given session id * * @param sessionId The session id for which the owning user should be found - * @return A Promise that either resolves with the username or rejects with an error + * @returns A Promise that either resolves with the username or rejects with an error */ async getUserIdForSessionId( sessionId: string, @@ -63,7 +63,7 @@ export class SessionService { * Extracts the hedgedoc session cookie from the given {@link IncomingMessage request} and checks if the signature is correct. * * @param request The http request that contains a session cookie - * @return An {@link Optional optional} that either contains the extracted session id or is empty if no session cookie has been found + * @returns An {@link Optional optional} that either contains the extracted session id or is empty if no session cookie has been found * @throws Error if the cookie has been found but the content is malformed * @throws Error if the cookie has been found but the content isn't signed */ @@ -79,7 +79,7 @@ export class SessionService { * Parses the given session cookie content and extracts the session id * * @param rawCookie The cookie to parse - * @return The extracted session id + * @returns The extracted session id * @throws Error if the cookie has been found but the content is malformed * @throws Error if the cookie has been found but the content isn't signed */ diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 096c4f8c4..b83c336fd 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -44,7 +44,7 @@ export class UsersService { * @param [email] New user's email address if exists * @param [photoUrl] URL of the user's profile picture if exists * @param transaction The optional transaction to access the db - * @return The id of newly created user + * @returns The id of newly created user * @throws {BadRequestException} if the username contains invalid characters or is too short * @throws {AlreadyInDBError} the username is already taken. * @thorws {GenericDBError} the database returned a non-expected value @@ -96,7 +96,7 @@ export class UsersService { /** * Creates a new guest user with a random displayName * - * @return The guest uuid and the id of the newly created user + * @returns The guest uuid and the id of the newly created user * @throws {GenericDBError} the database returned a non-expected value */ async createGuestUser(): Promise<[string, number]> { @@ -195,7 +195,7 @@ export class UsersService { * Checks if a given username is already taken * * @param username The username to check - * @return true if the user exists, false otherwise + * @returns true if the user exists, false otherwise */ async isUsernameTaken(username: string): Promise { const result = await this.knex(TableUser) @@ -209,7 +209,7 @@ export class UsersService { * * @param userId The id of the user to check * @param transaction the optional transaction to access the db - * @return true if the user is registered, false otherwise + * @returns true if the user is registered, false otherwise */ async isRegisteredUser( userId: User[FieldNameUser.id], @@ -228,7 +228,7 @@ export class UsersService { * Fetches the userId for a given username from the database * * @param username The username to fetch - * @return The found user object + * @returns The found user object * @throws {NotInDBError} if the user could not be found */ async getUserIdByUsername(username: string): Promise { @@ -250,7 +250,7 @@ export class UsersService { * Fetches the userId for a given username from the database * * @param uuid The uuid to fetch - * @return The found user object + * @returns The found user object * @throws {NotInDBError} if the user could not be found */ async getUserIdByGuestUuid(uuid: string): Promise { @@ -272,7 +272,7 @@ export class UsersService { * Fetches the user object for a given username from the database * * @param username The username to fetch - * @return The found user object + * @returns The found user object * @throws {NotInDBError} if the user could not be found */ async getUserDtoByUsername(username: string): Promise { @@ -295,7 +295,7 @@ export class UsersService { * Fetches the user object for a given username from the database * * @param userId The username to fetch - * @return The found user object + * @returns The found user object * @throws {NotInDBError} if the user could not be found */ async getUserById(userId: number): Promise { @@ -318,7 +318,7 @@ export class UsersService { * @param username The username to use as a seed when generating a random avatar * @param email The email address of the user for using livbravatar if configured * @param photoUrl The user-provided photo URL - * @return A URL to the user's profile picture. + * @returns A URL to the user's profile picture. */ private generatePhotoUrl( username: string, @@ -343,7 +343,7 @@ export class UsersService { * Creates a random author style index based on a hashing of the username * * @param username The username is used as input for the hash - * @return An index between 0 and 8 (including 0 and 8) + * @returns An index between 0 and 8 (including 0 and 8) */ private generateAuthorStyleIndex(username: string): number { let hash = 0; @@ -358,7 +358,7 @@ export class UsersService { * * @param user The user to fetch their data for * @param authProvider The auth provider used for the current login session - * @return The built OwnUserInfoDto + * @returns The built OwnUserInfoDto */ toLoginUserInfoDto( user: User, diff --git a/backend/src/utils/server-version.ts b/backend/src/utils/server-version.ts index bbc0cfe59..8c4d0ee35 100644 --- a/backend/src/utils/server-version.ts +++ b/backend/src/utils/server-version.ts @@ -13,7 +13,7 @@ let versionCache: ServerVersionDto | undefined = undefined; /** * Reads the HedgeDoc version from the root package.json. This is done only once per run. * - * @return {Promise} A Promise that contains the parsed server version. + * @returns {Promise} A Promise that contains the parsed server version. * @throws {Error} if the package.json couldn't be found or doesn't contain a correct version. */ export async function getServerVersionFromPackageJson(): Promise { diff --git a/backend/test/test-setup.ts b/backend/test/test-setup.ts index 91a9faf86..ebd317d81 100644 --- a/backend/test/test-setup.ts +++ b/backend/test/test-setup.ts @@ -414,17 +414,20 @@ export class TestSetupBuilder { ); // Create identities for login - await this.testSetup.localIdentityService.createLocalIdentity( + await this.testSetup.localIdentityService.createUserWithLocalIdentity( this.testSetup.users[0], password1, + '', ); - await this.testSetup.localIdentityService.createLocalIdentity( + await this.testSetup.localIdentityService.createUserWithLocalIdentity( this.testSetup.users[1], password2, + '', ); - await this.testSetup.localIdentityService.createLocalIdentity( + await this.testSetup.localIdentityService.createUserWithLocalIdentity( this.testSetup.users[2], password3, + '', ); // create auth tokens