chore(format): reformat using oxfmt

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-01-12 12:56:09 +01:00
parent 5b0f3a1c55
commit 5574d23889
284 changed files with 2354 additions and 5381 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
{
"singleQuote": true,
"trailingComma": "all"
"trailingComma": "all",
"ignorePatterns": [
"**/*.md"
]
}
+1 -1
View File
@@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
+33 -105
View File
@@ -14,12 +14,7 @@ import authConfigMock from '../config/mock/auth.config.mock';
import databaseConfigMock from '../config/mock/database.config.mock';
import noteConfigMock from '../config/mock/note.config.mock';
import { expectBindings } from '../database/mock/expect-bindings';
import {
mockDelete,
mockInsert,
mockSelect,
mockUpdate,
} from '../database/mock/mock-queries';
import { mockDelete, mockInsert, mockSelect, mockUpdate } from '../database/mock/mock-queries';
import { mockKnexDb } from '../database/mock/provider';
import {
AlreadyInDBError,
@@ -51,12 +46,7 @@ describe('AliasService', () => {
LoggerModule,
await ConfigModule.forRoot({
isGlobal: true,
load: [
appConfigMock,
databaseConfigMock,
authConfigMock,
noteConfigMock,
],
load: [appConfigMock, databaseConfigMock, authConfigMock, noteConfigMock],
}),
],
}).compile();
@@ -81,13 +71,7 @@ describe('AliasService', () => {
describe('addAlias', () => {
describe('creates', () => {
it('a primary alias if no aliases are already present', async () => {
mockSelect(
tracker,
[FieldNameAlias.alias],
TableAlias,
FieldNameAlias.noteId,
[],
);
mockSelect(tracker, [FieldNameAlias.alias], TableAlias, FieldNameAlias.noteId, []);
mockInsert(tracker, TableAlias, [
FieldNameAlias.alias,
FieldNameAlias.isPrimary,
@@ -99,13 +83,7 @@ describe('AliasService', () => {
});
it('a non-primary alias if a primary alias is already present', async () => {
mockSelect(
tracker,
[FieldNameAlias.alias],
TableAlias,
FieldNameAlias.noteId,
[alias2],
);
mockSelect(tracker, [FieldNameAlias.alias], TableAlias, FieldNameAlias.noteId, [alias2]);
mockInsert(tracker, TableAlias, [
FieldNameAlias.alias,
FieldNameAlias.isPrimary,
@@ -120,19 +98,9 @@ describe('AliasService', () => {
describe('makeAliasPrimary', () => {
it('marks the alias as primary', async () => {
mockUpdate(
tracker,
TableAlias,
[FieldNameAlias.isPrimary],
FieldNameAlias.noteId,
);
mockUpdate(tracker, TableAlias, [FieldNameAlias.isPrimary], FieldNameAlias.noteId);
mockUpdate(
tracker,
TableAlias,
[FieldNameAlias.isPrimary],
FieldNameAlias.noteId,
);
mockUpdate(tracker, TableAlias, [FieldNameAlias.isPrimary], FieldNameAlias.noteId);
await service.makeAliasPrimary(noteId1, alias2);
@@ -142,44 +110,24 @@ describe('AliasService', () => {
]);
});
it('does not mark the aliases as primary, if the alias does not exist', async () => {
mockUpdate(
tracker,
TableAlias,
[FieldNameAlias.isPrimary],
FieldNameAlias.noteId,
[],
mockUpdate(tracker, TableAlias, [FieldNameAlias.isPrimary], FieldNameAlias.noteId, []);
await expect(service.makeAliasPrimary(noteId1, 'i_dont_exist')).rejects.toThrow(
GenericDBError,
);
await expect(
service.makeAliasPrimary(noteId1, 'i_dont_exist'),
).rejects.toThrow(GenericDBError);
expectBindings(tracker, 'update', [[null, noteId1]]);
});
it("does not mark the aliases as primary, if the alias can't be made primary", async () => {
mockUpdate(
tracker,
TableAlias,
[FieldNameAlias.isPrimary],
FieldNameAlias.noteId,
[
{
[FieldNameAlias.isPrimary]: null,
},
],
);
mockUpdate(
tracker,
TableAlias,
[FieldNameAlias.isPrimary],
FieldNameAlias.noteId,
[
{
[FieldNameAlias.isPrimary]: null,
},
],
);
await expect(
service.makeAliasPrimary(noteId1, 'i_dont_exist'),
).rejects.toThrow(NotInDBError);
mockUpdate(tracker, TableAlias, [FieldNameAlias.isPrimary], FieldNameAlias.noteId, [
{
[FieldNameAlias.isPrimary]: null,
},
]);
mockUpdate(tracker, TableAlias, [FieldNameAlias.isPrimary], FieldNameAlias.noteId, [
{
[FieldNameAlias.isPrimary]: null,
},
]);
await expect(service.makeAliasPrimary(noteId1, 'i_dont_exist')).rejects.toThrow(NotInDBError);
expectBindings(tracker, 'update', [
[null, noteId1],
[true, noteId1, 'i_dont_exist'],
@@ -208,9 +156,7 @@ describe('AliasService', () => {
[FieldNameAlias.alias, FieldNameAlias.noteId, FieldNameAlias.isPrimary],
0,
);
await expect(service.removeAlias(alias1)).rejects.toThrow(
PrimaryAliasDeletionForbiddenError,
);
await expect(service.removeAlias(alias1)).rejects.toThrow(PrimaryAliasDeletionForbiddenError);
expectBindings(tracker, 'select', [[alias1]]);
expectBindings(tracker, 'delete', [[alias1, noteId1]]);
});
@@ -243,9 +189,7 @@ describe('AliasService', () => {
[FieldNameAlias.noteId, FieldNameAlias.isPrimary],
[],
);
await expect(service.getPrimaryAliasByNoteId(noteId1)).rejects.toThrow(
NotInDBError,
);
await expect(service.getPrimaryAliasByNoteId(noteId1)).rejects.toThrow(NotInDBError);
expectBindings(tracker, 'select', [[noteId1, true]], true);
});
@@ -274,9 +218,7 @@ describe('AliasService', () => {
[FieldNameAlias.noteId, FieldNameAlias.isPrimary],
[],
);
await expect(service.getPrimaryAliasByNoteId(noteId1)).rejects.toThrow(
NotInDBError,
);
await expect(service.getPrimaryAliasByNoteId(noteId1)).rejects.toThrow(NotInDBError);
});
it('returns all aliases for a note', async () => {
@@ -304,35 +246,21 @@ describe('AliasService', () => {
describe('ensureAliasIsAvailable', () => {
it('throws ForbiddenIdError for forbidden aliases', async () => {
await expect(
service.ensureAliasIsAvailable(forbiddenNoteId),
).rejects.toThrow(ForbiddenIdError);
await expect(service.ensureAliasIsAvailable(forbiddenNoteId)).rejects.toThrow(
ForbiddenIdError,
);
});
it('throws AlreadyInDBError for already used aliases', async () => {
mockSelect(
tracker,
[FieldNameAlias.alias],
TableAlias,
FieldNameAlias.alias,
[
{
[FieldNameAlias.alias]: alias1,
},
],
);
await expect(service.ensureAliasIsAvailable(alias1)).rejects.toThrow(
AlreadyInDBError,
);
mockSelect(tracker, [FieldNameAlias.alias], TableAlias, FieldNameAlias.alias, [
{
[FieldNameAlias.alias]: alias1,
},
]);
await expect(service.ensureAliasIsAvailable(alias1)).rejects.toThrow(AlreadyInDBError);
expectBindings(tracker, 'select', [[alias1]]);
});
it('returns void if alias can be used', async () => {
mockSelect(
tracker,
[FieldNameAlias.alias],
TableAlias,
FieldNameAlias.alias,
[],
);
mockSelect(tracker, [FieldNameAlias.alias], TableAlias, FieldNameAlias.alias, []);
await service.ensureAliasIsAvailable(alias1);
expectBindings(tracker, 'select', [[alias1]]);
});
+6 -24
View File
@@ -56,11 +56,7 @@ export class AliasService {
* @throws AlreadyInDBError The alias is already in use.
* @throws ForbiddenIdError The requested alias is forbidden
*/
async addAlias(
noteId: number,
alias: string,
transaction?: Knex,
): Promise<void> {
async addAlias(noteId: number, alias: string, transaction?: Knex): Promise<void> {
const dbActor: Knex = transaction ? transaction : this.knex;
const newAlias: Alias = {
[FieldNameAlias.alias]: alias,
@@ -133,9 +129,7 @@ export class AliasService {
*/
async removeAlias(alias: string): Promise<void> {
await this.knex.transaction(async (transaction) => {
const aliases = await transaction(TableAlias)
.select()
.where(FieldNameAlias.alias, alias);
const aliases = await transaction(TableAlias).select().where(FieldNameAlias.alias, alias);
if (aliases.length !== 1) {
throw new NotInDBError(
`The alias '${alias}' does not exist.`,
@@ -175,10 +169,7 @@ export class AliasService {
* @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<string> {
async getPrimaryAliasByNoteId(noteId: number, transaction?: Knex): Promise<string> {
const dbActor = transaction ?? this.knex;
const primaryAlias = await dbActor(TableAlias)
.select(FieldNameAlias.alias)
@@ -230,10 +221,7 @@ export class AliasService {
* @throws ForbiddenIdError The requested alias is not available
* @throws AlreadyInDBError The requested alias already exists
*/
async ensureAliasIsAvailable(
alias: string,
transaction?: Knex,
): Promise<void> {
async ensureAliasIsAvailable(alias: string, transaction?: Knex): Promise<void> {
if (this.isAliasForbidden(alias)) {
throw new ForbiddenIdError(
`The alias '${alias}' is forbidden by the administrator.`,
@@ -268,19 +256,13 @@ export class AliasService {
* @param transaction The optional transaction to access the db
* @returns true if the alias is already used, false otherwise
*/
private async isAliasUsed(
alias: string,
transaction?: Knex,
): Promise<boolean> {
private 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 alias '${alias}' already exists.`,
'isAliasUsed',
);
this.logger.log(`A note with the alias '${alias}' already exists.`, 'isAliasUsed');
return true;
}
return false;
+23 -92
View File
@@ -12,25 +12,12 @@ import type { Tracker } from 'knex-mock-client';
import appConfigMock from '../config/mock/app.config.mock';
import authConfigMock from '../config/mock/auth.config.mock';
import { expectBindings } from '../database/mock/expect-bindings';
import {
mockDelete,
mockInsert,
mockSelect,
mockUpdate,
} from '../database/mock/mock-queries';
import { mockDelete, mockInsert, mockSelect, mockUpdate } from '../database/mock/mock-queries';
import { mockKnexDb } from '../database/mock/provider';
import { ApiTokenWithSecretDto } from '../dtos/api-token-with-secret.dto';
import {
NotInDBError,
TokenNotValidError,
TooManyTokensError,
} from '../errors/errors';
import { NotInDBError, TokenNotValidError, TooManyTokensError } from '../errors/errors';
import { LoggerModule } from '../logger/logger.module';
import {
dateTimeToDB,
getCurrentDateTime,
isoStringToDateTime,
} from '../utils/datetime';
import { dateTimeToDB, getCurrentDateTime, isoStringToDateTime } from '../utils/datetime';
import * as passwordUtils from '../utils/password';
import { ApiTokenService, AUTH_TOKEN_PREFIX } from './api-token.service';
@@ -88,9 +75,7 @@ describe('ApiTokenService', () => {
describe('fails if', () => {
it('the keyId has an invalid length', async () => {
await expect(
service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.123456789.${validSecret}`,
),
service.getUserIdForToken(`${AUTH_TOKEN_PREFIX}.123456789.${validSecret}`),
).rejects.toThrow(TokenNotValidError);
});
it('the secret is missing', async () => {
@@ -100,50 +85,36 @@ describe('ApiTokenService', () => {
});
it('the secret has an invalid length', async () => {
await expect(
service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.${validKeyId}.${'a'.repeat(73)}`,
),
service.getUserIdForToken(`${AUTH_TOKEN_PREFIX}.${validKeyId}.${'a'.repeat(73)}`),
).rejects.toThrow(TokenNotValidError);
});
it('the prefix is wrong', async () => {
await expect(
service.getUserIdForToken(`hd1.${validKeyId}.${validSecret}`),
).rejects.toThrow(TokenNotValidError);
await expect(service.getUserIdForToken(`hd1.${validKeyId}.${validSecret}`)).rejects.toThrow(
TokenNotValidError,
);
});
it('the token contains sections after the secret', async () => {
await expect(
service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}.extra`,
),
service.getUserIdForToken(`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}.extra`),
).rejects.toThrow(TokenNotValidError);
});
it('the token does not exist in the database', async () => {
mockSelect(
tracker,
[
FieldNameApiToken.secretHash,
FieldNameApiToken.userId,
FieldNameApiToken.validUntil,
],
[FieldNameApiToken.secretHash, FieldNameApiToken.userId, FieldNameApiToken.validUntil],
TableApiToken,
FieldNameApiToken.id,
[],
);
await expect(
service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}`,
),
service.getUserIdForToken(`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}`),
).rejects.toThrow(TokenNotValidError);
expectBindings(tracker, 'select', [[validKeyId]], true);
});
it('ensureTokenIsValid does throw error', async () => {
mockSelect(
tracker,
[
FieldNameApiToken.secretHash,
FieldNameApiToken.userId,
FieldNameApiToken.validUntil,
],
[FieldNameApiToken.secretHash, FieldNameApiToken.userId, FieldNameApiToken.validUntil],
TableApiToken,
FieldNameApiToken.id,
[
@@ -158,9 +129,7 @@ describe('ApiTokenService', () => {
throw new TokenNotValidError();
});
await expect(
service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}`,
),
service.getUserIdForToken(`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}`),
).rejects.toThrow(TokenNotValidError);
expectBindings(tracker, 'select', [[validKeyId]], true);
});
@@ -168,11 +137,7 @@ describe('ApiTokenService', () => {
it('works', async () => {
mockSelect(
tracker,
[
FieldNameApiToken.secretHash,
FieldNameApiToken.userId,
FieldNameApiToken.validUntil,
],
[FieldNameApiToken.secretHash, FieldNameApiToken.userId, FieldNameApiToken.validUntil],
TableApiToken,
FieldNameApiToken.id,
[
@@ -183,13 +148,7 @@ describe('ApiTokenService', () => {
},
],
);
mockUpdate(
tracker,
TableApiToken,
[FieldNameApiToken.lastUsedAt],
FieldNameApiToken.id,
1,
);
mockUpdate(tracker, TableApiToken, [FieldNameApiToken.lastUsedAt], FieldNameApiToken.id, 1);
jest.spyOn(service, 'ensureTokenIsValid').mockImplementation(() => {});
const userByToken = await service.getUserIdForToken(
`${AUTH_TOKEN_PREFIX}.${validKeyId}.${validSecret}`,
@@ -212,11 +171,7 @@ describe('ApiTokenService', () => {
}),
);
await expect(
service.createToken(
userId,
label,
isoStringToDateTime(mockValidUntilIso),
),
service.createToken(userId, label, isoStringToDateTime(mockValidUntilIso)),
).rejects.toThrow(TooManyTokensError);
});
});
@@ -232,17 +187,9 @@ describe('ApiTokenService', () => {
.spyOn(passwordUtils, 'bufferToBase64Url')
.mockReturnValue(validSecret)
.mockReturnValue(validKeyId);
jest
.spyOn(passwordUtils, 'hashApiToken')
.mockReturnValue(mockSecretHash);
jest.spyOn(passwordUtils, 'hashApiToken').mockReturnValue(mockSecretHash);
token = {} as ApiTokenWithSecretDto;
mockSelect(
tracker,
[FieldNameApiToken.id],
TableApiToken,
FieldNameApiToken.userId,
[],
);
mockSelect(tracker, [FieldNameApiToken.id], TableApiToken, FieldNameApiToken.userId, []);
mockInsert(tracker, TableApiToken, [
FieldNameApiToken.createdAt,
FieldNameApiToken.id,
@@ -258,9 +205,7 @@ describe('ApiTokenService', () => {
expect(token.label).toEqual(label);
expect(token.validUntil).toEqual(expectedValidUntil);
expect(token.lastUsedAt).toBeNull();
expect(
token.secret.startsWith(AUTH_TOKEN_PREFIX + '.' + token.keyId),
).toBe(true);
expect(token.secret.startsWith(AUTH_TOKEN_PREFIX + '.' + token.keyId)).toBe(true);
expectBindings(tracker, 'select', [[userId]]);
expectBindings(tracker, 'insert', [
[
@@ -402,23 +347,11 @@ describe('ApiTokenService', () => {
describe('removeToken', () => {
it('throws if the token is not in the database', async () => {
mockDelete(
tracker,
TableApiToken,
[FieldNameApiToken.id, FieldNameApiToken.userId],
0,
);
await expect(service.removeToken(validKeyId, userId)).rejects.toThrow(
NotInDBError,
);
mockDelete(tracker, TableApiToken, [FieldNameApiToken.id, FieldNameApiToken.userId], 0);
await expect(service.removeToken(validKeyId, userId)).rejects.toThrow(NotInDBError);
});
it('works', async () => {
mockDelete(
tracker,
TableApiToken,
[FieldNameApiToken.id, FieldNameApiToken.userId],
1,
);
mockDelete(tracker, TableApiToken, [FieldNameApiToken.id, FieldNameApiToken.userId], 1);
await service.removeToken(validKeyId, userId);
expectBindings(tracker, 'delete', [[validKeyId, userId]]);
});
@@ -437,9 +370,7 @@ describe('ApiTokenService', () => {
describe('auto remove invalid tokens', () => {
beforeEach(() => {
jest
.spyOn(service, 'removeInvalidTokens')
.mockImplementation(async () => {});
jest.spyOn(service, 'removeInvalidTokens').mockImplementation(async () => {});
});
it('handleCron should call removeInvalidTokens', async () => {
+9 -38
View File
@@ -13,11 +13,7 @@ import { randomBytes } from 'node:crypto';
import { ApiTokenWithSecretDto } from '../dtos/api-token-with-secret.dto';
import { ApiTokenDto } from '../dtos/api-token.dto';
import {
NotInDBError,
TokenNotValidError,
TooManyTokensError,
} from '../errors/errors';
import { NotInDBError, TokenNotValidError, TooManyTokensError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import {
dateTimeToDB,
@@ -25,11 +21,7 @@ import {
dbToDateTime,
getCurrentDateTime,
} from '../utils/datetime';
import {
bufferToBase64Url,
checkTokenEquality,
hashApiToken,
} from '../utils/password';
import { bufferToBase64Url, checkTokenEquality, hashApiToken } from '../utils/password';
export const AUTH_TOKEN_PREFIX = 'hd2';
const MESSAGE_TOKEN_INVALID = 'API token is invalid, expired or not found';
@@ -118,9 +110,7 @@ export class ApiTokenService {
.select(FieldNameApiToken.id)
.where(FieldNameApiToken.userId, userId);
if (existingTokensForUser.length >= 200) {
throw new TooManyTokensError(
'There is a maximum of 200 API tokens per user',
);
throw new TooManyTokensError('There is a maximum of 200 API tokens per user');
}
const secret = bufferToBase64Url(randomBytes(64));
@@ -172,11 +162,7 @@ export class ApiTokenService {
* @param validUntil Expiry of the API token
* @throws TokenNotValidError if the token is invalid
*/
ensureTokenIsValid(
secret: string,
tokenHash: string,
validUntil: DateTime,
): void {
ensureTokenIsValid(secret: string, tokenHash: string, validUntil: DateTime): void {
const now = getCurrentDateTime();
// First, verify token expiry is not in the past (cheap operation)
if (validUntil.toMillis() < now.toMillis()) {
@@ -210,25 +196,14 @@ export class ApiTokenService {
])
.where(FieldNameApiToken.userId, userId);
return apiTokens.map(
(
apiToken: Omit<
ApiToken,
FieldNameApiToken.secretHash | FieldNameApiToken.userId
>,
) =>
(apiToken: Omit<ApiToken, FieldNameApiToken.secretHash | FieldNameApiToken.userId>) =>
ApiTokenDto.create({
label: apiToken[FieldNameApiToken.label],
keyId: apiToken[FieldNameApiToken.id],
createdAt: dateTimeToISOString(
dbToDateTime(apiToken[FieldNameApiToken.createdAt]),
),
validUntil: dateTimeToISOString(
dbToDateTime(apiToken[FieldNameApiToken.validUntil]),
),
createdAt: dateTimeToISOString(dbToDateTime(apiToken[FieldNameApiToken.createdAt])),
validUntil: dateTimeToISOString(dbToDateTime(apiToken[FieldNameApiToken.validUntil])),
lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt]
? dateTimeToISOString(
dbToDateTime(apiToken[FieldNameApiToken.lastUsedAt]),
)
? dateTimeToISOString(dbToDateTime(apiToken[FieldNameApiToken.lastUsedAt]))
: null,
}),
);
@@ -269,11 +244,7 @@ export class ApiTokenService {
*/
async removeInvalidTokens(): Promise<void> {
const numberOfDeletedTokens = await this.knex(TableApiToken)
.where(
FieldNameApiToken.validUntil,
'<',
dateTimeToDB(getCurrentDateTime()),
)
.where(FieldNameApiToken.validUntil, '<', dateTimeToDB(getCurrentDateTime()))
.delete();
this.logger.log(
`${numberOfDeletedTokens} expired API tokens were purged from the DB`,
@@ -53,14 +53,9 @@ export class AliasController {
existingAlias: string,
): Promise<number> {
const noteId = await this.noteService.getNoteIdByAlias(existingAlias);
const isUserNoteOwner = await this.permissionsService.isOwner(
userId,
noteId,
);
const isUserNoteOwner = await this.permissionsService.isOwner(userId, noteId);
if (!isUserNoteOwner) {
throw new UnauthorizedException(
'Modifying aliases requires note ownership permissions',
);
throw new UnauthorizedException('Modifying aliases requires note ownership permissions');
}
return noteId;
}
@@ -71,10 +66,7 @@ export class AliasController {
@RequestUserId() userId: number,
@Body() newAliasDto: AliasCreateDto,
): Promise<void> {
const noteId = await this.getNoteIdWithPermissionCheck(
userId,
newAliasDto.noteAlias,
);
const noteId = await this.getNoteIdWithPermissionCheck(userId, newAliasDto.noteAlias);
await this.aliasService.ensureAliasIsAvailable(newAliasDto.newAlias);
await this.aliasService.addAlias(noteId, newAliasDto.newAlias);
}
@@ -97,10 +89,7 @@ export class AliasController {
@Delete(':alias')
@OpenApi(204, 400, 404)
async removeAlias(
@RequestUserId() userId: number,
@Param('alias') alias: string,
): Promise<void> {
async removeAlias(@RequestUserId() userId: number, @Param('alias') alias: string): Promise<void> {
await this.getNoteIdWithPermissionCheck(userId, alias);
await this.aliasService.removeAlias(alias);
}
@@ -4,15 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FieldNameUser, User } from '@hedgedoc/database';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { DateTime } from 'luxon';
@@ -40,9 +32,7 @@ export class ApiTokensController {
@Get()
@OpenApi(200)
getUserTokens(
@RequestUserId({ forbidGuests: true }) userId: number,
): Promise<ApiTokenDto[]> {
getUserTokens(@RequestUserId({ forbidGuests: true }) userId: number): Promise<ApiTokenDto[]> {
return this.apiTokenService.getTokensOfUserById(userId);
}
@@ -56,11 +46,7 @@ export class ApiTokensController {
if (createDto.validUntil !== undefined) {
validUntil = isoStringToDateTime(createDto.validUntil);
}
return await this.apiTokenService.createToken(
userId,
createDto.label,
validUntil,
);
return await this.apiTokenService.createToken(userId, createDto.label, validUntil);
}
@Delete('/:keyId')
@@ -48,11 +48,7 @@ export class AuthController {
}
request.session.destroy((err) => {
if (err) {
this.logger.error(
'Error during logout:' + String(err),
undefined,
'logout',
);
this.logger.error('Error during logout:' + String(err), undefined, 'logout');
throw new InternalServerErrorException('Unable to log out');
}
});
@@ -63,15 +59,11 @@ export class AuthController {
@Get('pending-user')
@OpenApi(200, 400)
getPendingUserData(
@Req() request: RequestWithSession,
): Partial<PendingUserInfoDto> {
getPendingUserData(@Req() request: RequestWithSession): Partial<PendingUserInfoDto> {
if (!request.session.pendingUser?.confirmationData) {
throw new BadRequestException('No pending user data');
}
return PendingUserInfoDto.create(
request.session.pendingUser.confirmationData,
);
return PendingUserInfoDto.create(request.session.pendingUser.confirmationData);
}
@Put('pending-user')
@@ -96,10 +88,8 @@ export class AuthController {
request.session.pendingUser.authProviderIdentifier,
request.session.pendingUser.providerUserId,
);
request.session.authProviderType =
request.session.pendingUser.authProviderType;
request.session.authProviderIdentifier =
request.session.pendingUser.authProviderIdentifier;
request.session.authProviderType = request.session.pendingUser.authProviderType;
request.session.authProviderIdentifier = request.session.pendingUser.authProviderIdentifier;
// Cleanup
request.session.pendingUser = undefined;
}
@@ -5,14 +5,7 @@
*/
import { AuthProviderType } from '@hedgedoc/commons';
import { FieldNameIdentity } from '@hedgedoc/database';
import {
Body,
Controller,
InternalServerErrorException,
Param,
Post,
Req,
} from '@nestjs/common';
import { Body, Controller, InternalServerErrorException, Param, Post, Req } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../../auth/identity.service';
@@ -52,12 +45,11 @@ export class LdapController {
loginDto.password,
);
try {
const identity =
await this.identityService.getIdentityFromUserIdAndProviderType(
userInfo.id,
AuthProviderType.LDAP,
ldapIdentifier,
);
const identity = await this.identityService.getIdentityFromUserIdAndProviderType(
userInfo.id,
AuthProviderType.LDAP,
ldapIdentifier,
);
if (this.identityService.mayUpdateIdentity(ldapIdentifier)) {
await this.usersService.updateUser(
identity[FieldNameIdentity.userId],
@@ -5,15 +5,7 @@
*/
import { AuthProviderType } from '@hedgedoc/commons';
import { FieldNameIdentity, FieldNameUser } from '@hedgedoc/database';
import {
Body,
Controller,
Post,
Put,
Req,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { Body, Controller, Post, Put, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { LocalService } from '../../../../auth/local/local.service';
@@ -72,14 +64,8 @@ export class LocalController {
if (username === null) {
throw new NoLocalIdentityError('User has no username assigned');
}
await this.localIdentityService.checkLocalPassword(
username,
changePasswordDto.currentPassword,
);
await this.localIdentityService.updateLocalPassword(
userId,
changePasswordDto.newPassword,
);
await this.localIdentityService.checkLocalPassword(username, changePasswordDto.currentPassword);
await this.localIdentityService.updateLocalPassword(userId, changePasswordDto.newPassword);
}
@UseGuards(LoginEnabledGuard)
@@ -53,11 +53,7 @@ export class OidcController {
authProviderType: AuthProviderType.OIDC,
authProviderIdentifier: oidcIdentifier,
};
const authorizationUrl = this.oidcService.getAuthorizationUrl(
oidcIdentifier,
code,
state,
);
const authorizationUrl = this.oidcService.getAuthorizationUrl(oidcIdentifier, code, state);
return { url: authorizationUrl };
}
@@ -69,10 +65,7 @@ export class OidcController {
@Req() request: RequestWithSession,
): Promise<{ url: string }> {
try {
const userInfo = await this.oidcService.extractUserInfoFromCallback(
oidcIdentifier,
request,
);
const userInfo = await this.oidcService.extractUserInfoFromCallback(oidcIdentifier, request);
const oidcUserIdentifier = request.session.pendingUser?.providerUserId;
if (!oidcUserIdentifier) {
this.logger.log('No OIDC user identifier in callback', 'callback');
@@ -107,10 +100,7 @@ export class OidcController {
if (error instanceof HttpException) {
throw error;
}
this.logger.log(
'Error during OIDC callback: ' + String(error),
'callback',
);
this.logger.log('Error during OIDC callback: ' + String(error), 'callback');
throw new InternalServerErrorException();
}
}
@@ -52,13 +52,7 @@ export class ExploreController {
@Query('type') type?: OptionalNoteType,
): Promise<NoteExploreEntryDto[]> {
this.checkQueryParams(page, sort, type);
return this.exploreService.getMyNoteExploreEntries(
userId,
page,
type,
sort,
search,
);
return this.exploreService.getMyNoteExploreEntries(userId, page, type, sort, search);
}
@Get('shared')
@@ -71,13 +65,7 @@ export class ExploreController {
@Query('type') type?: OptionalNoteType,
): Promise<NoteExploreEntryDto[]> {
this.checkQueryParams(page, sort, type);
return this.exploreService.getSharedWithMeExploreEntries(
userId,
page,
type,
sort,
search,
);
return this.exploreService.getSharedWithMeExploreEntries(userId, page, type, sort, search);
}
@Get('public')
@@ -89,19 +77,12 @@ export class ExploreController {
@Query('type') type?: OptionalNoteType,
): Promise<NoteExploreEntryDto[]> {
this.checkQueryParams(page, sort, type);
return this.exploreService.getPublicNoteExploreEntries(
page,
type,
sort,
search,
);
return this.exploreService.getPublicNoteExploreEntries(page, type, sort, search);
}
@Get('pinned')
@OpenApi(200)
getMyPinnedNotes(
@RequestUserId() userId: number,
): Promise<NoteExploreEntryDto[]> {
getMyPinnedNotes(@RequestUserId() userId: number): Promise<NoteExploreEntryDto[]> {
return this.exploreService.getMyPinnedNoteExploreEntries(userId);
}
@@ -150,11 +131,7 @@ export class ExploreController {
return entry;
}
private checkQueryParams(
page: number,
sort?: OptionalSortMode,
type?: OptionalNoteType,
): void {
private checkQueryParams(page: number, sort?: OptionalSortMode, type?: OptionalNoteType): void {
this.ensurePageNumberIsValid(page);
this.ensureTypeQueryParamIsValid(type);
this.ensureSortQueryParamIsValid(sort);
+3 -10
View File
@@ -43,16 +43,14 @@ export class MeController {
@Get('media')
@OpenApi(200)
async getMyMedia(@RequestUserId() userId: number): Promise<MediaUploadDto[]> {
const mediaUuids =
await this.mediaService.getMediaUploadUuidsByUserId(userId);
const mediaUuids = await this.mediaService.getMediaUploadUuidsByUserId(userId);
return await this.mediaService.getMediaUploadDtosByUuids(mediaUuids);
}
@Delete()
@OpenApi(204, 404, 500)
async deleteUser(@RequestUserId() userId: number): Promise<void> {
const mediaUploads =
await this.mediaService.getMediaUploadUuidsByUserId(userId);
const mediaUploads = await this.mediaService.getMediaUploadUuidsByUserId(userId);
for (const mediaUpload of mediaUploads) {
await this.mediaService.deleteFile(mediaUpload);
}
@@ -67,11 +65,6 @@ export class MeController {
@RequestUserId({ forbidGuests: true }) userId: number,
@Body('displayName') newDisplayName: string,
): Promise<void> {
await this.userService.updateUser(
userId,
newDisplayName,
undefined,
undefined,
);
await this.userService.updateUser(userId, newDisplayName, undefined, undefined);
}
}
@@ -90,12 +90,7 @@ export class MediaController {
`Received filename '${file.originalname}' for note '${noteId}' from user '${userId}'`,
'uploadMedia',
);
return await this.mediaService.saveFile(
file.originalname,
file.buffer,
userId,
noteId,
);
return await this.mediaService.saveFile(file.originalname, file.buffer, userId, noteId);
}
@Get(':uuid')
@@ -106,12 +101,11 @@ export class MediaController {
@Delete(':uuid')
@OpenApi(204, 403, 404, 500)
async deleteMedia(
@RequestUserId() userId: number,
@Param('uuid') uuid: string,
): Promise<void> {
const hasUserMediaDeletePermission =
await this.permissionsService.checkMediaDeletePermission(userId, uuid);
async deleteMedia(@RequestUserId() userId: number, @Param('uuid') uuid: string): Promise<void> {
const hasUserMediaDeletePermission = await this.permissionsService.checkMediaDeletePermission(
userId,
uuid,
);
if (!hasUserMediaDeletePermission) {
this.logger.warn(
`${userId} tried to delete '${uuid}', but is not the owner of upload or connected note`,
@@ -88,9 +88,7 @@ export class NotesController {
@OpenApi(200)
@RequirePermission(PermissionLevel.READ)
@UseInterceptors(GetNoteIdInterceptor)
async getNotesMedia(
@RequestNoteId() noteId: number,
): Promise<MediaUploadDto[]> {
async getNotesMedia(@RequestNoteId() noteId: number): Promise<MediaUploadDto[]> {
const media = await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
return await this.mediaService.getMediaUploadDtosByUuids(media);
}
@@ -114,11 +112,7 @@ export class NotesController {
@Param('newNoteAlias') noteAlias: string,
@MarkdownBody() text: string,
): Promise<NoteDto> {
const createdNoteId = await this.noteService.createNote(
text,
userId,
noteAlias,
);
const createdNoteId = await this.noteService.createNote(text, userId, noteAlias);
return await this.noteService.toNoteDto(createdNoteId);
}
@@ -133,12 +127,9 @@ export class NotesController {
): Promise<void> {
const isOwner = await this.permissionService.isOwner(userId, noteId);
if (!isOwner) {
throw new PermissionError(
'You do not have the permission to delete this note.',
);
throw new PermissionError('You do not have the permission to delete this note.');
}
const mediaUploads =
await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
const mediaUploads = await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
for (const mediaUpload of mediaUploads) {
if (!noteMediaDeletionDto.keepMedia) {
await this.mediaService.deleteFile(mediaUpload);
@@ -153,9 +144,7 @@ export class NotesController {
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.READ)
@Get(':noteAlias/metadata')
async getNoteMetadata(
@RequestNoteId() noteId: number,
): Promise<NoteMetadataDto> {
async getNoteMetadata(@RequestNoteId() noteId: number): Promise<NoteMetadataDto> {
return await this.noteService.toNoteMetadataDto(noteId);
}
@@ -163,9 +152,7 @@ export class NotesController {
@OpenApi(200, 404)
@RequirePermission(PermissionLevel.READ)
@UseInterceptors(GetNoteIdInterceptor)
async getNoteRevisions(
@RequestNoteId() noteId: number,
): Promise<RevisionMetadataDto[]> {
async getNoteRevisions(@RequestNoteId() noteId: number): Promise<RevisionMetadataDto[]> {
return await this.revisionsService.getAllRevisionMetadataDto(noteId);
}
@@ -175,19 +162,14 @@ export class NotesController {
@UseInterceptors(GetNoteIdInterceptor)
async purgeNoteRevisions(@RequestNoteId() noteId: number): Promise<void> {
await this.revisionsService.purgeRevisions(noteId);
this.logger.debug(
`Successfully purged history of note ${noteId}`,
'purgeNoteRevisions',
);
this.logger.debug(`Successfully purged history of note ${noteId}`, 'purgeNoteRevisions');
}
@Get(':noteAlias/revisions/:revisionUuid')
@OpenApi(200, 404)
@RequirePermission(PermissionLevel.READ)
@UseInterceptors(GetNoteIdInterceptor)
async getNoteRevision(
@Param('revisionUuid') revisionUuid: string,
): Promise<RevisionDto> {
async getNoteRevision(@Param('revisionUuid') revisionUuid: string): Promise<RevisionDto> {
return await this.revisionsService.getRevisionDto(revisionUuid);
}
@@ -218,9 +200,7 @@ export class NotesController {
return await this.permissionService.getPermissionsDtoForNote(noteId);
} catch (e) {
if (e instanceof NotInDBError) {
throw new BadRequestException(
"Can't remove user from permissions. User not known.",
);
throw new BadRequestException("Can't remove user from permissions. User not known.");
}
throw e;
}
@@ -236,8 +216,7 @@ export class NotesController {
): Promise<NotePermissionsDto> {
if ((groupName as SpecialGroup) === SpecialGroup.EVERYONE) {
const maxGuestPermissionLevel = this.noteConfig.permissions.maxGuestLevel;
const requestedPermissionLevel =
convertEditabilityToPermissionLevel(canEdit);
const requestedPermissionLevel = convertEditabilityToPermissionLevel(canEdit);
if (requestedPermissionLevel > maxGuestPermissionLevel) {
throw new BadRequestException(
`Cannot set permission for guest group to '${PermissionLevelNames[requestedPermissionLevel]}' since this is higher than the maximum allowed permission level.`,
@@ -269,9 +248,7 @@ export class NotesController {
@RequestNoteId() noteId: number,
@Body() changeNoteOwnerDto: ChangeNoteOwnerDto,
): Promise<NotePermissionsDto> {
const newOwnerId = await this.userService.getUserIdByUsername(
changeNoteOwnerDto.owner,
);
const newOwnerId = await this.userService.getUserIdByUsername(changeNoteOwnerDto.owner);
await this.permissionService.changeOwner(noteId, newOwnerId);
return await this.permissionService.getPermissionsDtoForNote(noteId);
}
@@ -26,12 +26,8 @@ export class UsersController {
@Post('check')
@HttpCode(200)
@OpenApi(200)
async checkUsername(
@Body() usernameCheck: UsernameCheckDto,
): Promise<UsernameCheckResponseDto> {
const userExists = await this.userService.isUsernameTaken(
usernameCheck.username,
);
async checkUsername(@Body() usernameCheck: UsernameCheckDto): Promise<UsernameCheckResponseDto> {
const userExists = await this.userService.isUsernameTaken(usernameCheck.username);
// TODO Check if username is blocked (https://github.com/hedgedoc/hedgedoc/issues/5794)
return UsernameCheckResponseDto.create({ usernameAvailable: !userExists });
}
@@ -57,14 +57,10 @@ export class AliasController {
@RequestUserId() userId: number,
@Body() newAliasDto: AliasCreateDto,
): Promise<AliasDto> {
const noteId = await this.noteService.getNoteIdByAlias(
newAliasDto.noteAlias,
);
const noteId = await this.noteService.getNoteIdByAlias(newAliasDto.noteAlias);
const isUserOwner = await this.permissionsService.isOwner(userId, noteId);
if (!isUserOwner) {
throw new UnauthorizedException(
'Only the owner of a note can modify its aliases',
);
throw new UnauthorizedException('Only the owner of a note can modify its aliases');
}
await this.aliasService.ensureAliasIsAvailable(newAliasDto.newAlias);
await this.aliasService.addAlias(noteId, newAliasDto.newAlias);
@@ -109,10 +105,7 @@ export class AliasController {
403,
404,
)
async removeAlias(
@RequestUserId() user: number,
@Param('alias') alias: string,
): Promise<void> {
async removeAlias(@RequestUserId() user: number, @Param('alias') alias: string): Promise<void> {
const note = await this.noteService.getNoteIdByAlias(alias);
if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!');
+2 -6
View File
@@ -57,13 +57,9 @@ export class MeController {
isArray: true,
schema: NoteMetadataSchema,
})
async getMyNotes(
@RequestUserId() userId: number,
): Promise<NoteMetadataDto[]> {
async getMyNotes(@RequestUserId() userId: number): Promise<NoteMetadataDto[]> {
const noteIds = await this.notesService.getUserNoteIds(userId);
return await Promise.all(
noteIds.map((note) => this.notesService.toNoteMetadataDto(note)),
);
return await Promise.all(noteIds.map((note) => this.notesService.toNoteMetadataDto(note)));
}
@Get('media')
@@ -17,13 +17,7 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
ApiBody,
ApiConsumes,
ApiHeader,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ApiBody, ApiConsumes, ApiHeader, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { MediaUploadDto } from '../../../dtos/media-upload.dto';
import { PermissionError } from '../../../errors/errors';
@@ -114,18 +108,10 @@ export class MediaController {
@Delete(':uuid')
@OpenApi(204, 403, 404, 500)
async deleteMedia(
@RequestUserId() userId: number,
@Param('uuid') uuid: string,
): Promise<void> {
async deleteMedia(@RequestUserId() userId: number, @Param('uuid') uuid: string): Promise<void> {
const mediaUpload = await this.mediaService.findUploadByUuid(uuid);
if (
await this.permissionsService.checkMediaDeletePermission(userId, uuid)
) {
this.logger.debug(
`Deleting '${uuid}' for user '${userId}'`,
'deleteMedia',
);
if (await this.permissionsService.checkMediaDeletePermission(userId, uuid)) {
this.logger.debug(`Deleting '${uuid}' for user '${userId}'`, 'deleteMedia');
await this.mediaService.deleteFile(uuid);
} else {
this.logger.warn(
@@ -134,9 +120,7 @@ export class MediaController {
);
const mediaUploadNote = mediaUpload[FieldNameMediaUpload.noteId];
throw new PermissionError(
`Neither file '${uuid}' nor note '${
mediaUploadNote ?? 'unknown'
}'is owned by '${userId}'`,
`Neither file '${uuid}' nor note '${mediaUploadNote ?? 'unknown'}'is owned by '${userId}'`,
);
}
}
@@ -128,8 +128,7 @@ export class NotesController {
@RequestNoteId() noteId: number,
@Body() noteMediaDeletionDto: NoteMediaDeletionDto,
): Promise<void> {
const mediaUploads =
await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
const mediaUploads = await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
for (const mediaUpload of mediaUploads) {
if (!noteMediaDeletionDto.keepMedia) {
await this.mediaService.deleteFile(mediaUpload);
@@ -196,9 +195,7 @@ export class NotesController {
403,
404,
)
async getNoteMetadata(
@RequestNoteId() noteId: number,
): Promise<NoteMetadataDto> {
async getNoteMetadata(@RequestNoteId() noteId: number): Promise<NoteMetadataDto> {
return await this.noteService.toNoteMetadataDto(noteId);
}
@@ -214,9 +211,7 @@ export class NotesController {
403,
404,
)
async getPermissions(
@RequestNoteId() noteId: number,
): Promise<NotePermissionsDto> {
async getPermissions(@RequestNoteId() noteId: number): Promise<NotePermissionsDto> {
return await this.permissionService.getPermissionsDtoForNote(noteId);
}
@@ -239,11 +234,7 @@ export class NotesController {
@Body('canEdit') canEdit: boolean,
): Promise<NotePermissionsDto> {
const targetUserId = await this.userService.getUserIdByUsername(username);
await this.permissionService.setUserPermission(
noteId,
targetUserId,
canEdit,
);
await this.permissionService.setUserPermission(noteId, targetUserId, canEdit);
return await this.permissionService.getPermissionsDtoForNote(noteId);
}
@@ -351,10 +342,7 @@ export class NotesController {
@RequestNoteId() noteId: number,
@Body('newPubliclyVisible') newPubliclyVisible: boolean,
): Promise<NoteMetadataDto> {
await this.permissionService.changePubliclyVisible(
noteId,
newPubliclyVisible,
);
await this.permissionService.changePubliclyVisible(noteId, newPubliclyVisible);
return await this.noteService.toNoteMetadataDto(noteId);
}
@@ -372,9 +360,7 @@ export class NotesController {
403,
404,
)
async getNoteRevisions(
@RequestNoteId() noteId: number,
): Promise<RevisionMetadataDto[]> {
async getNoteRevisions(@RequestNoteId() noteId: number): Promise<RevisionMetadataDto[]> {
return await this.revisionsService.getAllRevisionMetadataDto(noteId);
}
@@ -390,9 +376,7 @@ export class NotesController {
403,
404,
)
async getNoteRevision(
@Param('revisionUuid') revisionUuid: string,
): Promise<RevisionDto> {
async getNoteRevision(@Param('revisionUuid') revisionUuid: string): Promise<RevisionDto> {
return await this.revisionsService.getRevisionDto(revisionUuid);
}
@@ -405,11 +389,8 @@ export class NotesController {
isArray: true,
schema: MediaUploadSchema,
})
async getNotesMedia(
@RequestNoteId() noteId: number,
): Promise<MediaUploadDto[]> {
const mediaUuids =
await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
async getNotesMedia(@RequestNoteId() noteId: number): Promise<MediaUploadDto[]> {
const mediaUuids = await this.mediaService.getMediaUploadUuidsByNoteId(noteId);
return await this.mediaService.getMediaUploadDtosByUuids(mediaUuids);
}
}
@@ -34,9 +34,7 @@ export const MarkdownBody = createParamDecorator(
throw new InternalServerErrorException('Failed to parse request body!');
}
} else {
throw new BadRequestException(
'Body Content-Type has to be text/markdown!',
);
throw new BadRequestException('Body Content-Type has to be text/markdown!');
}
},
[
@@ -47,10 +45,7 @@ export const MarkdownBody = createParamDecorator(
`Could not enhance param decorator for target ${target.toString()} because key is undefined`,
);
}
const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(
target,
key,
);
const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(target, key);
if (!ownPropertyDescriptor) {
throw new Error(
// oxlint-disable-next-line @typescript-eslint/no-base-to-string
@@ -33,18 +33,7 @@ import {
unauthorizedDescription,
} from '../descriptions';
export type HttpStatusCodes =
| 200
| 201
| 204
| 302
| 400
| 401
| 403
| 404
| 409
| 413
| 500;
export type HttpStatusCodes = 200 | 201 | 204 | 302 | 400 | 401 | 403 | 404 | 409 | 413 | 500;
/**
* Defines what the open api route should document.
@@ -102,10 +91,7 @@ export const OpenApi = (
isArray = entry.isArray;
schema = entry.schema;
if (entry.mimeType) {
decoratorsToApply.push(
ApiProduces(entry.mimeType),
Header('Content-Type', entry.mimeType),
);
decoratorsToApply.push(ApiProduces(entry.mimeType), Header('Content-Type', entry.mimeType));
}
}
@@ -17,13 +17,11 @@ import { CompleteRequest } from '../request.type';
* Will throw an {@link InternalServerErrorException} if no note is present
*/
// oxlint-disable-next-line @typescript-eslint/naming-convention
export const RequestNoteId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (!request.noteId) {
// We should have a note here, otherwise something is wrong
throw new InternalServerErrorException('Request is missing a noteId');
}
return request.noteId;
},
);
export const RequestNoteId = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (!request.noteId) {
// We should have a note here, otherwise something is wrong
throw new InternalServerErrorException('Request is missing a noteId');
}
return request.noteId;
});
@@ -4,11 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthProviderType } from '@hedgedoc/commons';
import {
createParamDecorator,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { CompleteRequest } from '../request.type';
@@ -25,10 +21,7 @@ type RequestUserIdParameter = {
*/
// oxlint-disable-next-line @typescript-eslint/naming-convention
export const RequestUserId = createParamDecorator(
(
data: RequestUserIdParameter = { forbidGuests: false },
ctx: ExecutionContext,
) => {
(data: RequestUserIdParameter = { forbidGuests: false }, ctx: ExecutionContext) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (
!request.authProviderType ||
@@ -17,15 +17,11 @@ import { CompleteRequest } from '../request.type';
* Will throw an {@link InternalServerErrorException} if no identifier is present
*/
// oxlint-disable-next-line @typescript-eslint/naming-convention
export const SessionAuthProvider = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (!request.session?.authProviderType) {
// We should have an auth provider here, otherwise something is wrong
throw new InternalServerErrorException(
'Session is missing an auth provider identifier',
);
}
return request.session.authProviderType;
},
);
export const SessionAuthProvider = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request: CompleteRequest = ctx.switchToHttp().getRequest();
if (!request.session?.authProviderType) {
// We should have an auth provider here, otherwise something is wrong
throw new InternalServerErrorException('Session is missing an auth provider identifier');
}
return request.session.authProviderType;
});
+9 -18
View File
@@ -5,26 +5,17 @@
*/
export const okDescription = 'This request was successful';
export const foundDescription =
'The requested resource was found at another URL';
export const createdDescription =
'The requested resource was successfully created';
export const noContentDescription =
'The requested resource was successfully deleted';
export const badRequestDescription =
"The request is malformed and can't be processed";
export const unauthorizedDescription =
'Authorization information is missing or invalid';
export const forbiddenDescription =
'Access to the requested resource is not permitted';
export const foundDescription = 'The requested resource was found at another URL';
export const createdDescription = 'The requested resource was successfully created';
export const noContentDescription = 'The requested resource was successfully deleted';
export const badRequestDescription = "The request is malformed and can't be processed";
export const unauthorizedDescription = 'Authorization information is missing or invalid';
export const forbiddenDescription = 'Access to the requested resource is not permitted';
export const notFoundDescription = 'The requested resource was not found';
export const successfullyDeletedDescription =
'The requested resource was successfully deleted';
export const unprocessableEntityDescription =
"The request change can't be processed";
export const successfullyDeletedDescription = 'The requested resource was successfully deleted';
export const unprocessableEntityDescription = "The request change can't be processed";
export const conflictDescription =
'The request conflicts with the current state of the application';
export const payloadTooLargeDescription =
'The note is longer than the maximal allowed length of a note';
export const internalServerErrorDescription =
'The request triggered an internal server error.';
export const internalServerErrorDescription = 'The request triggered an internal server error.';
@@ -54,46 +54,31 @@ describe('extract note from request', () => {
it('will return undefined if no id is present', async () => {
const request = createRequest(undefined, undefined);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
undefined,
);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(undefined);
});
it('can extract an id from parameters', async () => {
const request = createRequest(mockNoteIdOrAlias1, undefined);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
mockNote1,
);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(mockNote1);
});
it('can extract an id from headers if no parameter is given', async () => {
const request = createRequest(undefined, mockNoteIdOrAlias1);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
mockNote1,
);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(mockNote1);
});
it('can extract the first id from multiple id headers', async () => {
const request = createRequest(undefined, [
mockNoteIdOrAlias1,
mockNoteIdOrAlias2,
]);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
mockNote1,
);
const request = createRequest(undefined, [mockNoteIdOrAlias1, mockNoteIdOrAlias2]);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(mockNote1);
});
it('will return undefined if no parameter and empty id header array', async () => {
const request = createRequest(undefined, []);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
undefined,
);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(undefined);
});
it('will prefer the parameter over the header', async () => {
const request = createRequest(mockNoteIdOrAlias1, mockNoteIdOrAlias2);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(
mockNote1,
);
expect(await extractNoteIdFromRequest(request, notesService)).toBe(mockNote1);
});
});
@@ -18,8 +18,7 @@ export async function extractNoteIdFromRequest(
}
function extractNoteAlias(request: CompleteRequest): string | undefined {
const noteAlias =
request.params['noteAlias'] || request.headers['hedgedoc-note'];
const noteAlias = request.params['noteAlias'] || request.headers['hedgedoc-note'];
if (Array.isArray(noteAlias)) {
return noteAlias[0];
}
@@ -31,15 +31,11 @@ export class ApiTokenGuard implements CanActivate {
return false;
}
try {
request.userId = await this.apiTokenService.getUserIdForToken(
token.trim(),
);
request.userId = await this.apiTokenService.getUserIdForToken(token.trim());
request.authProviderType = AuthProviderType.TOKEN;
return true;
} catch (error) {
if (
!(error instanceof TokenNotValidError || error instanceof NotInDBError)
) {
if (!(error instanceof TokenNotValidError || error instanceof NotInDBError)) {
this.logger.error(
`Unknown Error during API token validation: ${String(error)}`,
undefined,
@@ -22,12 +22,7 @@ export class MockApiTokenGuard {
try {
this.userId = await this.usersService.getUserIdByUsername('hardcoded');
} catch {
this.userId = await this.usersService.createUser(
'hardcoded',
'Testy',
null,
null,
);
this.userId = await this.usersService.createUser('hardcoded', 'Testy', null, null);
}
}
req.userId = this.userId;
@@ -26,8 +26,7 @@ describe('get note interceptor', () => {
beforeEach(() => {
notesService = Mock.of<NoteService>({
getNoteIdByAlias: (id) =>
id === mockNoteId ? Promise.resolve(mockNote) : Promise.reject(),
getNoteIdByAlias: (id) => (id === mockNoteId ? Promise.resolve(mockNote) : Promise.reject()),
});
noteFetchSpy = jest.spyOn(notesService, 'getNoteIdByAlias');
});
@@ -3,12 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { NoteService } from '../../../notes/note.service';
@@ -23,10 +18,7 @@ import { CompleteRequest } from '../request.type';
export class GetNoteIdInterceptor implements NestInterceptor {
constructor(private noteService: NoteService) {}
async intercept<T>(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<T>> {
async intercept<T>(context: ExecutionContext, next: CallHandler): Promise<Observable<T>> {
const request: CompleteRequest = context.switchToHttp().getRequest();
const noteId = await extractNoteIdFromRequest(request, this.noteService);
if (noteId !== undefined) {
@@ -3,12 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { NoteService } from '../../../notes/note.service';
@@ -22,10 +17,7 @@ import { CompleteRequest } from '../request.type';
export class NoteHeaderInterceptor implements NestInterceptor {
constructor(private noteService: NoteService) {}
async intercept<T>(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<T>> {
async intercept<T>(context: ExecutionContext, next: CallHandler): Promise<Observable<T>> {
const request: CompleteRequest = context.switchToHttp().getRequest();
const noteId: string = request.headers['hedgedoc-note'] as string;
request.noteId = await this.noteService.getNoteIdByAlias(noteId);
+3 -13
View File
@@ -39,20 +39,13 @@ export async function setupApp(
await runMigrations(app, logger);
// Setup session handling
setupSessionMiddleware(
app,
authConfig,
app.get(SessionService).getSessionStore(),
);
setupSessionMiddleware(app, authConfig, app.get(SessionService).getSessionStore());
// Enable web security aspects
app.enableCors({
origin: appConfig.rendererBaseUrl,
});
logger.log(
`Enabling CORS for '${appConfig.rendererBaseUrl}'`,
'AppBootstrap',
);
logger.log(`Enabling CORS for '${appConfig.rendererBaseUrl}'`, 'AppBootstrap');
// TODO Add rate limiting (#442)
// TODO Add CSP (#1309)
// TODO Add common security headers and CSRF (#201)
@@ -70,10 +63,7 @@ export async function setupApp(
prefix: '/uploads/',
});
}
logger.log(
`Serving the local folder 'public' under '/public'`,
'AppBootstrap',
);
logger.log(`Serving the local folder 'public' under '/public'`, 'AppBootstrap');
app.useStaticAssets('public', {
prefix: '/public/',
});
+2 -6
View File
@@ -21,10 +21,7 @@ import appConfig, { AppConfig } from './config/app.config';
import authConfig from './config/auth.config';
import cspConfig from './config/csp.config';
import customizationConfig from './config/customization.config';
import databaseConfig, {
getKnexConfig,
PostgresDatabaseConfig,
} from './config/database.config';
import databaseConfig, { getKnexConfig, PostgresDatabaseConfig } from './config/database.config';
import externalConfig from './config/external-services.config';
import { Loglevel } from './config/loglevel.enum';
import mediaConfig from './config/media.config';
@@ -89,8 +86,7 @@ const routes: Routes = [
},
debug:
isDevMode() &&
(appConfig.log.level === Loglevel.DEBUG ||
appConfig.log.level === Loglevel.TRACE),
(appConfig.log.level === Loglevel.DEBUG || appConfig.log.level === Loglevel.TRACE),
},
}),
}),
+39 -63
View File
@@ -13,10 +13,7 @@ import {
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import LdapAuth from 'ldapauth-fork';
import authConfiguration, {
AuthConfig,
LdapConfig,
} from '../../config/auth.config';
import authConfiguration, { AuthConfig, LdapConfig } from '../../config/auth.config';
import { PendingLdapUserInfoDto } from '../../dtos/pending-ldap-user-info.dto';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
@@ -78,44 +75,40 @@ export class LdapService {
});
// oxlint-disable-next-line @typescript-eslint/no-empty-function
auth.on('error', () => {}); // Ignore further errors
auth.authenticate(
username,
password,
(error, userInfo: Record<string, string>) => {
auth.close(() => {
// We don't care about the closing
});
if (error) {
const exception = this.getLdapException(username, error);
return reject(exception);
}
auth.authenticate(username, password, (error, userInfo: Record<string, string>) => {
auth.close(() => {
// We don't care about the closing
});
if (error) {
const exception = this.getLdapException(username, error);
return reject(exception);
}
if (!userInfo) {
return reject(new UnauthorizedException(LDAP_ERROR_MAP['default']));
}
if (!userInfo) {
return reject(new UnauthorizedException(LDAP_ERROR_MAP['default']));
}
let email: string | null = null;
if (userInfo['mail']) {
if (Array.isArray(userInfo['mail'])) {
email = userInfo['mail'][0] as string;
} else {
email = userInfo['mail'];
}
let email: string | null = null;
if (userInfo['mail']) {
if (Array.isArray(userInfo['mail'])) {
email = userInfo['mail'][0] as string;
} else {
email = userInfo['mail'];
}
}
return resolve(
PendingLdapUserInfoDto.create({
email,
username: username,
id: userInfo[ldapConfig.userIdField],
displayName: userInfo[ldapConfig.displayNameField] ?? username,
photoUrl: null,
// TODO LDAP stores images as binaries, we need to upload them to the media backend
// https://github.com/hedgedoc/hedgedoc/issues/5032
}),
);
},
);
return resolve(
PendingLdapUserInfoDto.create({
email,
username: username,
id: userInfo[ldapConfig.userIdField],
displayName: userInfo[ldapConfig.displayNameField] ?? username,
photoUrl: null,
// TODO LDAP stores images as binaries, we need to upload them to the media backend
// https://github.com/hedgedoc/hedgedoc/issues/5032
}),
);
});
});
}
@@ -127,16 +120,10 @@ export class LdapService {
* @throws NotFoundException if there is no LDAP config with the given identifier
*/
getLdapConfig(ldapIdentifier: string): LdapConfig {
const ldapConfig = this.authConfig.ldap.find(
(config) => config.identifier === ldapIdentifier,
);
const ldapConfig = this.authConfig.ldap.find((config) => config.identifier === ldapIdentifier);
if (!ldapConfig) {
this.logger.warn(
`The LDAP config '${ldapIdentifier}' was requested, but doesn't exist`,
);
throw new NotFoundException(
`There is no LDAP config '${ldapIdentifier}'`,
);
this.logger.warn(`The LDAP config '${ldapIdentifier}' was requested, but doesn't exist`);
throw new NotFoundException(`There is no LDAP config '${ldapIdentifier}'`);
}
return ldapConfig;
}
@@ -149,22 +136,16 @@ export class LdapService {
* @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,
error: Error | string,
): HttpException {
private getLdapException(username: string, error: Error | string): HttpException {
// Invalid credentials / user not found are not errors but login failures
let message = '';
if (typeof error === 'object') {
switch (error.name) {
case 'InvalidCredentialsError': {
message = 'Invalid username/password';
const ldapComment = error.message.match(
/data ([\da-fA-F]*), v[\da-fA-F]*/,
);
const ldapComment = error.message.match(/data ([\da-fA-F]*), v[\da-fA-F]*/);
if (ldapComment && ldapComment[1]) {
message =
LDAP_ERROR_MAP[ldapComment[1]] || LDAP_ERROR_MAP['default'];
message = LDAP_ERROR_MAP[ldapComment[1]] || LDAP_ERROR_MAP['default'];
}
break;
}
@@ -179,13 +160,8 @@ export class LdapService {
break;
}
}
if (
message !== '' ||
(typeof error === 'string' && error.startsWith('no such user:'))
) {
this.logger.log(
`User with username '${username}' could not log in. Reason: ${message}`,
);
if (message !== '' || (typeof error === 'string' && error.startsWith('no such user:'))) {
this.logger.log(`User with username '${username}' could not log in. Reason: ${message}`);
return new UnauthorizedException(message);
}
+10 -28
View File
@@ -6,16 +6,8 @@
import { AuthProviderType } from '@hedgedoc/commons';
import { FieldNameIdentity, Identity, TableIdentity } from '@hedgedoc/database';
import { Inject, Injectable } from '@nestjs/common';
import {
OptionsGraph,
OptionsType,
zxcvbnAsync,
zxcvbnOptions,
} from '@zxcvbn-ts/core';
import {
adjacencyGraphs,
dictionary as zxcvbnCommonDictionary,
} from '@zxcvbn-ts/language-common';
import { OptionsGraph, OptionsType, zxcvbnAsync, zxcvbnOptions } from '@zxcvbn-ts/core';
import { adjacencyGraphs, dictionary as zxcvbnCommonDictionary } from '@zxcvbn-ts/language-common';
import {
dictionary as zxcvbnEnDictionary,
translations as zxcvbnEnTranslations,
@@ -24,10 +16,7 @@ import { Knex } from 'knex';
import { InjectConnection } from 'nest-knexjs';
import authConfiguration, { AuthConfig } from '../../config/auth.config';
import {
InvalidCredentialsError,
PasswordTooWeakError,
} from '../../errors/errors';
import { InvalidCredentialsError, PasswordTooWeakError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { dateTimeToDB, getCurrentDateTime } from '../../utils/datetime';
import { checkPassword, hashPassword } from '../../utils/password';
@@ -91,10 +80,7 @@ export class LocalService {
* @throws NoLocalIdentityError if the specified user has no local identity
* @throws PasswordTooWeakError if the password is too weak
*/
async updateLocalPassword(
userId: number,
newPassword: string,
): Promise<void> {
async updateLocalPassword(userId: number, newPassword: string): Promise<void> {
await this.checkPasswordStrength(newPassword);
const newPasswordHash = await hashPassword(newPassword);
await this.knex(TableIdentity)
@@ -114,16 +100,12 @@ export class LocalService {
* @returns The identity of the user if the credentials are valid
* @throws InvalidCredentialsError if the credentials are invalid
*/
async checkLocalPassword(
username: string,
password: string,
): Promise<Identity> {
const identity =
await this.identityService.getIdentityFromUserIdAndProviderType(
username,
AuthProviderType.LOCAL,
null,
);
async checkLocalPassword(username: string, password: string): Promise<Identity> {
const identity = await this.identityService.getIdentityFromUserIdAndProviderType(
username,
AuthProviderType.LOCAL,
null,
);
const passwordValid = await checkPassword(
password,
identity[FieldNameIdentity.passwordHash] ?? '',
+8 -27
View File
@@ -17,10 +17,7 @@ import { Client, generators, Issuer, UserinfoResponse } from 'openid-client';
import { RequestWithSession } from '../../api/utils/request.type';
import appConfiguration, { AppConfig } from '../../config/app.config';
import authConfiguration, {
AuthConfig,
OidcConfig,
} from '../../config/auth.config';
import authConfiguration, { AuthConfig, OidcConfig } from '../../config/auth.config';
import { PendingUserInfoDto } from '../../dtos/pending-user-info.dto';
import { NotInDBError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
@@ -75,9 +72,7 @@ export class OidcService {
* @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> {
private async fetchClientConfig(oidcConfig: OidcConfig): Promise<OidcClientConfigEntry> {
const useAutodiscover =
oidcConfig.authorizeUrl === undefined ||
oidcConfig.tokenUrl === undefined ||
@@ -143,16 +138,10 @@ export class OidcService {
* @param state The state generated for the login
* @returns The generated authorization URL
*/
getAuthorizationUrl(
oidcIdentifier: string,
code: string,
state: string,
): string {
getAuthorizationUrl(oidcIdentifier: string, code: string, state: string): string {
const clientConfig = this.clientConfigs.get(oidcIdentifier);
if (!clientConfig) {
throw new NotFoundException(
'OIDC configuration not found or initialized',
);
throw new NotFoundException('OIDC configuration not found or initialized');
}
const client = clientConfig.client;
return client.authorizationUrl({
@@ -179,9 +168,7 @@ export class OidcService {
): Promise<PendingUserInfoDto> {
const clientConfig = this.clientConfigs.get(oidcIdentifier);
if (!clientConfig) {
throw new NotFoundException(
'OIDC configuration not found or initialized',
);
throw new NotFoundException('OIDC configuration not found or initialized');
}
const client = clientConfig.client;
const oidcConfig = clientConfig.config;
@@ -255,9 +242,7 @@ export class OidcService {
): Promise<Identity | null> {
const clientConfig = this.clientConfigs.get(oidcIdentifier);
if (!clientConfig) {
throw new NotFoundException(
'OIDC configuration not found or initialized',
);
throw new NotFoundException('OIDC configuration not found or initialized');
}
try {
return await this.identityService.getIdentityFromUserIdAndProviderType(
@@ -269,9 +254,7 @@ export class OidcService {
// 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(
'Registration is disabled for this OIDC provider',
);
throw new ForbiddenException('Registration is disabled for this OIDC provider');
}
return null;
} else {
@@ -293,9 +276,7 @@ export class OidcService {
}
const clientConfig = this.clientConfigs.get(oidcIdentifier);
if (!clientConfig) {
throw new InternalServerErrorException(
'OIDC configuration not found or initialized',
);
throw new InternalServerErrorException('OIDC configuration not found or initialized');
}
const issuer = clientConfig.issuer;
const endSessionEndpoint = issuer.metadata.end_session_endpoint;
+1 -6
View File
@@ -3,12 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { CompleteRequest } from '../api/utils/request.type';
import { ConsoleLoggerService } from '../logger/console-logger.service';
+1 -3
View File
@@ -175,9 +175,7 @@ describe('appConfig', () => {
},
);
appConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
"HD_BASE_URL: Can't parse as URL",
);
expect(spyConsoleError.mock.calls[0][0]).toContain("HD_BASE_URL: Can't parse as URL");
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
+5 -25
View File
@@ -3,24 +3,13 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoSubdirectoryAllowedError,
parseUrl,
WrongProtocolError,
} from '@hedgedoc/commons';
import { NoSubdirectoryAllowedError, parseUrl, WrongProtocolError } from '@hedgedoc/commons';
import { registerAs } from '@nestjs/config';
import z, { RefinementCtx } from 'zod';
import { Loglevel } from './loglevel.enum';
import {
parseOptionalBoolean,
parseOptionalNumber,
printConfigErrorAndExit,
} from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { parseOptionalBoolean, parseOptionalNumber, printConfigErrorAndExit } from './utils';
import { buildErrorMessage, 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
@@ -71,22 +60,13 @@ const schema = z
.superRefine(validateUrl)
.default('')
.describe('HD_RENDERER_BASE_URL'),
backendPort: z
.number()
.positive()
.int()
.max(65535)
.default(3000)
.describe('HD_BACKEND_PORT'),
backendPort: z.number().positive().int().max(65535).default(3000).describe('HD_BACKEND_PORT'),
log: z.object({
level: z
.enum(Object.values(Loglevel) as [Loglevel, ...Loglevel[]])
.default(Loglevel.INFO)
.describe('HD_LOG_LEVEL'),
showTimestamp: z
.boolean()
.default(true)
.describe('HD_LOG_SHOW_TIMESTAMP'),
showTimestamp: z.boolean().default(true).describe('HD_LOG_SHOW_TIMESTAMP'),
}),
})
.transform((data) => {
+6 -18
View File
@@ -53,9 +53,7 @@ describe('authConfig', () => {
const config = authConfig();
expect(config.local.enableLogin).toEqual(enableLogin);
expect(config.local.enableRegister).toEqual(enableRegister);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
expect(config.local.minimalPasswordStrength).toEqual(minimalPasswordStrength);
restore();
});
@@ -75,9 +73,7 @@ describe('authConfig', () => {
const config = authConfig();
expect(config.local.enableLogin).toEqual(false);
expect(config.local.enableRegister).toEqual(enableRegister);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
expect(config.local.minimalPasswordStrength).toEqual(minimalPasswordStrength);
restore();
});
@@ -97,9 +93,7 @@ describe('authConfig', () => {
const config = authConfig();
expect(config.local.enableLogin).toEqual(enableLogin);
expect(config.local.enableRegister).toEqual(false);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
expect(config.local.minimalPasswordStrength).toEqual(minimalPasswordStrength);
restore();
});
@@ -542,9 +536,7 @@ describe('authConfig', () => {
},
);
authConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
'HD_AUTH_LDAP_FUTURAMA_URL: Required',
);
expect(spyConsoleError.mock.calls[0][0]).toContain('HD_AUTH_LDAP_FUTURAMA_URL: Required');
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
@@ -980,9 +972,7 @@ describe('authConfig', () => {
expect(firstOidc.userIdField).toEqual(userIdField);
expect(firstOidc.usernameField).toEqual(userNameField);
expect(firstOidc.displayNameField).toEqual(displayNameField);
expect(firstOidc.profilePictureField).toEqual(
defaultProfilePictureField,
);
expect(firstOidc.profilePictureField).toEqual(defaultProfilePictureField);
expect(firstOidc.emailField).toEqual(emailField);
expect(firstOidc.enableRegistration).toEqual(false);
restore();
@@ -1092,9 +1082,7 @@ describe('authConfig', () => {
},
);
authConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
'HD_AUTH_OIDC_GITLAB_ISSUER: Required',
);
expect(spyConsoleError.mock.calls[0][0]).toContain('HD_AUTH_OIDC_GITLAB_ISSUER: Required');
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
+35 -117
View File
@@ -15,45 +15,24 @@ import {
printConfigErrorAndExit,
toArrayConfig,
} from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const ldapSchema = z
.object({
identifier: z.string().describe('HD_AUTH_LDAP_SERVERS'),
providerName: z
.string()
.default('LDAP')
.describe('HD_AUTH_LDAP_*_PROVIDER_NAME'),
providerName: z.string().default('LDAP').describe('HD_AUTH_LDAP_*_PROVIDER_NAME'),
url: z.string().describe('HD_AUTH_LDAP_*_URL'),
bindDn: z.string().optional().describe('HD_AUTH_LDAP_*_BIND_DN'),
bindCredentials: z
.string()
.optional()
.describe('HD_AUTH_LDAP_*_BIND_CREDENTIALS'),
bindCredentials: z.string().optional().describe('HD_AUTH_LDAP_*_BIND_CREDENTIALS'),
searchBase: z.string().describe('HD_AUTH_LDAP_*_SEARCH_BASE'),
searchFilter: z
.string()
.default('(uid={{username}})')
.describe('HD_AUTH_LDAP_*_SEARCH_FILTER'),
searchAttributes: z
.array(z.string())
.optional()
.describe('HD_AUTH_LDAP_*_SEARCH_ATTRIBUTES'),
userIdField: z
.string()
.default('uid')
.describe('HD_AUTH_LDAP_*_USER_ID_FIELD'),
searchFilter: z.string().default('(uid={{username}})').describe('HD_AUTH_LDAP_*_SEARCH_FILTER'),
searchAttributes: z.array(z.string()).optional().describe('HD_AUTH_LDAP_*_SEARCH_ATTRIBUTES'),
userIdField: z.string().default('uid').describe('HD_AUTH_LDAP_*_USER_ID_FIELD'),
displayNameField: z
.string()
.default('displayName')
.describe('HD_AUTH_LDAP_*_DISPLAY_NAME_FIELD'),
emailField: z
.string()
.default('mail')
.describe('HD_AUTH_LDAP_*_EMAIL_FIELD'),
emailField: z.string().default('mail').describe('HD_AUTH_LDAP_*_EMAIL_FIELD'),
profilePictureField: z
.string()
.default('jpegPhoto')
@@ -91,16 +70,14 @@ const ldapSchema = z
if (tlsMin && tlsMax && tlsMin > tlsMax) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'TLS min version must be less than or equal to TLS max version',
message: 'TLS min version must be less than or equal to TLS max version',
fatal: true,
});
}
if ((tlsMin && tlsMin < '1.2') || (tlsMax && tlsMax < '1.2')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'For security reasons, consider using TLS version 1.2 or higher',
message: 'For security reasons, consider using TLS version 1.2 or higher',
fatal: false,
});
}
@@ -108,83 +85,38 @@ const ldapSchema = z
const oidcSchema = z.object({
identifier: z.string().describe('HD_AUTH_OIDC_SERVERS'),
providerName: z
.string()
.default('OpenID Connect')
.describe('HD_AUTH_OIDC_*_PROVIDER_NAME'),
providerName: z.string().default('OpenID Connect').describe('HD_AUTH_OIDC_*_PROVIDER_NAME'),
issuer: z.string().url().describe('HD_AUTH_OIDC_*_ISSUER'),
clientId: z.string().describe('HD_AUTH_OIDC_*_CLIENT_ID'),
clientSecret: z.string().describe('HD_AUTH_OIDC_*_CLIENT_SECRET'),
theme: z.nativeEnum(Theme).optional().describe('HD_AUTH_OIDC_*_THEME'),
authorizeUrl: z
.string()
.url()
.optional()
.describe('HD_AUTH_OIDC_*_AUTHORIZE_URL'),
authorizeUrl: z.string().url().optional().describe('HD_AUTH_OIDC_*_AUTHORIZE_URL'),
tokenUrl: z.string().url().optional().describe('HD_AUTH_OIDC_*_TOKEN_URL'),
userinfoUrl: z
.string()
.url()
.optional()
.describe('HD_AUTH_OIDC_*_USERINFO_URL'),
endSessionUrl: z
.string()
.url()
.optional()
.describe('HD_AUTH_OIDC_*_END_SESSION_URL'),
scope: z
.string()
.default('openid profile email')
.describe('HD_AUTH_OIDC_*_SCOPE'),
usernameField: z
.string()
.default('preferred_username')
.describe('HD_AUTH_OIDC_*_USERNAME_FIELD'),
userIdField: z
.string()
.default('sub')
.describe('HD_AUTH_OIDC_*_USER_ID_FIELD'),
displayNameField: z
.string()
.default('name')
.describe('HD_AUTH_OIDC_*_DISPLAY_NAME_FIELD'),
userinfoUrl: z.string().url().optional().describe('HD_AUTH_OIDC_*_USERINFO_URL'),
endSessionUrl: z.string().url().optional().describe('HD_AUTH_OIDC_*_END_SESSION_URL'),
scope: z.string().default('openid profile email').describe('HD_AUTH_OIDC_*_SCOPE'),
usernameField: z.string().default('preferred_username').describe('HD_AUTH_OIDC_*_USERNAME_FIELD'),
userIdField: z.string().default('sub').describe('HD_AUTH_OIDC_*_USER_ID_FIELD'),
displayNameField: z.string().default('name').describe('HD_AUTH_OIDC_*_DISPLAY_NAME_FIELD'),
profilePictureField: z
.string()
.default('picture')
.describe('HD_AUTH_OIDC_*_PROFILE_PICTURE_FIELD'),
emailField: z
.string()
.default('email')
.describe('HD_AUTH_OIDC_*_EMAIL_FIELD'),
enableRegistration: z
.boolean()
.default(true)
.describe('HD_AUTH_OIDC_*_ENABLE_REGISTRATION'),
emailField: z.string().default('email').describe('HD_AUTH_OIDC_*_EMAIL_FIELD'),
enableRegistration: z.boolean().default(true).describe('HD_AUTH_OIDC_*_ENABLE_REGISTRATION'),
});
const schema = z.object({
allowProfileEdits: z
.boolean()
.default(true)
.describe('HD_AUTH_ALLOW_PROFILE_EDITS'),
allowChooseUsername: z
.boolean()
.default(true)
.describe('HD_AUTH_ALLOW_CHOOSE_USERNAME'),
allowProfileEdits: z.boolean().default(true).describe('HD_AUTH_ALLOW_PROFILE_EDITS'),
allowChooseUsername: z.boolean().default(true).describe('HD_AUTH_ALLOW_CHOOSE_USERNAME'),
syncSource: z.string().optional().describe('HD_AUTH_SYNC_SOURCE'),
session: z.object({
secret: z.string().describe('HD_AUTH_SESSION_SECRET'),
lifetime: z.number().default(1209600).describe('HD_AUTH_SESSION_LIFETIME'), // 14 * 24 * 60 * 60s = 14 days
}),
local: z.object({
enableLogin: z
.boolean()
.default(false)
.describe('HD_AUTH_LOCAL_ENABLE_LOGIN'),
enableRegister: z
.boolean()
.default(false)
.describe('HD_AUTH_LOCAL_ENABLE_REGISTER'),
enableLogin: z.boolean().default(false).describe('HD_AUTH_LOCAL_ENABLE_LOGIN'),
enableRegister: z.boolean().default(false).describe('HD_AUTH_LOCAL_ENABLE_REGISTER'),
minimalPasswordStrength: z.coerce
.number()
.min(0)
@@ -201,21 +133,18 @@ export type LdapConfig = z.infer<typeof ldapSchema>;
export type OidcConfig = z.infer<typeof oidcSchema>;
export default registerAs('authConfig', () => {
const ldapServers = (process.env.HD_AUTH_LDAP_SERVERS?.split(',') ?? []).map(
(name) => name.toUpperCase(),
const ldapServers = (process.env.HD_AUTH_LDAP_SERVERS?.split(',') ?? []).map((name) =>
name.toUpperCase(),
);
ensureNoDuplicatesExist('LDAP', ldapServers);
const oidcServers = (process.env.HD_AUTH_OIDC_SERVERS?.split(',') ?? []).map(
(name) => name.toUpperCase(),
const oidcServers = (process.env.HD_AUTH_OIDC_SERVERS?.split(',') ?? []).map((name) =>
name.toUpperCase(),
);
ensureNoDuplicatesExist('OIDC', oidcServers);
const ldapConfig: Partial<LdapConfig>[] = ldapServers.map((name) => {
const caFiles = toArrayConfig(
process.env[`HD_AUTH_LDAP_${name}_TLS_CERT_PATHS`],
',',
);
const caFiles = toArrayConfig(process.env[`HD_AUTH_LDAP_${name}_TLS_CERT_PATHS`], ',');
let tlsCaCerts = undefined;
if (caFiles) {
tlsCaCerts = caFiles.map((fileName) => {
@@ -232,13 +161,11 @@ export default registerAs('authConfig', () => {
bindCredentials: process.env[`HD_AUTH_LDAP_${name}_BIND_CREDENTIALS`],
searchBase: process.env[`HD_AUTH_LDAP_${name}_SEARCH_BASE`],
searchFilter: process.env[`HD_AUTH_LDAP_${name}_SEARCH_FILTER`],
searchAttributes:
process.env[`HD_AUTH_LDAP_${name}_SEARCH_ATTRIBUTES`]?.split(','),
searchAttributes: process.env[`HD_AUTH_LDAP_${name}_SEARCH_ATTRIBUTES`]?.split(','),
userIdField: process.env[`HD_AUTH_LDAP_${name}_USER_ID_FIELD`],
displayNameField: process.env[`HD_AUTH_LDAP_${name}_DISPLAY_NAME_FIELD`],
emailField: process.env[`HD_AUTH_LDAP_${name}_EMAIL_FIELD`],
profilePictureField:
process.env[`HD_AUTH_LDAP_${name}_PROFILE_PICTURE_FIELD`],
profilePictureField: process.env[`HD_AUTH_LDAP_${name}_PROFILE_PICTURE_FIELD`],
// Technically this can be (string | undefined)[] | undefined, but an undefined array element tells us that the file is not there and the user input is invalid
tlsCaCerts: tlsCaCerts as string[] | undefined,
tlsRejectUnauthorized: parseOptionalBoolean(
@@ -251,9 +178,7 @@ export default registerAs('authConfig', () => {
tlsMinVersion: process.env[`HD_AUTH_LDAP_${name}_TLS_MIN_VERSION`] as
| 'TLSv1' // This typecast is required since zod validates the input later but TypeScript already expects valid input
| undefined,
tlsMaxVersion: process.env[`HD_AUTH_LDAP_${name}_TLS_MAX_VERSION`] as
| 'TLSv1'
| undefined,
tlsMaxVersion: process.env[`HD_AUTH_LDAP_${name}_TLS_MAX_VERSION`] as 'TLSv1' | undefined,
};
});
@@ -272,8 +197,7 @@ export default registerAs('authConfig', () => {
userIdField: process.env[`HD_AUTH_OIDC_${name}_USER_ID_FIELD`],
userNameField: process.env[`HD_AUTH_OIDC_${name}_USER_NAME_FIELD`],
displayNameField: process.env[`HD_AUTH_OIDC_${name}_DISPLAY_NAME_FIELD`],
profilePictureField:
process.env[`HD_AUTH_OIDC_${name}_PROFILE_PICTURE_FIELD`],
profilePictureField: process.env[`HD_AUTH_OIDC_${name}_PROFILE_PICTURE_FIELD`],
emailField: process.env[`HD_AUTH_OIDC_${name}_EMAIL_FIELD`],
enableRegistration: parseOptionalBoolean(
process.env[`HD_AUTH_OIDC_${name}_ENABLE_REGISTRATION`],
@@ -281,12 +205,8 @@ export default registerAs('authConfig', () => {
}));
const authConfig = schema.safeParse({
allowProfileEdits: parseOptionalBoolean(
process.env.HD_AUTH_ALLOW_PROFILE_EDITS,
),
allowChooseUsername: parseOptionalBoolean(
process.env.HD_AUTH_ALLOW_CHOOSE_USERNAME,
),
allowProfileEdits: parseOptionalBoolean(process.env.HD_AUTH_ALLOW_PROFILE_EDITS),
allowChooseUsername: parseOptionalBoolean(process.env.HD_AUTH_ALLOW_CHOOSE_USERNAME),
syncSource: process.env.HD_AUTH_SYNC_SOURCE?.toLowerCase(),
session: {
secret: process.env.HD_AUTH_SESSION_SECRET,
@@ -294,9 +214,7 @@ export default registerAs('authConfig', () => {
},
local: {
enableLogin: parseOptionalBoolean(process.env.HD_AUTH_LOCAL_ENABLE_LOGIN),
enableRegister: parseOptionalBoolean(
process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
),
enableRegister: parseOptionalBoolean(process.env.HD_AUTH_LOCAL_ENABLE_REGISTER),
minimalPasswordStrength: parseOptionalNumber(
process.env.HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH,
),
+3 -11
View File
@@ -8,10 +8,7 @@ import * as process from 'node:process';
import z from 'zod';
import { parseOptionalBoolean, printConfigErrorAndExit } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const cspSchema = z.object({
enable: z.boolean().default(true).describe('HD_CSP_ENABLED'),
@@ -21,13 +18,8 @@ const cspSchema = z.object({
export type CspConfig = z.infer<typeof cspSchema>;
export default registerAs('cspConfig', () => {
if (
process.env.HD_CSP_ENABLE !== undefined ||
process.env.HD_CSP_REPORT_URI !== undefined
) {
throw new Error(
"CSP config is currently not yet supported. Please don't configure it",
);
if (process.env.HD_CSP_ENABLE !== undefined || process.env.HD_CSP_REPORT_URI !== undefined) {
throw new Error("CSP config is currently not yet supported. Please don't configure it");
}
const cspConfig = cspSchema.safeParse({
@@ -78,18 +78,10 @@ describe('customizationConfig', () => {
},
);
customizationConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
'- HD_BRANDING_CUSTOM_LOGO: Invalid url',
);
expect(spyConsoleError.mock.calls[0][0]).toContain(
'- HD_URLS_PRIVACY: Invalid url',
);
expect(spyConsoleError.mock.calls[0][0]).toContain(
'- HD_URLS_TERMS_OF_USE: Invalid url',
);
expect(spyConsoleError.mock.calls[0][0]).toContain(
'- HD_URLS_IMPRINT: Invalid url',
);
expect(spyConsoleError.mock.calls[0][0]).toContain('- HD_BRANDING_CUSTOM_LOGO: Invalid url');
expect(spyConsoleError.mock.calls[0][0]).toContain('- HD_URLS_PRIVACY: Invalid url');
expect(spyConsoleError.mock.calls[0][0]).toContain('- HD_URLS_TERMS_OF_USE: Invalid url');
expect(spyConsoleError.mock.calls[0][0]).toContain('- HD_URLS_IMPRINT: Invalid url');
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
+2 -9
View File
@@ -7,19 +7,12 @@ import { registerAs } from '@nestjs/config';
import z from 'zod';
import { printConfigErrorAndExit } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const schema = z.object({
branding: z.object({
customName: z.string().or(z.null()).describe('HD_BRANDING_CUSTOM_NAME'),
customLogo: z
.string()
.url()
.or(z.null())
.describe('HD_BRANDING_CUSTOM_LOGO'),
customLogo: z.string().url().or(z.null()).describe('HD_BRANDING_CUSTOM_LOGO'),
}),
urls: z.object({
privacy: z.string().url().or(z.null()).describe('HD_URLS_PRIVACY'),
+6 -29
View File
@@ -10,10 +10,7 @@ import z from 'zod';
import { DatabaseType } from './database-type.enum';
import { parseOptionalNumber, printConfigErrorAndExit } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const sqliteDbSchema = z.object({
type: z.literal(DatabaseType.SQLITE).describe('HD_DATABASE_TYPE'),
@@ -26,12 +23,7 @@ const postgresDbSchema = z.object({
username: z.string().describe('HD_DATABASE_USERNAME'),
password: z.string().describe('HD_DATABASE_PASSWORD'),
host: z.string().describe('HD_DATABASE_HOST'),
port: z
.number()
.positive()
.max(65535)
.default(5432)
.describe('HD_DATABASE_PORT'),
port: z.number().positive().max(65535).default(5432).describe('HD_DATABASE_PORT'),
});
const mariaDbSchema = z.object({
@@ -40,19 +32,10 @@ const mariaDbSchema = z.object({
username: z.string().describe('HD_DATABASE_USERNAME'),
password: z.string().describe('HD_DATABASE_PASSWORD'),
host: z.string().describe('HD_DATABASE_HOST'),
port: z
.number()
.positive()
.max(65535)
.default(3306)
.describe('HD_DATABASE_PORT'),
port: z.number().positive().max(65535).default(3306).describe('HD_DATABASE_PORT'),
});
const dbSchema = z.discriminatedUnion('type', [
sqliteDbSchema,
mariaDbSchema,
postgresDbSchema,
]);
const dbSchema = z.discriminatedUnion('type', [sqliteDbSchema, mariaDbSchema, postgresDbSchema]);
export type SqliteDatabaseConfig = z.infer<typeof sqliteDbSchema>;
export type PostgresDatabaseConfig = z.infer<typeof postgresDbSchema>;
@@ -91,14 +74,8 @@ export function getKnexConfig(databaseConfig: DatabaseConfig): Knex.Config {
case DatabaseType.POSTGRES:
// If we don't set the type parsers for TIMESTAMP and TIMESTAMPTZ, pg would return JSDate objects here
// This is not what we want, so we set them to the string representation of the timestamp
pgTypes.setTypeParser(
pgTypes.builtins.TIMESTAMP,
(value: string) => value,
);
pgTypes.setTypeParser(
pgTypes.builtins.TIMESTAMPTZ,
(value: string) => value,
);
pgTypes.setTypeParser(pgTypes.builtins.TIMESTAMP, (value: string) => value);
pgTypes.setTypeParser(pgTypes.builtins.TIMESTAMPTZ, (value: string) => value);
return {
client: 'pg',
connection: {
@@ -60,9 +60,7 @@ describe('externalServices', () => {
},
);
externalServicesConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
'HD_PLANTUML_SERVER: Invalid url',
);
expect(spyConsoleError.mock.calls[0][0]).toContain('HD_PLANTUML_SERVER: Invalid url');
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
@@ -7,10 +7,7 @@ import { registerAs } from '@nestjs/config';
import z from 'zod';
import { printConfigErrorAndExit } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const schema = z.object({
plantumlServer: z.string().url().or(z.null()).describe('HD_PLANTUML_SERVER'),
@@ -21,9 +18,7 @@ export type ExternalServicesConfig = z.infer<typeof schema>;
export default registerAs('externalServicesConfig', () => {
if (process.env.HD_IMAGE_PROXY !== undefined) {
throw new Error(
"HD_IMAGE_PROXY is currently not yet supported. Please don't configure it",
);
throw new Error("HD_IMAGE_PROXY is currently not yet supported. Please don't configure it");
}
const externalConfig = schema.safeParse({
plantumlServer: process.env.HD_PLANTUML_SERVER ?? null,
+3 -9
View File
@@ -96,9 +96,7 @@ describe('mediaConfig', () => {
);
const config = mediaConfig() as { backend: AzureMediaConfig };
expect(config.backend.type).toEqual(MediaBackendType.AZURE);
expect(config.backend.azure.connectionString).toEqual(
azureConnectionString,
);
expect(config.backend.azure.connectionString).toEqual(azureConnectionString);
expect(config.backend.azure.container).toEqual(container);
restore();
});
@@ -137,9 +135,7 @@ describe('mediaConfig', () => {
);
const config = mediaConfig() as { backend: WebdavMediaConfig };
expect(config.backend.type).toEqual(MediaBackendType.WEBDAV);
expect(config.backend.webdav.connectionString).toEqual(
webdavConnectionString,
);
expect(config.backend.webdav.connectionString).toEqual(webdavConnectionString);
expect(config.backend.webdav.uploadDir).toEqual(uploadDir);
expect(config.backend.webdav.publicUrl).toEqual(publicUrl);
restore();
@@ -245,9 +241,7 @@ describe('mediaConfig', () => {
},
);
mediaConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
'HD_MEDIA_BACKEND_S3_BUCKET: Required',
);
expect(spyConsoleError.mock.calls[0][0]).toContain('HD_MEDIA_BACKEND_S3_BUCKET: Required');
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
+6 -22
View File
@@ -8,17 +8,12 @@ import { registerAs } from '@nestjs/config';
import z from 'zod';
import { parseOptionalBoolean, printConfigErrorAndExit } from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const azureSchema = z.object({
type: z.literal(MediaBackendType.AZURE),
azure: z.object({
connectionString: z
.string()
.describe('HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING'),
connectionString: z.string().describe('HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING'),
container: z.string().describe('HD_MEDIA_BACKEND_AZURE_CONTAINER'),
}),
});
@@ -45,24 +40,15 @@ const s3Schema = z.object({
bucket: z.string().describe('HD_MEDIA_BACKEND_S3_BUCKET'),
endpoint: z.string().url().describe('HD_MEDIA_BACKEND_S3_ENDPOINT'),
region: z.string().optional().describe('HD_MEDIA_BACKEND_S3_REGION'),
pathStyle: z
.boolean()
.default(false)
.describe('HD_MEDIA_BACKEND_S3_PATH_STYLE'),
pathStyle: z.boolean().default(false).describe('HD_MEDIA_BACKEND_S3_PATH_STYLE'),
}),
});
const webdavSchema = z.object({
type: z.literal(MediaBackendType.WEBDAV),
webdav: z.object({
connectionString: z
.string()
.url()
.describe('HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING'),
uploadDir: z
.string()
.optional()
.describe('HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR'),
connectionString: z.string().url().describe('HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING'),
uploadDir: z.string().optional().describe('HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR'),
publicUrl: z.string().url().describe('HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL'),
}),
});
@@ -97,9 +83,7 @@ export default registerAs('mediaConfig', () => {
bucket: process.env.HD_MEDIA_BACKEND_S3_BUCKET,
endpoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
region: process.env.HD_MEDIA_BACKEND_S3_REGION,
pathStyle: parseOptionalBoolean(
process.env.HD_MEDIA_BACKEND_S3_PATH_STYLE,
),
pathStyle: parseOptionalBoolean(process.env.HD_MEDIA_BACKEND_S3_PATH_STYLE),
},
azure: {
connectionString: process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
@@ -24,14 +24,8 @@ export function createDefaultMockCustomizationConfig(): CustomizationConfig {
export function registerCustomizationConfig(
customizationConfig: CustomizationConfig,
): ConfigFactory<CustomizationConfig> &
ConfigFactoryKeyHost<CustomizationConfig> {
return registerAs(
'customizationConfig',
(): CustomizationConfig => customizationConfig,
);
): ConfigFactory<CustomizationConfig> & ConfigFactoryKeyHost<CustomizationConfig> {
return registerAs('customizationConfig', (): CustomizationConfig => customizationConfig);
}
export default registerCustomizationConfig(
createDefaultMockCustomizationConfig(),
);
export default registerCustomizationConfig(createDefaultMockCustomizationConfig());
@@ -11,8 +11,7 @@ import { DatabaseConfig } from '../database.config';
export function createDefaultMockDatabaseConfig(): DatabaseConfig {
return {
type: (process.env.HEDGEDOC_TEST_DB_TYPE ||
DatabaseType.SQLITE) as DatabaseType,
type: (process.env.HEDGEDOC_TEST_DB_TYPE || DatabaseType.SQLITE) as DatabaseType,
name: 'hedgedoc',
password: 'hedgedoc',
host: 'localhost',
@@ -17,14 +17,8 @@ export function createDefaultMockExternalServicesConfig(): ExternalServicesConfi
export function registerExternalServiceConfig(
externalServicesConfig: ExternalServicesConfig,
): ConfigFactory<ExternalServicesConfig> &
ConfigFactoryKeyHost<ExternalServicesConfig> {
return registerAs(
'externalServicesConfig',
(): ExternalServicesConfig => externalServicesConfig,
);
): ConfigFactory<ExternalServicesConfig> & ConfigFactoryKeyHost<ExternalServicesConfig> {
return registerAs('externalServicesConfig', (): ExternalServicesConfig => externalServicesConfig);
}
export default registerExternalServiceConfig(
createDefaultMockExternalServicesConfig(),
);
export default registerExternalServiceConfig(createDefaultMockExternalServicesConfig());
+1 -2
View File
@@ -14,8 +14,7 @@ export function createDefaultMockMediaConfig(): MediaConfig {
backend: {
type: MediaBackendType.FILESYSTEM,
filesystem: {
uploadPath:
'test_uploads' + Math.floor(Math.random() * 100000).toString(),
uploadPath: 'test_uploads' + Math.floor(Math.random() * 100000).toString(),
},
},
};
+105 -202
View File
@@ -29,12 +29,9 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_PUBLICLY_VISIBLE:
publiclyVisible.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_PUBLICLY_VISIBLE: publiclyVisible.toString(),
HD_NOTE_REVISION_RETENTION_DAYS: retentionDays.toString(),
HD_NOTE_PERSIST_INTERVAL: persistInteval.toString(),
/* oxlint-enable @typescript-eslint/naming-convention */
@@ -47,15 +44,9 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.publiclyVisible).toEqual(
publiclyVisible,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(publiclyVisible);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
expect(config.revisionRetentionDays).toEqual(retentionDays);
expect(config.persistInterval).toEqual(persistInteval);
@@ -67,10 +58,8 @@ describe('noteConfig', () => {
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -80,12 +69,8 @@ describe('noteConfig', () => {
const config = noteConfig();
expect(config.forbiddenAliases).toHaveLength(0);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
restore();
@@ -97,10 +82,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAlias,
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -111,12 +94,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(1);
expect(config.forbiddenAliases[0]).toEqual(forbiddenAlias);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
restore();
@@ -127,10 +106,8 @@ describe('noteConfig', () => {
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -141,12 +118,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(100000);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
restore();
@@ -158,10 +131,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -185,8 +156,7 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -197,12 +167,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
restore();
@@ -214,8 +180,7 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -226,12 +191,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(PermissionLevel.FULL);
restore();
@@ -243,10 +204,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -257,12 +216,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
expect(config.revisionRetentionDays).toEqual(0);
@@ -275,10 +230,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.WRITE],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.WRITE],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -289,12 +242,8 @@ describe('noteConfig', () => {
expect(config.forbiddenAliases).toHaveLength(forbiddenAliases.length);
expect(config.forbiddenAliases).toEqual(forbiddenAliases);
expect(config.maxLength).toEqual(maxLength);
expect(config.permissions.default.everyone).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.loggedIn).toEqual(
PermissionLevel.WRITE,
);
expect(config.permissions.default.everyone).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.WRITE);
expect(config.permissions.default.publiclyVisible).toEqual(false);
expect(config.persistInterval).toEqual(10);
restore();
@@ -327,10 +276,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: invalidforbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -354,10 +301,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: negativeMaxDocumentLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -378,10 +323,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: floatMaxDocumentLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -402,10 +345,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: invalidMaxDocumentLength,
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -427,8 +368,7 @@ describe('noteConfig', () => {
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: wrongDefaultPermission,
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
@@ -449,8 +389,7 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: wrongDefaultPermission,
/* oxlint-enable @typescript-eslint/naming-convention */
},
@@ -472,10 +411,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: wrongDefaultPermission,
/* oxlint-enable @typescript-eslint/naming-convention */
},
@@ -497,10 +434,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: 'deny',
/* oxlint-enable @typescript-eslint/naming-convention */
},
@@ -518,18 +453,9 @@ describe('noteConfig', () => {
describe('more complex permissions', () => {
describe.each([
[
PermissionLevelNames[PermissionLevel.WRITE],
PermissionLevelNames[PermissionLevel.READ],
],
[
PermissionLevelNames[PermissionLevel.WRITE],
PermissionLevelNames[PermissionLevel.DENY],
],
[
PermissionLevelNames[PermissionLevel.READ],
PermissionLevelNames[PermissionLevel.DENY],
],
[PermissionLevelNames[PermissionLevel.WRITE], PermissionLevelNames[PermissionLevel.READ]],
[PermissionLevelNames[PermissionLevel.WRITE], PermissionLevelNames[PermissionLevel.DENY]],
[PermissionLevelNames[PermissionLevel.READ], PermissionLevelNames[PermissionLevel.DENY]],
])(
'check default everyone and logged-in interaction',
(everyonePermission, loggedInPermission) => {
@@ -556,76 +482,57 @@ describe('noteConfig', () => {
},
);
describe.each([
[
PermissionLevelNames[PermissionLevel.READ],
PermissionLevelNames[PermissionLevel.DENY],
],
])(
'check default everyone and max guest level full',
(defaultEveryone) => {
it(`when HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL is set to ${PermissionLevelNames[PermissionLevel.FULL]} and HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE is set to ${defaultEveryone}`, async () => {
const restore = mockedEnv(
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: defaultEveryone,
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL:
PermissionLevelNames[PermissionLevel.FULL],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
noteConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
`'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${PermissionLevelNames[PermissionLevel.FULL]}', but 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}'. This does not allow the guest users to write in the notes they can create.`,
);
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
},
);
[PermissionLevelNames[PermissionLevel.READ], PermissionLevelNames[PermissionLevel.DENY]],
])('check default everyone and max guest level full', (defaultEveryone) => {
it(`when HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL is set to ${PermissionLevelNames[PermissionLevel.FULL]} and HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE is set to ${defaultEveryone}`, async () => {
const restore = mockedEnv(
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: defaultEveryone,
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: PermissionLevelNames[PermissionLevel.FULL],
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
noteConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
`'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${PermissionLevelNames[PermissionLevel.FULL]}', but 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}'. This does not allow the guest users to write in the notes they can create.`,
);
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
});
describe.each([
[
PermissionLevelNames[PermissionLevel.WRITE],
PermissionLevelNames[PermissionLevel.READ],
],
[
PermissionLevelNames[PermissionLevel.WRITE],
PermissionLevelNames[PermissionLevel.DENY],
],
[
PermissionLevelNames[PermissionLevel.READ],
PermissionLevelNames[PermissionLevel.DENY],
],
])(
'check default everyone and max guest level full',
(defaultEveryone, maxGuestLevel) => {
it(`when 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}', but 'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${maxGuestLevel}'`, async () => {
const restore = mockedEnv(
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: defaultEveryone,
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: maxGuestLevel,
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
noteConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
`HD_NOTE: 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}', but 'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${maxGuestLevel}'. This does not work since the default level may not be higher than the maximum guest level.`,
);
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
},
);
[PermissionLevelNames[PermissionLevel.WRITE], PermissionLevelNames[PermissionLevel.READ]],
[PermissionLevelNames[PermissionLevel.WRITE], PermissionLevelNames[PermissionLevel.DENY]],
[PermissionLevelNames[PermissionLevel.READ], PermissionLevelNames[PermissionLevel.DENY]],
])('check default everyone and max guest level full', (defaultEveryone, maxGuestLevel) => {
it(`when 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}', but 'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${maxGuestLevel}'`, async () => {
const restore = mockedEnv(
{
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: defaultEveryone,
HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL: maxGuestLevel,
/* oxlint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
noteConfig();
expect(spyConsoleError.mock.calls[0][0]).toContain(
`HD_NOTE: 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${defaultEveryone}', but 'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${maxGuestLevel}'. This does not work since the default level may not be higher than the maximum guest level.`,
);
expect(spyProcessExit).toHaveBeenCalledWith(1);
restore();
});
});
});
it('when given a negative retention days', async () => {
@@ -634,10 +541,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_REVISION_RETENTION_DAYS: (-1).toString(),
/* oxlint-enable @typescript-eslint/naming-convention */
},
@@ -659,10 +564,8 @@ describe('noteConfig', () => {
/* oxlint-disable @typescript-eslint/naming-convention */
HD_NOTE_FORBIDDEN_ALIASES: forbiddenAliases.join(' , '),
HD_NOTE_MAX_LENGTH: maxLength.toString(),
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN: PermissionLevelNames[PermissionLevel.READ],
HD_NOTE_REVISION_RETENTION_DAYS: retentionDays.toString(),
HD_NOTE_PERSIST_INTERVAL: (-1).toString(),
/* oxlint-enable @typescript-eslint/naming-convention */
+4 -16
View File
@@ -3,11 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
PermissionLevel,
PermissionLevelNames,
PermissionLevelValues,
} from '@hedgedoc/commons';
import { PermissionLevel, PermissionLevelNames, PermissionLevelValues } from '@hedgedoc/commons';
import { registerAs } from '@nestjs/config';
import z from 'zod';
@@ -17,10 +13,7 @@ import {
printConfigErrorAndExit,
toArrayConfig,
} from './utils';
import {
buildErrorMessage,
extractDescriptionFromZodIssue,
} from './zod-error-message';
import { buildErrorMessage, extractDescriptionFromZodIssue } from './zod-error-message';
const schema = z
.object({
@@ -88,10 +81,7 @@ const schema = z
const defaultEveryone = config.permissions.default.everyone;
const defaultLoggedIn = config.permissions.default.loggedIn;
const maxGuestLevel = config.permissions.maxGuestLevel;
if (
maxGuestLevel === PermissionLevel.FULL &&
defaultEveryone !== PermissionLevel.WRITE
) {
if (maxGuestLevel === PermissionLevel.FULL && defaultEveryone !== PermissionLevel.WRITE) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `'HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL' is set to '${PermissionLevelNames[maxGuestLevel]}', but 'HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE' is set to '${PermissionLevelNames[defaultEveryone]}'. This does not allow the guest users to write in the notes they can create.`,
@@ -130,9 +120,7 @@ export default registerAs('noteConfig', () => {
),
},
},
revisionRetentionDays: parseOptionalNumber(
process.env.HD_NOTE_REVISION_RETENTION_DAYS,
),
revisionRetentionDays: parseOptionalNumber(process.env.HD_NOTE_REVISION_RETENTION_DAYS),
persistInterval: parseOptionalNumber(process.env.HD_NOTE_PERSIST_INTERVAL),
});
if (noteConfig.error) {
+4 -8
View File
@@ -42,9 +42,9 @@ describe('config utils', () => {
);
});
it('throws error if there are multiple duplicates', () => {
expect(() =>
ensureNoDuplicatesExist('Test', ['A', 'A', 'B', 'B']),
).toThrow("Your Test names 'A,A,B,B' contain duplicates: 'A,B'");
expect(() => ensureNoDuplicatesExist('Test', ['A', 'A', 'B', 'B'])).toThrow(
"Your Test names 'A,A,B,B' contain duplicates: 'A,B'",
);
});
});
describe('toArrayConfig', () => {
@@ -59,11 +59,7 @@ describe('config utils', () => {
expect(toArrayConfig('one, two, three')).toEqual(['one', 'two', 'three']);
});
it('non default seperator', () => {
expect(toArrayConfig('one ; two ; three', ';')).toEqual([
'one',
'two',
'three',
]);
expect(toArrayConfig('one ; two ; three', ';')).toEqual(['one', 'two', 'three']);
});
});
describe('needToLog', () => {
+5 -18
View File
@@ -13,9 +13,7 @@ import { Loglevel } from './loglevel.enum';
* @returns An array containing the duplicate items
*/
export function findDuplicatesInArray<T>(array: T[]): T[] {
return Array.from(
new Set(array.filter((item, index) => array.indexOf(item) !== index)),
);
return Array.from(new Set(array.filter((item, index) => array.indexOf(item) !== index)));
}
/**
@@ -26,16 +24,11 @@ export function findDuplicatesInArray<T>(array: T[]): T[] {
* @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[],
): void {
export function ensureNoDuplicatesExist(authName: string, names: string[]): void {
const duplicates = findDuplicatesInArray(names);
if (duplicates.length !== 0) {
throw new Error(
`Your ${authName} names '${names.join(
',',
)}' contain duplicates: '${duplicates.join(',')}'`,
`Your ${authName} names '${names.join(',')}' contain duplicates: '${duplicates.join(',')}'`,
);
}
}
@@ -47,10 +40,7 @@ export function ensureNoDuplicatesExist(
* @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 = ',',
): string[] | undefined {
export function toArrayConfig(configValue?: string, separator = ','): string[] | undefined {
if (!configValue) {
return undefined;
}
@@ -67,10 +57,7 @@ export function toArrayConfig(
* @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,
): boolean {
export function needToLog(currentLoglevel: Loglevel, requestedLoglevel: Loglevel): boolean {
const current = transformLoglevelToInt(currentLoglevel);
const requested = transformLoglevelToInt(requestedLoglevel);
return current >= requested;
+2 -6
View File
@@ -26,9 +26,7 @@ describe('zod error message', () => {
extractDescriptionFromZodIssue(issue, PREFIX),
);
expect(errorMessages).toHaveLength(1);
expect(errorMessages[0]).toEqual(
`${PREFIX}_PORT: Number must be greater than 0`,
);
expect(errorMessages[0]).toEqual(`${PREFIX}_PORT: Number must be greater than 0`);
});
it('correctly builds an error message on an array object', () => {
const schema = z.object({
@@ -45,9 +43,7 @@ describe('zod error message', () => {
extractDescriptionFromZodIssue(issue, PREFIX),
);
expect(errorMessages).toHaveLength(1);
expect(errorMessages[0]).toEqual(
`${PREFIX}_ARRAY[1]: Number must be greater than 0`,
);
expect(errorMessages[0]).toEqual(`${PREFIX}_ARRAY[1]: Number must be greater than 0`);
});
});
});
@@ -129,13 +129,9 @@ const up = async function (knex) {
.onDelete('CASCADE');
table.string(FieldNameApiToken.label).notNullable();
table.string(FieldNameApiToken.secretHash).notNullable();
table
.timestamp(FieldNameApiToken.validUntil, { useTz: false })
.notNullable();
table.timestamp(FieldNameApiToken.validUntil, { useTz: false }).notNullable();
table.timestamp(FieldNameApiToken.lastUsedAt, { useTz: false }).nullable();
table
.timestamp(FieldNameApiToken.createdAt, { useTz: false, precision: 3 })
.notNullable();
table.timestamp(FieldNameApiToken.createdAt, { useTz: false, precision: 3 }).notNullable();
table.index([FieldNameApiToken.userId], 'idx_api_token_user_id');
});
@@ -244,14 +240,8 @@ const up = async function (knex) {
.inTable(TableRevision)
.onDelete('CASCADE');
table.string(FieldNameRevisionTag.tag).notNullable();
table.primary([
FieldNameRevisionTag.revisionUuid,
FieldNameRevisionTag.tag,
]);
table.index(
[FieldNameRevisionTag.revisionUuid],
'idx_revision_tag_revision_uuid',
);
table.primary([FieldNameRevisionTag.revisionUuid, FieldNameRevisionTag.tag]);
table.index([FieldNameRevisionTag.revisionUuid], 'idx_revision_tag_revision_uuid');
});
// Create authorship_info table
@@ -269,23 +259,14 @@ const up = async function (knex) {
.references(FieldNameUser.id)
.inTable(TableUser)
.onDelete('CASCADE');
table
.integer(FieldNameAuthorshipInfo.startPosition)
.unsigned()
.notNullable();
table.integer(FieldNameAuthorshipInfo.startPosition).unsigned().notNullable();
table.integer(FieldNameAuthorshipInfo.endPosition).unsigned().notNullable();
table.timestamp(FieldNameAuthorshipInfo.createdAt, {
useTz: false,
precision: 3,
});
table.index(
[FieldNameAuthorshipInfo.revisionUuid],
'idx_authorship_info_revision_uuid',
);
table.index(
[FieldNameAuthorshipInfo.authorId],
'idx_authorship_info_author_id',
);
table.index([FieldNameAuthorshipInfo.revisionUuid], 'idx_authorship_info_revision_uuid');
table.index([FieldNameAuthorshipInfo.authorId], 'idx_authorship_info_author_id');
});
// Create note_user_permission table
@@ -304,22 +285,10 @@ const up = async function (knex) {
.references(FieldNameUser.id)
.inTable(TableUser)
.onDelete('CASCADE');
table
.boolean(FieldNameNoteUserPermission.canEdit)
.notNullable()
.defaultTo(false);
table.primary([
FieldNameNoteUserPermission.noteId,
FieldNameNoteUserPermission.userId,
]);
table.index(
[FieldNameNoteUserPermission.noteId],
'idx_note_user_permission_note_id',
);
table.index(
[FieldNameNoteUserPermission.userId],
'idx_note_user_permission_user_id',
);
table.boolean(FieldNameNoteUserPermission.canEdit).notNullable().defaultTo(false);
table.primary([FieldNameNoteUserPermission.noteId, FieldNameNoteUserPermission.userId]);
table.index([FieldNameNoteUserPermission.noteId], 'idx_note_user_permission_note_id');
table.index([FieldNameNoteUserPermission.userId], 'idx_note_user_permission_user_id');
});
// Create note_group_permission table
@@ -338,22 +307,10 @@ const up = async function (knex) {
.references(FieldNameGroup.id)
.inTable(TableGroup)
.onDelete('CASCADE');
table
.boolean(FieldNameNoteGroupPermission.canEdit)
.notNullable()
.defaultTo(false);
table.primary([
FieldNameNoteGroupPermission.noteId,
FieldNameNoteGroupPermission.groupId,
]);
table.index(
[FieldNameNoteGroupPermission.noteId],
'idx_note_group_permission_note_id',
);
table.index(
[FieldNameNoteGroupPermission.groupId],
'idx_note_group_permission_group_id',
);
table.boolean(FieldNameNoteGroupPermission.canEdit).notNullable().defaultTo(false);
table.primary([FieldNameNoteGroupPermission.noteId, FieldNameNoteGroupPermission.groupId]);
table.index([FieldNameNoteGroupPermission.noteId], 'idx_note_group_permission_note_id');
table.index([FieldNameNoteGroupPermission.groupId], 'idx_note_group_permission_group_id');
});
// Create media_upload table
@@ -414,18 +371,9 @@ const up = async function (knex) {
.references(FieldNameNote.id)
.inTable(TableNote)
.onDelete('CASCADE');
table.primary([
FieldNameUserPinnedNote.userId,
FieldNameUserPinnedNote.noteId,
]);
table.index(
[FieldNameUserPinnedNote.userId],
'idx_user_pinned_note_user_id',
);
table.index(
[FieldNameUserPinnedNote.noteId],
'idx_user_pinned_note_note_id',
);
table.primary([FieldNameUserPinnedNote.userId, FieldNameUserPinnedNote.noteId]);
table.index([FieldNameUserPinnedNote.userId], 'idx_user_pinned_note_user_id');
table.index([FieldNameUserPinnedNote.noteId], 'idx_user_pinned_note_note_id');
});
// Create visited_notes table
+1 -1
View File
@@ -3,4 +3,4 @@
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.
CommonJS package.
+1 -3
View File
@@ -31,9 +31,7 @@ export function expectBindings(
}
if (usesFirst) {
if (method !== 'select') {
throw new Error(
'Expected `select` as method if `usesFirst` is set to true',
);
throw new Error('Expected `select` as method if `usesFirst` is set to true');
}
bindings[0].push(IS_FIRST);
}
+2 -5
View File
@@ -32,8 +32,7 @@ export function mockSelect(
returnValue: unknown = [],
joins: JoinDefinition[] = [],
): void {
const selection =
variables.length > 0 ? variables.map((v) => `"${v}"`).join(', ') : '\\*';
const selection = variables.length > 0 ? variables.map((v) => `"${v}"`).join(', ') : '\\*';
const joinStatement =
joins.length > 0
? joins
@@ -44,9 +43,7 @@ export function mockSelect(
})
.join(' ') + ' '
: '';
const whereClause = Array.isArray(where)
? where.map((w) => `"${w}"`).join('.*')
: `"${where}"`;
const whereClause = Array.isArray(where) ? where.map((w) => `"${w}"`).join('.*') : `"${where}"`;
const regex = `select(?: distinct)? ${selection} from "${table}" ${joinStatement}where .*${whereClause}.*`;
const selectRegex = new RegExp(regex);
tracker.on.select(selectRegex).responseOnce(returnValue);
+1 -6
View File
@@ -4,12 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthProviderType } from '@hedgedoc/commons';
import {
FieldNameIdentity,
FieldNameUser,
TableIdentity,
TableUser,
} from '@hedgedoc/database';
import { FieldNameIdentity, FieldNameUser, TableIdentity, TableUser } from '@hedgedoc/database';
import { Knex } from 'knex';
import { dateTimeToDB, getCurrentDateTime } from '../../utils/datetime';
+3 -11
View File
@@ -7,19 +7,13 @@ import { FieldNameApiToken, TableApiToken } from '@hedgedoc/database';
import { createHash } from 'crypto';
import { Knex } from 'knex';
import {
dateTimeToDB,
dateTimeToISOString,
getCurrentDateTime,
} from '../../utils/datetime';
import { dateTimeToDB, dateTimeToISOString, getCurrentDateTime } from '../../utils/datetime';
export async function seed(knex: Knex): Promise<void> {
// Clear table beforehand
await knex(TableApiToken).del();
const validUntil = dateTimeToISOString(
getCurrentDateTime().plus({ year: 1 }),
);
const validUntil = dateTimeToISOString(getCurrentDateTime().plus({ year: 1 }));
// Insert an api token
const apiToken =
@@ -28,9 +22,7 @@ export async function seed(knex: Knex): Promise<void> {
[FieldNameApiToken.id]: 'pA4mOf51bpY',
[FieldNameApiToken.userId]: 2,
[FieldNameApiToken.label]: 'Local Test User API Token',
[FieldNameApiToken.secretHash]: createHash('sha512')
.update(apiToken)
.digest('hex'),
[FieldNameApiToken.secretHash]: createHash('sha512').update(apiToken).digest('hex'),
[FieldNameApiToken.validUntil]: validUntil,
[FieldNameApiToken.createdAt]: dateTimeToDB(getCurrentDateTime()),
[FieldNameApiToken.lastUsedAt]: null,
+3 -15
View File
@@ -107,11 +107,7 @@ export async function seed(knex: Knex): Promise<void> {
{
[FieldNameRevision.uuid]: guestNoteRevisionUuid,
[FieldNameRevision.noteId]: 1,
[FieldNameRevision.patch]: createPatch(
guestNoteAlias,
'',
guestNoteContent,
),
[FieldNameRevision.patch]: createPatch(guestNoteAlias, '', guestNoteContent),
[FieldNameRevision.content]: guestNoteContent,
[FieldNameRevision.yjsStateVector]: null,
[FieldNameRevision.noteType]: NoteType.DOCUMENT,
@@ -122,11 +118,7 @@ export async function seed(knex: Knex): Promise<void> {
{
[FieldNameRevision.uuid]: userNoteRevisionUuid,
[FieldNameRevision.noteId]: 1,
[FieldNameRevision.patch]: createPatch(
userNoteAlias,
'',
userNoteContent,
),
[FieldNameRevision.patch]: createPatch(userNoteAlias, '', userNoteContent),
[FieldNameRevision.content]: userNoteContent,
[FieldNameRevision.yjsStateVector]: null,
[FieldNameRevision.noteType]: NoteType.DOCUMENT,
@@ -137,11 +129,7 @@ export async function seed(knex: Knex): Promise<void> {
{
[FieldNameRevision.uuid]: userSlideRevisionUuid,
[FieldNameRevision.noteId]: 1,
[FieldNameRevision.patch]: createPatch(
userSlideAlias,
'',
userSlideContent,
),
[FieldNameRevision.patch]: createPatch(userSlideAlias, '', userSlideContent),
[FieldNameRevision.content]: userSlideContent,
[FieldNameRevision.yjsStateVector]: null,
[FieldNameRevision.noteType]: NoteType.SLIDE,
+3 -15
View File
@@ -52,22 +52,10 @@ declare module 'knex/types/tables.js' {
[TableAlias]: Knex.CompositeTableType<Alias, Alias, TypeUpdateAlias>;
[TableApiToken]: Knex.CompositeTableType<ApiToken>;
[TableAuthorshipInfo]: Knex.CompositeTableType<AuthorshipInfo>;
[TableGroup]: Knex.CompositeTableType<
Group,
TypeInsertGroup,
TypeUpdateGroup
>;
[TableGroup]: Knex.CompositeTableType<Group, TypeInsertGroup, TypeUpdateGroup>;
[TableGroupUser]: GroupUser;
[TableIdentity]: Knex.CompositeTableType<
Identity,
Identity,
TypeUpdateIdentity
>;
[TableMediaUpload]: Knex.CompositeTableType<
MediaUpload,
MediaUpload,
TypeUpdateMediaUpload
>;
[TableIdentity]: Knex.CompositeTableType<Identity, Identity, TypeUpdateIdentity>;
[TableMediaUpload]: Knex.CompositeTableType<MediaUpload, MediaUpload, TypeUpdateMediaUpload>;
[TableNote]: Knex.CompositeTableType<Note, TypeInsertNote, TypeUpdateNote>;
[TableNoteGroupPermission]: Knex.CompositeTableType<
NoteGroupPermission,
@@ -6,6 +6,4 @@
import { ApiTokenWithSecretSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class ApiTokenWithSecretDto extends createZodDto(
ApiTokenWithSecretSchema,
) {}
export class ApiTokenWithSecretDto extends createZodDto(ApiTokenWithSecretSchema) {}
@@ -6,6 +6,4 @@
import { AuthProviderWithCustomNameSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class AuthProviderWithCustomNameDto extends createZodDto(
AuthProviderWithCustomNameSchema,
) {}
export class AuthProviderWithCustomNameDto extends createZodDto(AuthProviderWithCustomNameSchema) {}
@@ -6,6 +6,4 @@
import { ChangeNoteVisibilitySchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class ChangeNoteVisibilityDto extends createZodDto(
ChangeNoteVisibilitySchema,
) {}
export class ChangeNoteVisibilityDto extends createZodDto(ChangeNoteVisibilitySchema) {}
@@ -6,6 +6,4 @@
import { GuestRegistrationResponseSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class GuestRegistrationResponseDto extends createZodDto(
GuestRegistrationResponseSchema,
) {}
export class GuestRegistrationResponseDto extends createZodDto(GuestRegistrationResponseSchema) {}
+1 -3
View File
@@ -6,6 +6,4 @@
import { LdapLoginResponseSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class LdapLoginResponseDto extends createZodDto(
LdapLoginResponseSchema,
) {}
export class LdapLoginResponseDto extends createZodDto(LdapLoginResponseSchema) {}
@@ -6,6 +6,4 @@
import { NoteGroupPermissionEntrySchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class NoteGroupPermissionEntryDto extends createZodDto(
NoteGroupPermissionEntrySchema,
) {}
export class NoteGroupPermissionEntryDto extends createZodDto(NoteGroupPermissionEntrySchema) {}
@@ -11,6 +11,4 @@ import { createZodDto } from 'nestjs-zod';
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteGroupPermissionUpdateDto extends createZodDto(
NoteGroupPermissionUpdateSchema,
) {}
export class NoteGroupPermissionUpdateDto extends createZodDto(NoteGroupPermissionUpdateSchema) {}
+1 -3
View File
@@ -6,6 +6,4 @@
import { NoteMetadataUpdateSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class NoteMetadataUpdateDto extends createZodDto(
NoteMetadataUpdateSchema,
) {}
export class NoteMetadataUpdateDto extends createZodDto(NoteMetadataUpdateSchema) {}
@@ -6,6 +6,4 @@
import { NoteUserPermissionEntrySchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class NoteUserPermissionEntryDto extends createZodDto(
NoteUserPermissionEntrySchema,
) {}
export class NoteUserPermissionEntryDto extends createZodDto(NoteUserPermissionEntrySchema) {}
@@ -6,6 +6,4 @@
import { NoteUserPermissionUpdateSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class NoteUserPermissionUpdateDto extends createZodDto(
NoteUserPermissionUpdateSchema,
) {}
export class NoteUserPermissionUpdateDto extends createZodDto(NoteUserPermissionUpdateSchema) {}
+1 -3
View File
@@ -6,6 +6,4 @@
import { NoteMediaDeletionSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class NoteMediaDeletionDto extends createZodDto(
NoteMediaDeletionSchema,
) {}
export class NoteMediaDeletionDto extends createZodDto(NoteMediaDeletionSchema) {}
@@ -6,6 +6,4 @@
import { PendingLdapUserInfoSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class PendingLdapUserInfoDto extends createZodDto(
PendingLdapUserInfoSchema,
) {}
export class PendingLdapUserInfoDto extends createZodDto(PendingLdapUserInfoSchema) {}
@@ -6,6 +6,4 @@
import { PendingUserConfirmationSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class PendingUserConfirmationDto extends createZodDto(
PendingUserConfirmationSchema,
) {}
export class PendingUserConfirmationDto extends createZodDto(PendingUserConfirmationSchema) {}
@@ -6,6 +6,4 @@
import { UsernameCheckResponseSchema } from '@hedgedoc/commons';
import { createZodDto } from 'nestjs-zod';
export class UsernameCheckResponseDto extends createZodDto(
UsernameCheckResponseSchema,
) {}
export class UsernameCheckResponseDto extends createZodDto(UsernameCheckResponseSchema) {}
+31 -77
View File
@@ -22,70 +22,36 @@ import { ZodError } from 'zod';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { ErrorWithContextDetails } from './errors';
import {
buildHttpExceptionObject,
HttpExceptionObject,
} from './http-exception-object';
import { buildHttpExceptionObject, HttpExceptionObject } from './http-exception-object';
type HttpExceptionConstructor = (object: HttpExceptionObject) => HttpException;
const mapOfHedgeDocErrorsToHttpErrors: Map<string, HttpExceptionConstructor> =
new Map([
['NotInDBError', (object): HttpException => new NotFoundException(object)],
[
'AlreadyInDBError',
(object): HttpException => new ConflictException(object),
],
[
'ForbiddenIdError',
(object): HttpException => new ForbiddenException(object),
],
['ClientError', (object): HttpException => new BadRequestException(object)],
[
'PermissionError',
(object): HttpException => new ForbiddenException(object),
],
[
'TokenNotValidError',
(object): HttpException => new UnauthorizedException(object),
],
[
'TooManyTokensError',
(object): HttpException => new BadRequestException(object),
],
[
'PermissionsUpdateInconsistentError',
(object): HttpException => new BadRequestException(object),
],
[
'MediaBackendError',
(object): HttpException => new InternalServerErrorException(object),
],
[
'PrimaryAliasDeletionForbiddenError',
(object): HttpException => new BadRequestException(object),
],
[
'InvalidCredentialsError',
(object): HttpException => new UnauthorizedException(object),
],
[
'NoLocalIdentityError',
(object): HttpException => new BadRequestException(object),
],
[
'PasswordTooWeakError',
(object): HttpException => new BadRequestException(object),
],
[
'MaximumDocumentLengthExceededError',
(object): HttpException => new PayloadTooLargeException(object),
],
[
'FeatureDisabledError',
(object): HttpException => new ForbiddenException(object),
],
]);
const mapOfHedgeDocErrorsToHttpErrors: Map<string, HttpExceptionConstructor> = new Map([
['NotInDBError', (object): HttpException => new NotFoundException(object)],
['AlreadyInDBError', (object): HttpException => new ConflictException(object)],
['ForbiddenIdError', (object): HttpException => new ForbiddenException(object)],
['ClientError', (object): HttpException => new BadRequestException(object)],
['PermissionError', (object): HttpException => new ForbiddenException(object)],
['TokenNotValidError', (object): HttpException => new UnauthorizedException(object)],
['TooManyTokensError', (object): HttpException => new BadRequestException(object)],
[
'PermissionsUpdateInconsistentError',
(object): HttpException => new BadRequestException(object),
],
['MediaBackendError', (object): HttpException => new InternalServerErrorException(object)],
[
'PrimaryAliasDeletionForbiddenError',
(object): HttpException => new BadRequestException(object),
],
['InvalidCredentialsError', (object): HttpException => new UnauthorizedException(object)],
['NoLocalIdentityError', (object): HttpException => new BadRequestException(object)],
['PasswordTooWeakError', (object): HttpException => new BadRequestException(object)],
[
'MaximumDocumentLengthExceededError',
(object): HttpException => new PayloadTooLargeException(object),
],
['FeatureDisabledError', (object): HttpException => new ForbiddenException(object)],
]);
@Catch()
/**
@@ -111,32 +77,20 @@ export class ErrorExceptionMapping extends BaseExceptionFilter<Error> {
* @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,
);
const httpExceptionConstructor = mapOfHedgeDocErrorsToHttpErrors.get(error.name);
if (error instanceof ZodSerializationException) {
const zodError = error.getZodError();
if (zodError instanceof ZodError) {
this.loggerService.error(
`ZodSerializationException: ${zodError.message}`,
);
this.loggerService.error(`ZodSerializationException: ${zodError.message}`);
}
} else if (error instanceof ErrorWithContextDetails) {
this.loggerService.error(
error.message,
undefined,
error.functionContext,
error.classContext,
);
this.loggerService.error(error.message, undefined, error.functionContext, error.classContext);
}
if (httpExceptionConstructor === undefined) {
// We don't know how to map this error and just leave it be
return error;
}
const httpExceptionObject = buildHttpExceptionObject(
error.name,
error.message,
);
const httpExceptionObject = buildHttpExceptionObject(error.name, error.message);
return httpExceptionConstructor(httpExceptionObject);
}
}
+1 -4
View File
@@ -9,10 +9,7 @@ export interface HttpExceptionObject {
message: string;
}
export function buildHttpExceptionObject(
name: string,
message: string,
): HttpExceptionObject {
export function buildHttpExceptionObject(name: string, message: string): HttpExceptionObject {
return {
name: name,
message: message,
+27 -92
View File
@@ -26,11 +26,7 @@ import appConfigMock from '../config/mock/app.config.mock';
import databaseConfigMock from '../config/mock/database.config.mock';
import noteConfigMock from '../config/mock/note.config.mock';
import { expectBindings, IS_FIRST } from '../database/mock/expect-bindings';
import {
mockDelete,
mockQuery,
mockSelect,
} from '../database/mock/mock-queries';
import { mockDelete, mockQuery, mockSelect } from '../database/mock/mock-queries';
import { mockKnexDb } from '../database/mock/provider';
import { NoteEventMap } from '../events';
import { GroupsService } from '../groups/groups.service';
@@ -40,16 +36,8 @@ import { PermissionService } from '../permissions/permission.service';
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
import { RevisionsService } from '../revisions/revisions.service';
import { UsersService } from '../users/users.service';
import {
dateTimeToDB,
dateTimeToISOString,
getCurrentDateTime,
} from '../utils/datetime';
import {
ENTRIES_PER_PAGE_LIMIT,
ExploreService,
QueryResult,
} from './explore.service';
import { dateTimeToDB, dateTimeToISOString, getCurrentDateTime } from '../utils/datetime';
import { ENTRIES_PER_PAGE_LIMIT, ExploreService, QueryResult } from './explore.service';
describe('ExploreService', () => {
let service: ExploreService;
@@ -168,14 +156,7 @@ describe('ExploreService', () => {
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note"."owner_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
],
] as [
string,
OptionalNoteType,
OptionalSortMode,
string,
RegExp,
unknown[],
][])(
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
'correctly get all notes owned by user with',
(name, noteType, sortBy, search, regex, bindings) => {
// oxlint-disable-next-line jest/valid-title
@@ -244,14 +225,7 @@ describe('ExploreService', () => {
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note_user_permission" inner join "note" on "note_user_permission"."note_id" = "note"."id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_user_permission"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
],
] as [
string,
OptionalNoteType,
OptionalSortMode,
string,
RegExp,
unknown[],
][])(
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
'correctly get all notes shared with the user with',
(name, noteType, sortBy, search, regex, bindings) => {
// oxlint-disable-next-line jest/valid-title
@@ -302,14 +276,7 @@ describe('ExploreService', () => {
'',
'',
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and "revision"."note_type" = \$5 order by "revision"."created_at" desc limit \$6/,
[
1,
true,
mockEveryoneGroupId,
true,
NoteType.SLIDE,
ENTRIES_PER_PAGE_LIMIT,
],
[1, true, mockEveryoneGroupId, true, NoteType.SLIDE, ENTRIES_PER_PAGE_LIMIT],
],
[
'sorting',
@@ -327,32 +294,19 @@ describe('ExploreService', () => {
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and LOWER\("revision"."title"\) LIKE \$5 order by "revision"."created_at" desc limit \$6/,
[1, true, mockEveryoneGroupId, true, '%test%', ENTRIES_PER_PAGE_LIMIT],
],
] as [
string,
OptionalNoteType,
OptionalSortMode,
string,
RegExp,
unknown[],
][])(
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
'correctly get all public notes with',
(name, noteType, sortBy, search, regex, bindings) => {
// oxlint-disable-next-line jest/valid-title
it(name, async () => {
mockSelect(
tracker,
[FieldNameGroup.id],
TableGroup,
FieldNameGroup.name,
[
{
[FieldNameGroup.id]: mockEveryoneGroupId,
[FieldNameGroup.name]: '_EVERYONE',
[FieldNameGroup.displayName]: 'Everyone',
[FieldNameGroup.isSpecial]: true,
},
],
);
mockSelect(tracker, [FieldNameGroup.id], TableGroup, FieldNameGroup.name, [
{
[FieldNameGroup.id]: mockEveryoneGroupId,
[FieldNameGroup.name]: '_EVERYONE',
[FieldNameGroup.displayName]: 'Everyone',
[FieldNameGroup.isSpecial]: true,
},
]);
mockQuery('select', tracker, regex, selectedRows);
mockQuery(
'select',
@@ -400,8 +354,7 @@ describe('ExploreService', () => {
/select "tag" from "revision_tag" where "revision_id" = \$1/,
selectedTags,
);
const exploreEntries =
await service.getMyPinnedNoteExploreEntries(mockUserId);
const exploreEntries = await service.getMyPinnedNoteExploreEntries(mockUserId);
expect(exploreEntries.length).toBe(1);
expect(exploreEntries[0]).toEqual({
primaryAlias: mockPrimaryAlias,
@@ -412,10 +365,7 @@ describe('ExploreService', () => {
lastChangedAt: dateTimeToISOString(now),
lastVisitedAt: null,
});
expectBindings(tracker, 'select', [
[1, true, mockUserId],
[mockRevisionUuid],
]);
expectBindings(tracker, 'select', [[1, true, mockUserId], [mockRevisionUuid]]);
});
});
describe('getRecentlyVisitedNoteExploreEntries', () => {
@@ -452,14 +402,7 @@ describe('ExploreService', () => {
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "visited_notes"."visited_at" as "lastVisitedAt", "note"."id" as "noteId" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "visited_notes" on "visited_notes"."note_id" = "note"."id" where "alias"."is_primary" = \$2 and "visited_notes"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
],
] as [
string,
OptionalNoteType,
OptionalSortMode,
string,
RegExp,
unknown[],
][])(
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
'correctly get all notes visited by the user with',
(name, noteType, sortBy, search, regex, bindings) => {
// oxlint-disable-next-line jest/valid-title
@@ -482,14 +425,13 @@ describe('ExploreService', () => {
/select "tag" from "revision_tag" where "revision_id" = \$1/,
selectedTags,
);
const exploreEntries =
await service.getRecentlyVisitedNoteExploreEntries(
mockUserId,
pageNumber,
noteType,
sortBy,
search,
);
const exploreEntries = await service.getRecentlyVisitedNoteExploreEntries(
mockUserId,
pageNumber,
noteType,
sortBy,
search,
);
expect(exploreEntries.length).toBe(1);
expect(exploreEntries[0]).toEqual({
primaryAlias: mockPrimaryAlias,
@@ -500,11 +442,7 @@ describe('ExploreService', () => {
lastChangedAt: dateTimeToISOString(now),
lastVisitedAt: null,
});
expectBindings(tracker, 'select', [
bindings,
[mockNoteId, IS_FIRST],
[mockRevisionUuid],
]);
expectBindings(tracker, 'select', [bindings, [mockNoteId, IS_FIRST], [mockRevisionUuid]]);
});
},
);
@@ -531,10 +469,7 @@ describe('ExploreService', () => {
selectedTags,
);
await service.setNotePinStatus(mockUserId, mockNoteId, true);
expectBindings(tracker, 'select', [
[1, mockNoteId, true],
[mockRevisionUuid],
]);
expectBindings(tracker, 'select', [[1, mockNoteId, true], [mockRevisionUuid]]);
});
it('unpins note', async () => {
+33 -117
View File
@@ -3,12 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoteType,
OptionalSortMode,
PermissionLevel,
SortMode,
} from '@hedgedoc/commons';
import { NoteType, OptionalSortMode, PermissionLevel, SortMode } from '@hedgedoc/commons';
import {
FieldNameAlias,
FieldNameNote,
@@ -93,10 +88,7 @@ export class ExploreService {
return await this.knex.transaction(async (transaction) => {
const queryBase = transaction(TableNote);
let query = this.applyCommonQuery(queryBase, transaction);
if (
sortBy === SortMode.LAST_VISITED_ASC ||
sortBy === SortMode.LAST_VISITED_DESC
) {
if (sortBy === SortMode.LAST_VISITED_ASC || sortBy === SortMode.LAST_VISITED_DESC) {
query = this.joinWithTableVisitedNote(query);
}
query = query.andWhere(`${TableNote}.${FieldNameNote.ownerId}`, userId);
@@ -133,10 +125,7 @@ export class ExploreService {
`${TableNote}.${FieldNameNote.id}`,
);
let query = this.applyCommonQuery(queryBase, transaction);
if (
sortBy === SortMode.LAST_VISITED_ASC ||
sortBy === SortMode.LAST_VISITED_DESC
) {
if (sortBy === SortMode.LAST_VISITED_ASC || sortBy === SortMode.LAST_VISITED_DESC) {
query = this.joinWithTableVisitedNote(query);
}
query = query.andWhere(
@@ -167,9 +156,7 @@ export class ExploreService {
sortBy?: OptionalSortMode,
search?: string,
): Promise<NoteExploreEntryDto[]> {
const everyoneGroupId = await this.groupsService.getGroupIdByName(
SpecialGroup.EVERYONE,
);
const everyoneGroupId = await this.groupsService.getGroupIdByName(SpecialGroup.EVERYONE);
return await this.knex.transaction(async (transaction) => {
const queryBase = transaction(TableNote).join(
TableNoteGroupPermission,
@@ -183,10 +170,7 @@ export class ExploreService {
everyoneGroupId,
)
.andWhere(`${TableNote}.${FieldNameNote.publiclyVisible}`, true);
if (
sortBy === SortMode.LAST_VISITED_ASC ||
sortBy === SortMode.LAST_VISITED_DESC
) {
if (sortBy === SortMode.LAST_VISITED_ASC || sortBy === SortMode.LAST_VISITED_DESC) {
query = this.joinWithTableVisitedNote(query);
}
query = this.applyFiltersToQuery(query, noteType, search);
@@ -203,9 +187,7 @@ export class ExploreService {
* @param userId The user that has the notes pinned.
* @return A list of {@link NoteExploreEntryDto}
*/
async getMyPinnedNoteExploreEntries(
userId: number,
): Promise<NoteExploreEntryDto[]> {
async getMyPinnedNoteExploreEntries(userId: number): Promise<NoteExploreEntryDto[]> {
return await this.knex.transaction(async (transaction) => {
const queryBase = transaction(TableUserPinnedNote).join(
TableNote,
@@ -213,10 +195,7 @@ export class ExploreService {
`${TableNote}.${FieldNameNote.id}`,
);
let query = this.applyCommonQuery(queryBase, transaction);
query = query.andWhere(
`${TableUserPinnedNote}.${FieldNameUserPinnedNote.userId}`,
userId,
);
query = query.andWhere(`${TableUserPinnedNote}.${FieldNameUserPinnedNote.userId}`, userId);
query = this.applySortingToQuery(query, SortMode.UPDATED_AT_DESC);
const results = (await query) as QueryResult[];
return await this.transformQueryResultIntoDtos(results, transaction);
@@ -270,20 +249,16 @@ export class ExploreService {
noteId: `${TableNote}.${FieldNameNote.id}`,
});
query = this.joinWithTableVisitedNote(query);
query = query.andWhere(
`${TableVisitedNote}.${FieldNameVisitedNote.userId}`,
userId,
);
query = query.andWhere(`${TableVisitedNote}.${FieldNameVisitedNote.userId}`, userId);
query = this.applyFiltersToQuery(query, noteType, search);
query = this.applySortingToQuery(query, sortBy);
query = this.applyPaginationToQuery(query, page);
const results = (await query) as QueryResultWithNoteId[];
const filteredResults =
await this.checkPermissionsAndCleanUpRecentlyVisitedNotes(
userId,
results,
transaction,
);
const filteredResults = await this.checkPermissionsAndCleanUpRecentlyVisitedNotes(
userId,
results,
transaction,
);
if (retry < 2) {
// To prevent multiple loops, we only try this once again if we filtered everything out
if (filteredResults.length === 0 && results.length !== 0) {
@@ -299,10 +274,7 @@ export class ExploreService {
);
}
}
return await this.transformQueryResultIntoDtos(
filteredResults,
transaction,
);
return await this.transformQueryResultIntoDtos(filteredResults, transaction);
}
private async checkPermissionsAndCleanUpRecentlyVisitedNotes(
@@ -312,11 +284,7 @@ export class ExploreService {
): Promise<QueryResultWithNoteId[]> {
const rejectedNoteIds: number[] = [];
const permissionLevelPromises = resultWithNoteIds.map((result) =>
this.permissionsService.determinePermission(
userId,
result.noteId,
transaction,
),
this.permissionsService.determinePermission(userId, result.noteId, transaction),
);
const permissionLevels = await Promise.all(permissionLevelPromises);
for (let i = 0; i < resultWithNoteIds.length; i++) {
@@ -330,9 +298,7 @@ export class ExploreService {
.delete();
}
}
return resultWithNoteIds.filter(
(result) => !rejectedNoteIds.includes(result.noteId),
);
return resultWithNoteIds.filter((result) => !rejectedNoteIds.includes(result.noteId));
}
private async transformQueryResultIntoDtos(
@@ -341,10 +307,7 @@ export class ExploreService {
): Promise<NoteExploreEntryDto[]> {
const resultsWithTagList: QueryResultWithTagList[] = [];
const tagsPromises = results.map((result) =>
this.revisionsService.getTagsByRevisionUuid(
result.revisionUuid,
transaction,
),
this.revisionsService.getTagsByRevisionUuid(result.revisionUuid, transaction),
);
const tags = await Promise.all(tagsPromises);
for (let i = 0; i < results.length; i++) {
@@ -380,10 +343,7 @@ export class ExploreService {
// The correct return type with all joins and selects is very specific and should just be inferred from Knex
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type
private applyCommonQuery(
query: Knex.QueryBuilder,
transaction: Knex.Transaction,
) {
private applyCommonQuery(query: Knex.QueryBuilder, transaction: Knex.Transaction) {
const subquery = transaction(TableRevision)
.select(`${FieldNameRevision.uuid}`, `${FieldNameRevision.noteId}`)
.rowNumber('rn', function () {
@@ -407,11 +367,7 @@ export class ExploreService {
`${TableAlias}.${FieldNameAlias.noteId}`,
`${TableNote}.${FieldNameNote.id}`,
)
.join(
TableUser,
`${TableUser}.${FieldNameUser.id}`,
`${TableNote}.${FieldNameNote.ownerId}`,
)
.join(TableUser, `${TableUser}.${FieldNameUser.id}`, `${TableNote}.${FieldNameNote.ownerId}`)
.join(
this.knex
.select(`${FieldNameRevision.uuid}`, `${FieldNameRevision.noteId}`)
@@ -459,61 +415,29 @@ export class ExploreService {
return filteredQuery;
}
private applyPaginationToQuery<T extends Knex.QueryBuilder>(
query: T,
page: number,
): T {
return query
.limit(ENTRIES_PER_PAGE_LIMIT)
.offset((page - 1) * ENTRIES_PER_PAGE_LIMIT) as T;
private applyPaginationToQuery<T extends Knex.QueryBuilder>(query: T, page: number): T {
return query.limit(ENTRIES_PER_PAGE_LIMIT).offset((page - 1) * ENTRIES_PER_PAGE_LIMIT) as T;
}
private applySortingToQuery<T extends Knex.QueryBuilder>(
query: T,
sortBy?: OptionalSortMode,
): T {
private applySortingToQuery<T extends Knex.QueryBuilder>(query: T, sortBy?: OptionalSortMode): T {
switch (sortBy) {
case SortMode.TITLE_ASC:
return query.orderBy(
`${TableRevision}.${FieldNameRevision.title}`,
'asc',
) as T;
return query.orderBy(`${TableRevision}.${FieldNameRevision.title}`, 'asc') as T;
case SortMode.TITLE_DESC:
return query.orderBy(
`${TableRevision}.${FieldNameRevision.title}`,
'desc',
) as T;
return query.orderBy(`${TableRevision}.${FieldNameRevision.title}`, 'desc') as T;
case SortMode.LAST_VISITED_ASC:
return query.orderBy(
`${TableVisitedNote}.${FieldNameVisitedNote.visitedAt}`,
'asc',
) as T;
return query.orderBy(`${TableVisitedNote}.${FieldNameVisitedNote.visitedAt}`, 'asc') as T;
case SortMode.LAST_VISITED_DESC:
return query.orderBy(
`${TableVisitedNote}.${FieldNameVisitedNote.visitedAt}`,
'desc',
) as T;
return query.orderBy(`${TableVisitedNote}.${FieldNameVisitedNote.visitedAt}`, 'desc') as T;
case SortMode.CREATED_AT_ASC:
return query.orderBy(
`${TableNote}.${FieldNameNote.createdAt}`,
'asc',
) as T;
return query.orderBy(`${TableNote}.${FieldNameNote.createdAt}`, 'asc') as T;
case SortMode.CREATED_AT_DESC:
return query.orderBy(
`${TableNote}.${FieldNameNote.createdAt}`,
'desc',
) as T;
return query.orderBy(`${TableNote}.${FieldNameNote.createdAt}`, 'desc') as T;
case SortMode.UPDATED_AT_ASC:
return query.orderBy(
`${TableRevision}.${FieldNameRevision.createdAt}`,
'asc',
) as T;
return query.orderBy(`${TableRevision}.${FieldNameRevision.createdAt}`, 'asc') as T;
default:
case SortMode.UPDATED_AT_DESC:
return query.orderBy(
`${TableRevision}.${FieldNameRevision.createdAt}`,
'desc',
) as T;
return query.orderBy(`${TableRevision}.${FieldNameRevision.createdAt}`, 'desc') as T;
}
}
@@ -540,20 +464,12 @@ export class ExploreService {
[FieldNameUserPinnedNote.userId]: userId,
[FieldNameUserPinnedNote.noteId]: noteId,
})
.onConflict([
FieldNameUserPinnedNote.userId,
FieldNameUserPinnedNote.noteId,
])
.onConflict([FieldNameUserPinnedNote.userId, FieldNameUserPinnedNote.noteId])
.ignore();
const queryBase = transaction(TableNote).where(
`${TableNote}.${FieldNameNote.id}`,
noteId,
);
const queryBase = transaction(TableNote).where(`${TableNote}.${FieldNameNote.id}`, noteId);
const query = this.applyCommonQuery(queryBase, transaction);
const results = (await query) as QueryResult[];
return (
await this.transformQueryResultIntoDtos(results, transaction)
)[0];
return (await this.transformQueryResultIntoDtos(results, transaction))[0];
} else {
await transaction(TableUserPinnedNote)
.where({
@@ -3,11 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
AuthProviderType,
PermissionLevel,
PermissionLevelNames,
} from '@hedgedoc/commons';
import { AuthProviderType, PermissionLevel, PermissionLevelNames } from '@hedgedoc/commons';
import { ConfigModule, registerAs } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { URL } from 'url';
@@ -140,20 +136,14 @@ describe('FrontendConfigService', () => {
});
}
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.LDAP,
).length,
config.authProviders.filter((provider) => provider.type === AuthProviderType.LDAP).length,
).toEqual(authConfig.ldap.length);
expect(
config.authProviders.filter(
(provider) => provider.type === AuthProviderType.OIDC,
).length,
config.authProviders.filter((provider) => provider.type === AuthProviderType.OIDC).length,
).toEqual(authConfig.oidc.length);
if (authConfig.ldap.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.LDAP,
),
config.authProviders.find((provider) => provider.type === AuthProviderType.LDAP),
).toEqual({
type: AuthProviderType.LDAP,
providerName: authConfig.ldap[0].providerName,
@@ -162,9 +152,7 @@ describe('FrontendConfigService', () => {
}
if (authConfig.oidc.length > 0) {
expect(
config.authProviders.find(
(provider) => provider.type === AuthProviderType.OIDC,
),
config.authProviders.find((provider) => provider.type === AuthProviderType.OIDC),
).toEqual({
type: AuthProviderType.OIDC,
providerName: authConfig.oidc[0].providerName,
@@ -239,14 +227,8 @@ describe('FrontendConfigService', () => {
load: [
registerAs('appConfig', () => appConfig),
registerAs('authConfig', () => authConfig),
registerAs(
'customizationConfig',
() => customizationConfig,
),
registerAs(
'externalServicesConfig',
() => externalServicesConfig,
),
registerAs('customizationConfig', () => customizationConfig),
registerAs('externalServicesConfig', () => externalServicesConfig),
registerAs('noteConfig', () => noteConfig),
],
}),
@@ -258,18 +240,14 @@ describe('FrontendConfigService', () => {
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
expect(config.allowRegister).toEqual(enableRegister);
expect(config.guestAccess).toEqual(
noteConfig.permissions.maxGuestLevel,
);
expect(config.guestAccess).toEqual(noteConfig.permissions.maxGuestLevel);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo !== null ? new URL(customLogo).toString() : null,
);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.plantUmlServer).toEqual(
plantUmlServer !== null
? new URL(plantUmlServer).toString()
: null,
plantUmlServer !== null ? new URL(plantUmlServer).toString() : null,
);
expect(config.specialUrls.imprint).toEqual(
imprintLink !== null ? new URL(imprintLink).toString() : null,
@@ -278,14 +256,10 @@ describe('FrontendConfigService', () => {
privacyLink !== null ? new URL(privacyLink).toString() : null,
);
expect(config.specialUrls.termsOfUse).toEqual(
termsOfUseLink !== null
? new URL(termsOfUseLink).toString()
: null,
termsOfUseLink !== null ? new URL(termsOfUseLink).toString() : null,
);
expect(config.useImageProxy).toEqual(!!imageProxy);
expect(config.version).toEqual(
await getServerVersionFromPackageJson(),
);
expect(config.version).toEqual(await getServerVersionFromPackageJson());
});
index += 1;
}
@@ -8,9 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { URL } from 'url';
import authConfiguration, { AuthConfig } from '../config/auth.config';
import customizationConfiguration, {
CustomizationConfig,
} from '../config/customization.config';
import customizationConfiguration, { CustomizationConfig } from '../config/customization.config';
import externalServicesConfiguration, {
ExternalServicesConfig,
} from '../config/external-services.config';
+11 -45
View File
@@ -70,13 +70,11 @@ describe('GroupsService', () => {
it('throws AlreadyInDBError if group already exists', async () => {
tracker.on
.insert(
/^insert into "group" \("display_name", "is_special", "name"\) values .*/,
)
.insert(/^insert into "group" \("display_name", "is_special", "name"\) values .*/)
.simulateError('duplicate key value violates unique constraint');
await expect(
service.createGroup(groupName, groupDisplayName),
).rejects.toThrow(AlreadyInDBError);
await expect(service.createGroup(groupName, groupDisplayName)).rejects.toThrow(
AlreadyInDBError,
);
});
});
@@ -99,9 +97,7 @@ describe('GroupsService', () => {
it('throws NotInDBError if group not found', async () => {
mockSelect(tracker, [], TableGroup, FieldNameGroup.name, undefined);
await expect(service.getGroupInfoDtoByName(groupName)).rejects.toThrow(
NotInDBError,
);
await expect(service.getGroupInfoDtoByName(groupName)).rejects.toThrow(NotInDBError);
expectBindings(tracker, 'select', [[groupName]], true);
});
});
@@ -111,29 +107,15 @@ describe('GroupsService', () => {
const groupRow = {
[FieldNameGroup.id]: groupId,
};
mockSelect(
tracker,
[FieldNameGroup.id],
TableGroup,
FieldNameGroup.name,
groupRow,
);
mockSelect(tracker, [FieldNameGroup.id], TableGroup, FieldNameGroup.name, groupRow);
const result = await service.getGroupIdByName(groupName);
expect(result).toBe(groupId);
expectBindings(tracker, 'select', [[groupName]], true);
});
it('throws NotInDBError if group not found', async () => {
mockSelect(
tracker,
[FieldNameGroup.id],
TableGroup,
FieldNameGroup.name,
undefined,
);
await expect(service.getGroupIdByName(groupName)).rejects.toThrow(
NotInDBError,
);
mockSelect(tracker, [FieldNameGroup.id], TableGroup, FieldNameGroup.name, undefined);
await expect(service.getGroupIdByName(groupName)).rejects.toThrow(NotInDBError);
expectBindings(tracker, 'select', [[groupName]], true);
});
});
@@ -159,13 +141,7 @@ describe('GroupsService', () => {
};
beforeEach(() => {
mockSelect(
tracker,
[],
TableGroup,
FieldNameGroup.name,
mockEveryoneGroup,
);
mockSelect(tracker, [], TableGroup, FieldNameGroup.name, mockEveryoneGroup);
});
it('returns EVERYONE, LOGGED_IN, and user groups for registered user', async () => {
@@ -184,19 +160,9 @@ describe('GroupsService', () => {
],
);
jest.spyOn(usersService, 'isRegisteredUser').mockResolvedValueOnce(true);
mockSelect(
tracker,
[],
TableGroup,
FieldNameGroup.name,
mockLoggedInGroup,
);
mockSelect(tracker, [], TableGroup, FieldNameGroup.name, mockLoggedInGroup);
const result = await service.getGroupsForUser(123);
expect(result).toEqual([
mockEveryoneGroup,
mockLoggedInGroup,
mockUserGroup1,
]);
expect(result).toEqual([mockEveryoneGroup, mockLoggedInGroup, mockUserGroup1]);
});
it('returns EVERYONE and user groups for unregistered user', async () => {
+5 -20
View File
@@ -100,15 +100,9 @@ export class GroupsService {
* @returns The raw database group object
* @throws NotInDBError if there is no group with this name
*/
private async getRawGroupByName(
name: string,
transaction?: Knex,
): Promise<Group | undefined> {
private async getRawGroupByName(name: string, transaction?: Knex): Promise<Group | undefined> {
const dbActor = transaction ?? this.knex;
return await dbActor(TableGroup)
.select()
.where(FieldNameGroup.name, name)
.first();
return await dbActor(TableGroup).select().where(FieldNameGroup.name, name).first();
}
/**
@@ -129,14 +123,8 @@ export class GroupsService {
});
}
private async innerGetGroupsForUser(
userId: number,
transaction: Knex,
): Promise<Group[]> {
const specialGroupEveryone = await this.getRawGroupByName(
SpecialGroup.EVERYONE,
transaction,
);
private async innerGetGroupsForUser(userId: number, transaction: Knex): Promise<Group[]> {
const specialGroupEveryone = await this.getRawGroupByName(SpecialGroup.EVERYONE, transaction);
if (specialGroupEveryone === undefined) {
throw new NotInDBError(
`Special group '${SpecialGroup.EVERYONE}' not found. Did the database migrations run?`,
@@ -150,10 +138,7 @@ export class GroupsService {
)
.where(`${TableGroupUser}.${FieldNameGroupUser.userId}`, userId)
.select();
const isRegisteredUser = await this.usersService.isRegisteredUser(
userId,
transaction,
);
const isRegisteredUser = await this.usersService.isRegisteredUser(userId, transaction);
if (isRegisteredUser) {
const specialGroupLoggedIn = await this.getRawGroupByName(
SpecialGroup.LOGGED_IN,
@@ -9,9 +9,7 @@ describe('sanitize', () => {
it('removes non-printable ASCII character', () => {
for (let i = 0; i < 32; i++) {
const nonPrintableString = String.fromCharCode(i);
expect(ConsoleLoggerService.sanitize(`a${nonPrintableString}b`)).toEqual(
'ab',
);
expect(ConsoleLoggerService.sanitize(`a${nonPrintableString}b`)).toEqual('ab');
}
});
it('replaces non-zero-width space with space', () => {
+12 -57
View File
@@ -3,13 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
Inject,
Injectable,
LoggerService,
Optional,
Scope,
} from '@nestjs/common';
import { Inject, Injectable, LoggerService, Optional, Scope } from '@nestjs/common';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { blue, cyanBright, green, magentaBright, red, yellow } from 'cli-color';
@@ -20,11 +14,7 @@ import { isDevMode } from '../utils/dev-mode';
import DateTimeFormatOptions = Intl.DateTimeFormatOptions;
const CONTEXTS_TO_IGNORE = [
'RouterExplorer',
'RoutesResolver',
'InstanceLoader',
];
const CONTEXTS_TO_IGNORE = ['RouterExplorer', 'RoutesResolver', 'InstanceLoader'];
@Injectable({ scope: Scope.TRANSIENT })
export class ConsoleLoggerService implements LoggerService {
@@ -52,27 +42,13 @@ export class ConsoleLoggerService implements LoggerService {
this.skipColor = skipColor;
}
error(
message: unknown,
trace = '',
functionContext?: string,
classContext?: string,
): void {
this.printMessage(
message,
red,
this.makeContextString(functionContext, classContext),
false,
);
error(message: unknown, trace = '', functionContext?: string, classContext?: string): void {
this.printMessage(message, red, this.makeContextString(functionContext, classContext), false);
ConsoleLoggerService.printStackTrace(trace);
}
log(message: unknown, functionContext?: string, classContext?: string): void {
if (
!isDevMode() &&
functionContext &&
CONTEXTS_TO_IGNORE.includes(functionContext)
) {
if (!isDevMode() && functionContext && CONTEXTS_TO_IGNORE.includes(functionContext)) {
return;
}
if (needToLog(this.appConfig.log.level, Loglevel.INFO)) {
@@ -85,11 +61,7 @@ export class ConsoleLoggerService implements LoggerService {
}
}
warn(
message: unknown,
functionContext?: string,
classContext?: string,
): void {
warn(message: unknown, functionContext?: string, classContext?: string): void {
if (needToLog(this.appConfig.log.level, Loglevel.WARN)) {
this.printMessage(
message,
@@ -100,11 +72,7 @@ export class ConsoleLoggerService implements LoggerService {
}
}
debug(
message: unknown,
functionContext?: string,
classContext?: string,
): void {
debug(message: unknown, functionContext?: string, classContext?: string): void {
if (needToLog(this.appConfig.log.level, Loglevel.DEBUG)) {
this.printMessage(
message,
@@ -115,11 +83,7 @@ export class ConsoleLoggerService implements LoggerService {
}
}
verbose(
message: unknown,
functionContext?: string,
classContext?: string,
): void {
verbose(message: unknown, functionContext?: string, classContext?: string): void {
if (needToLog(this.appConfig.log.level, Loglevel.TRACE)) {
this.printMessage(
message,
@@ -130,10 +94,7 @@ export class ConsoleLoggerService implements LoggerService {
}
}
private makeContextString(
functionContext?: string,
classContext?: string,
): string {
private makeContextString(functionContext?: string, classContext?: string): string {
let context = classContext ?? this.classContext;
if (!context) {
context = 'HedgeDoc';
@@ -189,23 +150,17 @@ export class ConsoleLoggerService implements LoggerService {
};
let timeString = '';
if (this.appConfig.log.showTimestamp) {
timeString =
new Date(Date.now()).toLocaleString(undefined, localeStringOptions) +
' ';
timeString = new Date(Date.now()).toLocaleString(undefined, localeStringOptions) + ' ';
}
const contextMessage = context ? blue(`[${context}] `) : '';
const timestampDiff = this.updateAndGetTimestampDiff(isTimeDiffEnabled);
process.stdout.write(
`${timeString}${contextMessage}${output}${timestampDiff}\n`,
);
process.stdout.write(`${timeString}${contextMessage}${output}${timestampDiff}\n`);
}
private updateAndGetTimestampDiff(isTimeDiffEnabled?: boolean): string {
const includeTimestamp = this.lastTimestamp && isTimeDiffEnabled;
const result = includeTimestamp
? yellow(` +${Date.now() - this.lastTimestamp}ms`)
: '';
const result = includeTimestamp ? yellow(` +${Date.now() - this.lastTimestamp}ms`) : '';
this.lastTimestamp = Date.now();
return result;
}
@@ -24,10 +24,7 @@ export class MediaRedirectController {
@Get(':uuid')
@OpenApi(302, 404, 500)
async getMedia(
@Param('uuid') uuid: string,
@Res() response: Response,
): Promise<void> {
async getMedia(@Param('uuid') uuid: string, @Res() response: Response): Promise<void> {
const url = await this.mediaService.getFileUrl(uuid);
response.redirect(url);
}
+10 -33
View File
@@ -15,10 +15,7 @@ import { MediaBackendType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { FileTypeResult } from 'file-type';
import mediaConfiguration, {
AzureMediaConfig,
MediaConfig,
} from '../../config/media.config';
import mediaConfiguration, { AzureMediaConfig, MediaConfig } from '../../config/media.config';
import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface';
@@ -40,21 +37,13 @@ export class AzureBackend implements MediaBackend {
return;
}
this.config = this.mediaConfig.backend.azure;
const blobServiceClient = BlobServiceClient.fromConnectionString(
this.config.connectionString,
);
this.credential =
blobServiceClient.credential as StorageSharedKeyCredential;
const blobServiceClient = BlobServiceClient.fromConnectionString(this.config.connectionString);
this.credential = blobServiceClient.credential as StorageSharedKeyCredential;
this.client = blobServiceClient.getContainerClient(this.config.container);
}
async saveFile(
uuid: string,
buffer: Buffer,
fileType: FileTypeResult,
): Promise<null> {
const blockBlobClient: BlockBlobClient =
this.client.getBlockBlobClient(uuid);
async saveFile(uuid: string, buffer: Buffer, fileType: FileTypeResult): Promise<null> {
const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid);
try {
await blockBlobClient.upload(buffer, buffer.length, {
blobHTTPHeaders: {
@@ -64,39 +53,27 @@ export class AzureBackend implements MediaBackend {
this.logger.log(`Uploaded file ${uuid}`, 'saveFile');
return null;
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'saveFile',
);
this.logger.error(`error: ${(e as Error).message}`, (e as Error).stack, 'saveFile');
throw new MediaBackendError(`Could not save file '${uuid}'`);
}
}
async deleteFile(uuid: string, _: unknown): Promise<void> {
const blockBlobClient: BlockBlobClient =
this.client.getBlockBlobClient(uuid);
const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid);
try {
const response = await blockBlobClient.delete();
if (response.errorCode !== undefined) {
throw new MediaBackendError(
`Could not delete '${uuid}': ${response.errorCode}`,
);
throw new MediaBackendError(`Could not delete '${uuid}': ${response.errorCode}`);
}
this.logger.log(`Deleted file ${uuid}`, 'deleteFile');
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'deleteFile',
);
this.logger.error(`error: ${(e as Error).message}`, (e as Error).stack, 'deleteFile');
throw new MediaBackendError(`Could not delete file ${uuid}`);
}
}
getFileUrl(uuid: string, _: unknown): Promise<string> {
const blockBlobClient: BlockBlobClient =
this.client.getBlockBlobClient(uuid);
const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid);
const blobSAS = generateBlobSASQueryParameters(
{
containerName: this.config.container,
@@ -30,16 +30,10 @@ export class FilesystemBackend implements MediaBackend {
return;
}
this.uploadDirectory = this.mediaConfig.backend.filesystem.uploadPath;
this.logger.debug(
`Activated media backend filesystem using ${this.uploadDirectory}`,
);
this.logger.debug(`Activated media backend filesystem using ${this.uploadDirectory}`);
}
async saveFile(
uuid: string,
buffer: Buffer,
fileType: FileTypeResult,
): Promise<string> {
async saveFile(uuid: string, buffer: Buffer, fileType: FileTypeResult): Promise<string> {
const filePath = this.getFilePath(uuid, fileType.ext);
this.logger.debug(`Writing uploaded file to '${filePath}'`, 'saveFile');
await this.ensureDirectory();
@@ -99,14 +93,8 @@ export class FilesystemBackend implements MediaBackend {
);
await fs.mkdir(this.uploadDirectory);
} catch (e) {
this.logger.error(
(e as Error).message,
(e as Error).stack,
'ensureDirectory',
);
throw new MediaBackendError(
`Could not create '${this.uploadDirectory}'`,
);
this.logger.error((e as Error).message, (e as Error).stack, 'ensureDirectory');
throw new MediaBackendError(`Could not create '${this.uploadDirectory}'`);
}
}
}
+8 -22
View File
@@ -8,10 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import fetch, { Response } from 'node-fetch';
import { URLSearchParams } from 'url';
import mediaConfiguration, {
ImgurMediaConfig,
MediaConfig,
} from '../../config/media.config';
import mediaConfiguration, { ImgurMediaConfig, MediaConfig } from '../../config/media.config';
import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface';
@@ -66,11 +63,7 @@ export class ImgurBackend implements MediaBackend {
};
return JSON.stringify(backendData);
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'saveFile',
);
this.logger.error(`error: ${(e as Error).message}`, (e as Error).stack, 'saveFile');
throw new MediaBackendError(`Could not save file ${uuid}`);
}
}
@@ -83,24 +76,17 @@ export class ImgurBackend implements MediaBackend {
);
}
try {
const result = await fetch(
`https://api.imgur.com/3/image/${backendData.deleteHash}`,
{
method: 'DELETE',
// oxlint-disable-next-line @typescript-eslint/naming-convention
headers: { Authorization: `Client-ID ${this.config.clientId}` },
},
);
const result = await fetch(`https://api.imgur.com/3/image/${backendData.deleteHash}`, {
method: 'DELETE',
// oxlint-disable-next-line @typescript-eslint/naming-convention
headers: { Authorization: `Client-ID ${this.config.clientId}` },
});
ImgurBackend.checkStatus(result);
// oxlint-disable-next-line @typescript-eslint/no-base-to-string
this.logger.log(`Deleted file ${uuid}`, 'deleteFile');
return;
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'deleteFile',
);
this.logger.error(`error: ${(e as Error).message}`, (e as Error).stack, 'deleteFile');
throw new MediaBackendError(`Could not delete file '${uuid}'`);
}
}
+11 -19
View File
@@ -35,9 +35,7 @@ describe('s3 backend', () => {
presignedGetObject: jest.fn(),
});
clientConstructorSpy = jest
.spyOn(MinioModule, 'Client')
.mockImplementation(() => mockedClient);
clientConstructorSpy = jest.spyOn(MinioModule, 'Client').mockImplementation(() => mockedClient);
});
function mockMediaConfig(endPoint: string): MediaConfig {
@@ -136,14 +134,12 @@ describe('s3 backend', () => {
describe('save', () => {
it('can save a file', async () => {
const mediaConfig = mockMediaConfig('https://s3.example.org');
const saveSpy = jest
.spyOn(mockedClient, 'putObject')
.mockImplementation(() =>
Promise.resolve({
etag: 'mock',
versionId: 'mock',
}),
);
const saveSpy = jest.spyOn(mockedClient, 'putObject').mockImplementation(() =>
Promise.resolve({
etag: 'mock',
versionId: 'mock',
}),
);
const sut = new S3Backend(mockedLoggerService, mediaConfig);
@@ -179,9 +175,7 @@ describe('s3 backend', () => {
mime: 'image/png',
ext: 'png',
}),
).rejects.toThrow(
'Could not save file cbe87987-8e70-4092-a879-878e70b09245',
);
).rejects.toThrow('Could not save file cbe87987-8e70-4092-a879-878e70b09245');
expect(saveSpy).toHaveBeenCalledWith(
mockedS3Bucket,
@@ -244,11 +238,9 @@ describe('s3 backend', () => {
});
it('throws a MediaBackendError if the client could not generate a signed url', async () => {
const mediaConfig = mockMediaConfig('https://s3.example.org');
const fileUrlSpy = jest
.spyOn(mockedClient, 'presignedGetObject')
.mockImplementation(() => {
throw new Error('mocked error');
});
const fileUrlSpy = jest.spyOn(mockedClient, 'presignedGetObject').mockImplementation(() => {
throw new Error('mocked error');
});
const sut = new S3Backend(mockedLoggerService, mediaConfig);
+6 -19
View File
@@ -9,10 +9,7 @@ import { FileTypeResult } from 'file-type';
import { Client } from 'minio';
import { URL } from 'url';
import mediaConfiguration, {
MediaConfig,
S3MediaConfig,
} from '../../config/media.config';
import mediaConfiguration, { MediaConfig, S3MediaConfig } from '../../config/media.config';
import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface';
@@ -51,22 +48,12 @@ export class S3Backend implements MediaBackend {
});
}
async saveFile(
uuid: string,
buffer: Buffer,
fileType: FileTypeResult,
): Promise<null> {
async saveFile(uuid: string, buffer: Buffer, fileType: FileTypeResult): Promise<null> {
try {
await this.client.putObject(
this.config.bucket,
uuid,
buffer,
buffer.length,
{
// oxlint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': fileType.mime,
},
);
await this.client.putObject(this.config.bucket, uuid, buffer, buffer.length, {
// oxlint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': fileType.mime,
});
this.logger.log(`Uploaded file ${uuid}`, 'saveFile');
return null;
} catch (e) {
+6 -25
View File
@@ -9,10 +9,7 @@ import { FileTypeResult } from 'file-type';
import fetch, { Response } from 'node-fetch';
import { URL } from 'url';
import mediaConfiguration, {
MediaConfig,
WebdavMediaConfig,
} from '../../config/media.config';
import mediaConfiguration, { MediaConfig, WebdavMediaConfig } from '../../config/media.config';
import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface';
@@ -40,10 +37,7 @@ export class WebdavBackend implements MediaBackend {
if (this.config.uploadDir && this.config.uploadDir !== '') {
this.baseUrl = WebdavBackend.joinURL(this.baseUrl, this.config.uploadDir);
}
this.authHeader = WebdavBackend.generateBasicAuthHeader(
url.username,
url.password,
);
this.authHeader = WebdavBackend.generateBasicAuthHeader(url.username, url.password);
fetch(this.baseUrl, {
method: 'PROPFIND',
headers: {
@@ -62,11 +56,7 @@ export class WebdavBackend implements MediaBackend {
});
}
async saveFile(
uuid: string,
buffer: Buffer,
fileType: FileTypeResult,
): Promise<string> {
async saveFile(uuid: string, buffer: Buffer, fileType: FileTypeResult): Promise<string> {
try {
const contentLength = buffer.length;
const remoteFileName = `${uuid}.${fileType.ext}`;
@@ -121,26 +111,17 @@ export class WebdavBackend implements MediaBackend {
if (!file) {
throw new MediaBackendError('No file name in backend data');
}
return Promise.resolve(
WebdavBackend.joinURL(this.config.publicUrl, '/', file),
);
return Promise.resolve(WebdavBackend.joinURL(this.config.publicUrl, '/', file));
}
private static generateBasicAuthHeader(
username: string,
password: string,
): string {
private static generateBasicAuthHeader(username: string, password: string): string {
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
return `Basic ${encoded}`;
}
private static joinURL(...urlParts: Array<string>): string {
return urlParts.reduce((output, next, index) => {
if (
index === 0 ||
next !== '/' ||
(next === '/' && output[output.length - 1] !== '/')
) {
if (index === 0 || next !== '/' || (next === '/' && output[output.length - 1] !== '/')) {
output += next;
}
return output;

Some files were not shown because too many files have changed in this diff Show More