mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-06-23 04:10:17 +00:00
chore(format): reformat using oxfmt
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "all",
|
||||
"ignorePatterns": [
|
||||
"**/*.md"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
@@ -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/',
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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] ?? '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user