wip: chore(backend): update and unify ESDoc and parameter names

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-05-20 13:16:02 +00:00
parent 327cc1f925
commit d68873de85
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
32 changed files with 289 additions and 310 deletions

View file

@ -7,8 +7,6 @@ import { AliasDto } from '@hedgedoc/commons';
import {
Alias,
FieldNameAlias,
FieldNameNote,
Note,
TableAlias,
TypeInsertAlias,
} from '@hedgedoc/database';
@ -64,8 +62,8 @@ export class AliasService {
* @throws {ForbiddenIdError} The requested alias is forbidden
*/
async addAlias(
noteId: Note[FieldNameNote.id],
alias: Alias[FieldNameAlias.alias],
noteId: number,
alias: string,
transaction?: Knex,
): Promise<void> {
const dbActor: Knex = transaction ? transaction : this.knex;
@ -93,10 +91,7 @@ export class AliasService {
* @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<void> {
async makeAliasPrimary(noteId: number, alias: string): Promise<void> {
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,7 +125,7 @@ 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
@ -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
*/
async getPrimaryAliasByNoteId(
noteId: number,
transaction?: Knex,
): Promise<Alias[FieldNameAlias.alias]> {
): Promise<string> {
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',
);
@ -230,7 +227,7 @@ export class AliasService {
* @throws {AlreadyInDBError} The requested alias already exists
*/
async ensureAliasIsAvailable(
alias: Alias[FieldNameAlias.alias],
alias: string,
transaction?: Knex,
): Promise<void> {
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<boolean> {
async isAliasUsed(alias: string, transaction?: Knex): Promise<boolean> {
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 {

View file

@ -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
* @throws TokenNotValidError if the token is not valid
* @returns The userId associated with the token
* @throws {TokenNotValidError} if the token is not valid
*/
async getUserIdForToken(tokenString: string): Promise<number> {
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<ApiTokenWithSecretDto> {
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,37 +125,34 @@ 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
* @param tokenHash The hash from the database
* @param validUntil Expiry of the API token
* @throws TokenNotValidError if the token is invalid
* @throws {TokenNotValidError} if the token is invalid
*/
ensureTokenIsValid(
secret: string,
@ -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<ApiToken[]> {
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<void> {
@ -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',
);
}

View file

@ -51,7 +51,7 @@ export class LocalController {
@Body() registerDto: RegisterDto,
): Promise<void> {
await this.localIdentityService.checkPasswordStrength(registerDto.password);
const userId = await this.localIdentityService.createLocalIdentity(
const userId = await this.localIdentityService.createUserWithLocalIdentity(
registerDto.username,
registerDto.password,
registerDto.displayName,

View file

@ -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,
@ -80,13 +74,12 @@ export class IdentityService {
/**
* 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 identiy uses that.
* @param transaction the database transaction to use if any
*/
async createIdentity(
userId: number,
@ -97,14 +90,13 @@ export class IdentityService {
transaction?: Knex,
): Promise<void> {
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);
});
}
/**
@ -118,7 +110,7 @@ export class IdentityService {
* @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<User[FieldNameUser.id]> {
): Promise<number> {
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<User[FieldNameUser.id]> {
): Promise<number> {
const profileEditsAllowed = this.authConfig.common.allowProfileEdits;
const chooseUsernameAllowed = this.authConfig.common.allowChooseUsername;

View file

@ -32,7 +32,7 @@ const LDAP_ERROR_MAP: Record<string, string> = {
'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
*
* @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,18 +130,21 @@ 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
* @returns {HttpException} 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(

View file

@ -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
*
* @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<User[FieldNameUser.id]> {
): Promise<number> {
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,11 @@ 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
* @throws {InvalidCredentialsError} if the credentials are invalid
*/
async checkLocalPassword(
username: string,
@ -129,12 +122,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 +137,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
*
* @param password The password to check
* @throws {PasswordTooWeakError} if the password is too weak
*/
async checkPasswordStrength(password: string): Promise<void> {
if (password.length < 6) {

View file

@ -69,16 +69,18 @@ export class OidcService {
}
/**
* @async
* Fetches the client and its config (issuer, metadata) for the given OIDC configuration.
* Fetches the client and its config (issuer, metadata) for the given OIDC configuration
*
* @param {OidcConfig} oidcConfig The OIDC configuration to fetch the client config for
* @returns {OidcClientConfigEntry} A promise that resolves to the client configuration.
* @param oidcConfig The OIDC configuration to fetch the client config for
* @returns A promise that resolves to the client configuration.
*/
private async fetchClientConfig(
oidcConfig: OidcConfig,
): Promise<OidcClientConfigEntry> {
const useAutodiscover = oidcConfig.authorizeUrl === undefined;
const useAutodiscover =
oidcConfig.authorizeUrl === undefined ||
oidcConfig.tokenUrl === undefined ||
oidcConfig.userinfoUrl === undefined;
const issuer = useAutodiscover
? await Issuer.discover(oidcConfig.issuer)
: new Issuer({
@ -117,7 +119,7 @@ export class OidcService {
/**
* Generates a secure code verifier for the OIDC login.
*
* @returns {string} The generated code verifier.
* @returns The generated code verifier.
*/
generateCode(): string {
return generators.codeVerifier();
@ -126,7 +128,7 @@ export class OidcService {
/**
* Generates a random state for the OIDC login.
*
* @returns {string} The generated state.
* @returns The generated state.
*/
generateState(): string {
return generators.state();
@ -135,10 +137,10 @@ export class OidcService {
/**
* Generates the authorization URL for the given OIDC identifier and code.
*
* @param {string} oidcIdentifier The identifier of the OIDC configuration
* @param {string} code The code verifier generated for the login
* @param {string} state The state generated for the login
* @returns {string} The generated authorization URL
* @param oidcIdentifier The identifier of the OIDC configuration
* @param code The code verifier generated for the login
* @param state The state generated for the login
* @returns The generated authorization URL
*/
getAuthorizationUrl(
oidcIdentifier: string,
@ -163,7 +165,6 @@ export class OidcService {
}
/**
* @async
* Extracts the user information from the callback and stores them in the session.
* Afterward, the user information is returned.
*
@ -231,20 +232,18 @@ export class OidcService {
};
request.session.providerUserId = userId;
request.session.newUserData = newUserData;
// Cleanup: The code isn't necessary anymore
// Cleanup: The OIDC login code and state are not required anymore
request.session.oidcLoginCode = undefined;
request.session.oidcLoginState = undefined;
return newUserData;
}
/**
* @async
* Checks if an identity exists for a given OIDC user and returns it if it does.
* Checks if an identity exists for a given OIDC user and returns it if it does
*
* @param {string} oidcIdentifier The identifier of the OIDC configuration
* @param {string} oidcUserId The id of the user in the OIDC system
* @returns {Identity} The identity if it exists
* @returns {null} when the identity does not exist
* @param oidcIdentifier The identifier of the OIDC configuration
* @param oidcUserId The id of the user in the OIDC system
* @returns The identity if it exists, null otherwise
*/
async getExistingOidcIdentity(
oidcIdentifier: string,
@ -263,6 +262,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(
@ -277,11 +277,10 @@ export class OidcService {
}
/**
* Returns the logout URL for the given request if the user is logged in with OIDC.
* Returns the logout URL for the given request if the user is logged in with OIDC
*
* @param {RequestWithSession} request The request containing the session
* @returns {string} The logout URL if the user is logged in with OIDC
* @returns {null} when there is no logout URL to redirect to
* @param request The request containing the session
* @returns The logout URL if the user is logged in with OIDC, or null if there is no URL to redirect to
*/
getLogoutUrl(request: RequestWithSession): string | null {
const oidcIdentifier = request.session.authProviderIdentifier;
@ -304,12 +303,12 @@ export class OidcService {
}
/**
* Returns a specific field from the userinfo object or a default value.
* Returns a specific field from the userinfo object or a default value
*
* @param {UserinfoResponse} response The response from the OIDC userinfo endpoint
* @param {string} field The field to get from the response
* @param {string|undefined} defaultValue The default value to return if the value is empty
* @returns {string|undefined} The value of the field from the response or the default value
* @param response The response from the OIDC userinfo endpoint
* @param field The field to get from the response
* @param defaultValue The default value to return if the value is empty
* @returns The value of the field from the response or the default value
*/
private static getResponseFieldValue<T extends string | undefined>(
response: UserinfoResponse,

View file

@ -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 {
@ -33,7 +33,7 @@ export class SessionGuard implements CanActivate {
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;

View file

@ -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;

View file

@ -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.

View file

@ -24,7 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
{
[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,

View file

@ -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<FrontendConfigDto> {
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

View file

@ -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<void> {
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,7 +48,7 @@ export class GroupsService {
* Fetches a group by its identifier name
*
* @param name Name of the group to query
* @return The group
* @returns The group's metadata
* @throws {NotInDBError} if there is no group with this name
*/
async getGroupInfoDtoByName(name: string): Promise<GroupInfoDto> {
@ -76,7 +71,7 @@ export class GroupsService {
*
* @param name Name of the group to query
* @param transaction The optional database transaction to use
* @return The groupId
* @returns The groupId
* @throws {NotInDBError} if there is no group with this name
*/
async getGroupIdByName(name: string, transaction?: Knex): Promise<number> {

View file

@ -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
*/
@ -12,7 +12,7 @@ export interface MediaBackend {
* @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
*/
saveFile(
uuid: string,
@ -33,7 +33,7 @@ export interface MediaBackend {
* @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
*/
getFileUrl(uuid: string, backendData: string | null): Promise<string>;
}

View file

@ -81,7 +81,7 @@ 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
* @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
@ -123,7 +123,7 @@ 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
@ -150,10 +150,10 @@ 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.
* @param uuid - the uuid of the file to get the URL for.
* @returns the URL of the file.
* @throws {MediaBackendError} - there was an error retrieving the url
*/
async getFileUrl(uuid: string): Promise<string> {
@ -178,9 +178,9 @@ export class MediaService {
}
/**
* @async
*
* Find a file entry by its UUID.
* @param {string} uuid - The UUID of the MediaUpload entity to find.
* @param 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.
*/
@ -196,10 +196,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
* @returns {MediaUpload[]} arary of media uploads owned by the user
*/
async getMediaUploadUuidsByUserId(
userId: number,
@ -211,10 +211,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
* @returns {MediaUpload[]} array of media uploads owned by the user
*/
async getMediaUploadUuidsByNoteId(
noteId: number,
@ -228,9 +228,9 @@ export class MediaService {
}
/**
* @async
*
* Set the note of a mediaUpload to null
* @param {string} uuid - the media upload to be changed
* @param uuid - the media upload to be changed
*/
async removeNoteFromMediaUpload(uuid: string): Promise<void> {
this.logger.debug(

View file

@ -79,7 +79,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<number[]> {
const result = await this.knex(TableNote)
@ -94,7 +94,7 @@ 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
* @returns 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
@ -197,7 +197,7 @@ export class NoteService {
* @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
*/
async getNoteContent(noteId: number, transaction?: Knex): Promise<string> {
const realtimeContent = this.realtimeNoteStore
@ -220,7 +220,7 @@ export class NoteService {
*
* @param alias the notes id or aliases
* @param transaction The optional database transaction to use
* @return the note id
* @returns the note id
* @throws {NotInDBError} there is no note with this id or aliases
* @throws {ForbiddenIdError} the requested id or aliases is forbidden
*/
@ -280,7 +280,7 @@ export class NoteService {
*
* @param noteId - the note
* @param noteContent - the new content
* @return the note with a new revision and new content
* @returns the note with a new revision and new content
* @throws {NotInDBError} there is no note with this id or aliases
*/
async updateNote(noteId: number, noteContent: string): Promise<void> {
@ -292,7 +292,7 @@ export class NoteService {
* Build NotePermissionsDto from a note.
* @param noteId The id of the ntoe 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,
@ -374,11 +374,11 @@ export class NoteService {
}
/**
* @async
*
* Build NoteMetadataDto from a note.
* @param noteId The if 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,
@ -473,7 +473,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 {NoteDto} the built NoteDto
*/
async toNoteDto(noteId: number): Promise<NoteDto> {
return await this.knex.transaction(async (transaction) => {

View file

@ -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,

View file

@ -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<NotePermissionLevel>} The determined permission
* @returns {Promise<NotePermissionLevel>} 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<void> {
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<void> {
const result = await this.knex(TableNote)

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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<RealtimeNote> {
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<RealtimeNote> {
const lastRevision = await this.revisionsService.getLatestRevision(noteId);

View file

@ -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<RealtimeNoteEventMap> {
/**
* 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<RealtimeNoteEventMap> {
/**
* 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<RealtimeNoteEventMap> {
/**
* 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<RealtimeNoteEventMap> {
/**
* Get the {@link Note note} that is edited.
*
* @return the {@link Note note}
* @returns the {@link Note note}
*/
public getNoteId(): number {
return this.noteId;

View file

@ -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 {

View file

@ -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(

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<SessionState> {
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
*/

View file

@ -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<boolean> {
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<number> {
@ -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<User[FieldNameUser.id]> {
@ -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<UserInfoDto> {
@ -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<User> {
@ -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,

View file

@ -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<ServerVersionDto>} A Promise that contains the parsed server version.
* @returns {Promise<ServerVersionDto>} 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<ServerVersionDto> {

View file

@ -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