refactor(backend): use @hedgedoc/commons DTOs

Co-authored-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2025-03-22 00:38:15 +01:00
parent 7285c2bc50
commit b11dbd51c8
94 changed files with 514 additions and 1642 deletions

View file

@ -1,45 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Type } from 'class-transformer';
import { IsDate, IsNumber, IsOptional, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { TimestampMillis } from '../utils/timestamp';
export class ApiTokenDto extends BaseDto {
@IsString()
label: string;
@IsString()
keyId: string;
@IsDate()
@Type(() => Date)
createdAt: Date;
@IsDate()
@Type(() => Date)
validUntil: Date;
@IsDate()
@Type(() => Date)
@IsOptional()
lastUsedAt: Date | null;
}
export class ApiTokenWithSecretDto extends ApiTokenDto {
@IsString()
secret: string;
}
export class ApiTokenCreateDto extends BaseDto {
@IsString()
label: string;
@IsNumber()
@Type(() => Number)
validUntil: TimestampMillis;
}

View file

@ -1,9 +1,8 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DeepPartial } from '@hedgedoc/commons';
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
@ -150,7 +149,7 @@ describe('ApiTokenService', () => {
const [accessToken, secret] = service.createToken(
user,
'TestToken',
undefined,
null,
);
expect(() =>
@ -158,7 +157,7 @@ describe('ApiTokenService', () => {
).not.toThrow();
});
it('AuthToken has wrong hash', () => {
const [accessToken] = service.createToken(user, 'TestToken', undefined);
const [accessToken] = service.createToken(user, 'TestToken', null);
expect(() =>
service.checkToken('secret', accessToken as ApiToken),
).toThrow(TokenNotValidError);
@ -167,7 +166,7 @@ describe('ApiTokenService', () => {
const [accessToken, secret] = service.createToken(
user,
'Test',
1549312452000,
new Date(1549312452000),
);
expect(() => service.checkToken(secret, accessToken as ApiToken)).toThrow(
TokenNotValidError,
@ -295,18 +294,16 @@ describe('ApiTokenService', () => {
jest
.spyOn(apiTokenRepo, 'save')
.mockImplementationOnce(
async (
apiTokenSaved: DeepPartial<ApiToken>,
_,
): Promise<ApiToken> => {
async (apiTokenSaved: ApiToken, _): Promise<ApiToken> => {
expect(apiTokenSaved.lastUsedAt).toBeNull();
apiTokenSaved.createdAt = new Date(1);
return apiTokenSaved;
},
);
const token = await service.addToken(user, identifier, 0);
const token = await service.addToken(user, identifier, new Date(0));
expect(token.label).toEqual(identifier);
expect(
token.validUntil.getTime() -
new Date(token.validUntil).getTime() -
(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000),
).toBeLessThanOrEqual(10000);
expect(token.lastUsedAt).toBeNull();
@ -317,18 +314,17 @@ describe('ApiTokenService', () => {
jest
.spyOn(apiTokenRepo, 'save')
.mockImplementationOnce(
async (
apiTokenSaved: DeepPartial<ApiToken>,
_,
): Promise<ApiToken> => {
async (apiTokenSaved: ApiToken, _): Promise<ApiToken> => {
expect(apiTokenSaved.lastUsedAt).toBeNull();
apiTokenSaved.createdAt = new Date(1);
return apiTokenSaved;
},
);
const validUntil = new Date().getTime() + 30000;
const validUntil = new Date();
validUntil.setTime(validUntil.getTime() + 30000);
const token = await service.addToken(user, identifier, validUntil);
expect(token.label).toEqual(identifier);
expect(token.validUntil.getTime()).toEqual(validUntil);
expect(new Date(token.validUntil)).toEqual(validUntil);
expect(token.lastUsedAt).toBeNull();
expect(token.secret.startsWith('hd2.' + token.keyId)).toBeTruthy();
});
@ -340,7 +336,8 @@ describe('ApiTokenService', () => {
inValidToken.length = 201;
return inValidToken;
});
const validUntil = new Date().getTime() + 30000;
const validUntil = new Date();
validUntil.setTime(validUntil.getTime() + 30000);
await expect(
service.addToken(user, identifier, validUntil),
).rejects.toThrow(TooManyTokensError);
@ -400,17 +397,17 @@ describe('ApiTokenService', () => {
expect(tokenDto.keyId).toEqual(apiToken.keyId);
expect(tokenDto.lastUsedAt).toBeNull();
expect(tokenDto.label).toEqual(apiToken.label);
expect(tokenDto.validUntil.getTime()).toEqual(
expect(new Date(tokenDto.validUntil).getTime()).toEqual(
apiToken.validUntil.getTime(),
);
expect(tokenDto.createdAt.getTime()).toEqual(
expect(new Date(tokenDto.createdAt).getTime()).toEqual(
apiToken.createdAt.getTime(),
);
});
it('should have lastUsedAt', () => {
apiToken.lastUsedAt = new Date();
const tokenDto = service.toAuthTokenDto(apiToken);
expect(tokenDto.lastUsedAt).toEqual(apiToken.lastUsedAt);
expect(tokenDto.lastUsedAt).toEqual(apiToken.lastUsedAt.toISOString());
});
});
});

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { Cron, Timeout } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
@ -17,8 +18,6 @@ import {
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { User } from '../users/user.entity';
import { bufferToBase64Url } from '../utils/password';
import { TimestampMillis } from '../utils/timestamp';
import { ApiTokenDto, ApiTokenWithSecretDto } from './api-token.dto';
import { ApiToken } from './api-token.entity';
export const AUTH_TOKEN_PREFIX = 'hd2';
@ -54,19 +53,19 @@ export class ApiTokenService {
createToken(
user: User,
identifier: string,
userDefinedValidUntil: TimestampMillis | undefined,
userDefinedValidUntil: Date | null,
): [Omit<ApiToken, 'id' | 'createdAt'>, string] {
const secret = bufferToBase64Url(randomBytes(64));
const keyId = bufferToBase64Url(randomBytes(8));
// More about the choice of SHA-512 in the dev docs
const accessTokenHash = createHash('sha512').update(secret).digest('hex');
// Tokens can only be valid for a maximum of 2 years
const maximumTokenValidity =
new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000;
const maximumTokenValidity = new Date();
maximumTokenValidity.setTime(
maximumTokenValidity.getTime() + 2 * 365 * 24 * 60 * 60 * 1000,
);
const isTokenLimitedToMaximumValidity =
!userDefinedValidUntil ||
userDefinedValidUntil === 0 ||
userDefinedValidUntil > maximumTokenValidity;
!userDefinedValidUntil || userDefinedValidUntil > maximumTokenValidity;
const validUntil = isTokenLimitedToMaximumValidity
? maximumTokenValidity
: userDefinedValidUntil;
@ -83,7 +82,7 @@ export class ApiTokenService {
async addToken(
user: User,
identifier: string,
validUntil: TimestampMillis | undefined,
validUntil: Date | null,
): Promise<ApiTokenWithSecretDto> {
user.apiTokens = this.getTokensByUser(user);
@ -176,13 +175,13 @@ export class ApiTokenService {
const tokenDto: ApiTokenDto = {
label: authToken.label,
keyId: authToken.keyId,
createdAt: authToken.createdAt,
validUntil: authToken.validUntil,
createdAt: authToken.createdAt.toISOString(),
validUntil: authToken.validUntil.toISOString(),
lastUsedAt: null,
};
if (authToken.lastUsedAt) {
tokenDto.lastUsedAt = new Date(authToken.lastUsedAt);
tokenDto.lastUsedAt = new Date(authToken.lastUsedAt).toISOString();
}
return tokenDto;

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -23,7 +23,12 @@ export class MockApiTokenGuard {
try {
this.user = await this.usersService.getUserByUsername('hardcoded');
} catch (e) {
this.user = await this.usersService.createUser('hardcoded', 'Testy');
this.user = await this.usersService.createUser(
'hardcoded',
'Testy',
null,
null,
);
}
}
req.user = this.user;

View file

@ -1,8 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AliasCreateDto } from '@hedgedoc/commons';
import { AliasUpdateDto } from '@hedgedoc/commons';
import { AliasDto } from '@hedgedoc/commons';
import {
BadRequestException,
Body,
@ -18,9 +21,6 @@ import { ApiTags } from '@nestjs/swagger';
import { SessionGuard } from '../../../auth/session.guard';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { AliasCreateDto } from '../../../notes/alias-create.dto';
import { AliasUpdateDto } from '../../../notes/alias-update.dto';
import { AliasDto } from '../../../notes/alias.dto';
import { AliasService } from '../../../notes/alias.service';
import { NotesService } from '../../../notes/notes.service';
import { PermissionsService } from '../../../permissions/permissions.service';

View file

@ -1,8 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
FullUserInfoDto,
LogoutResponseDto,
PendingUserConfirmationDto,
ProviderType,
} from '@hedgedoc/commons';
import {
BadRequestException,
Body,
@ -17,11 +23,8 @@ import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../auth/identity.service';
import { OidcService } from '../../../auth/oidc/oidc.service';
import { PendingUserConfirmationDto } from '../../../auth/pending-user-confirmation.dto';
import { ProviderType } from '../../../auth/provider-type.enum';
import { RequestWithSession, SessionGuard } from '../../../auth/session.guard';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { FullUserInfoDto } from '../../../users/user-info.dto';
import { OpenApi } from '../../utils/openapi.decorator';
@ApiTags('auth')
@ -38,7 +41,7 @@ export class AuthController {
@UseGuards(SessionGuard)
@Delete('logout')
@OpenApi(200, 400, 401)
logout(@Req() request: RequestWithSession): { redirect: string } {
logout(@Req() request: RequestWithSession): LogoutResponseDto {
let logoutUrl: string | null = null;
if (request.session.authProviderType === ProviderType.OIDC) {
logoutUrl = this.oidcService.getLogoutUrl(request);
@ -60,9 +63,7 @@ export class AuthController {
@Get('pending-user')
@OpenApi(200, 400)
getPendingUserData(
@Req() request: RequestWithSession,
): Partial<FullUserInfoDto> {
getPendingUserData(@Req() request: RequestWithSession): FullUserInfoDto {
if (
!request.session.newUserData ||
!request.session.authProviderIdentifier ||

View file

@ -1,8 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
LdapLoginDto,
LdapLoginResponseDto,
ProviderType,
} from '@hedgedoc/commons';
import {
Body,
Controller,
@ -14,14 +19,11 @@ import {
import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../../auth/identity.service';
import { LdapLoginDto } from '../../../../auth/ldap/ldap-login.dto';
import { LdapService } from '../../../../auth/ldap/ldap.service';
import { ProviderType } from '../../../../auth/provider-type.enum';
import { RequestWithSession } from '../../../../auth/session.guard';
import { NotInDBError } from '../../../../errors/errors';
import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
import { UsersService } from '../../../../users/users.service';
import { makeUsernameLowercase } from '../../../../utils/username';
import { OpenApi } from '../../../utils/openapi.decorator';
@ApiTags('auth')
@ -43,7 +45,7 @@ export class LdapController {
request: RequestWithSession,
@Param('ldapIdentifier') ldapIdentifier: string,
@Body() loginDto: LdapLoginDto,
): Promise<{ newUser: boolean }> {
): Promise<LdapLoginResponseDto> {
const ldapConfig = this.ldapService.getLdapConfig(ldapIdentifier);
const userInfo = await this.ldapService.getUserInfoFromLdap(
ldapConfig,
@ -61,7 +63,7 @@ export class LdapController {
);
if (this.identityService.mayUpdateIdentity(ldapIdentifier)) {
const user = await this.usersService.getUserByUsername(
makeUsernameLowercase(loginDto.username),
loginDto.username.toLowerCase(),
);
await this.usersService.updateUser(
user,
@ -70,7 +72,7 @@ export class LdapController {
userInfo.photoUrl,
);
}
request.session.username = makeUsernameLowercase(loginDto.username);
request.session.username = loginDto.username;
return { newUser: false };
} catch (error) {
if (error instanceof NotInDBError) {

View file

@ -1,8 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
LoginDto,
ProviderType,
RegisterDto,
UpdatePasswordDto,
} from '@hedgedoc/commons';
import {
Body,
Controller,
@ -15,10 +21,6 @@ import {
import { ApiTags } from '@nestjs/swagger';
import { LocalService } from '../../../../auth/local/local.service';
import { LoginDto } from '../../../../auth/local/login.dto';
import { RegisterDto } from '../../../../auth/local/register.dto';
import { UpdatePasswordDto } from '../../../../auth/local/update-password.dto';
import { ProviderType } from '../../../../auth/provider-type.enum';
import {
RequestWithSession,
SessionGuard,
@ -53,6 +55,8 @@ export class LocalController {
const user = await this.usersService.createUser(
registerDto.username,
registerDto.displayName,
null,
null,
);
await this.localIdentityService.createLocalIdentity(
user,

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ProviderType } from '@hedgedoc/commons';
import {
Controller,
Get,
@ -17,7 +18,6 @@ import { ApiTags } from '@nestjs/swagger';
import { IdentityService } from '../../../../auth/identity.service';
import { OidcService } from '../../../../auth/oidc/oidc.service';
import { ProviderType } from '../../../../auth/provider-type.enum';
import { RequestWithSession } from '../../../../auth/session.guard';
import { ConsoleLoggerService } from '../../../../logger/console-logger.service';
import { UsersService } from '../../../../users/users.service';

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FrontendConfigDto } from '@hedgedoc/commons';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { FrontendConfigDto } from '../../../frontend-config/frontend-config.dto';
import { FrontendConfigService } from '../../../frontend-config/frontend-config.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { OpenApi } from '../../utils/openapi.decorator';

View file

@ -1,13 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GroupInfoDto } from '@hedgedoc/commons';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { SessionGuard } from '../../../auth/session.guard';
import { GroupInfoDto } from '../../../groups/group-info.dto';
import { GroupsService } from '../../../groups/groups.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { OpenApi } from '../../utils/openapi.decorator';

View file

@ -1,16 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Body, Controller, Delete, Get, Post, UseGuards } from '@nestjs/common';
import { ApiBody, ApiTags } from '@nestjs/swagger';
import {
LoginUserInfoDto,
MediaUploadDto,
ProviderType,
} from '@hedgedoc/commons';
import { Body, Controller, Delete, Get, Put, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { SessionGuard } from '../../../auth/session.guard';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { UserLoginInfoDto } from '../../../users/user-info.dto';
import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service';
import { OpenApi } from '../../utils/openapi.decorator';
@ -34,9 +37,9 @@ export class MeController {
@OpenApi(200)
getMe(
@RequestUser() user: User,
@SessionAuthProvider() authProvider: string,
): UserLoginInfoDto {
return this.userService.toUserLoginInfoDto(user, authProvider);
@SessionAuthProvider() authProvider: ProviderType,
): LoginUserInfoDto {
return this.userService.toLoginUserInfoDto(user, authProvider);
}
@Get('media')
@ -60,18 +63,9 @@ export class MeController {
this.logger.debug(`Deleted ${user.username}`);
}
@Post('profile')
@ApiBody({
schema: {
type: 'object',
properties: {
displayName: { type: 'string', nullable: false },
},
required: ['displayName'],
},
})
@Put('profile')
@OpenApi(200)
async updateDisplayName(
async updateProfile(
@RequestUser() user: User,
@Body('displayName') newDisplayName: string,
): Promise<void> {

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MediaUploadDto, MediaUploadSchema } from '@hedgedoc/commons';
import {
BadRequestException,
Controller,
@ -22,7 +23,6 @@ import { Response } from 'express';
import { SessionGuard } from '../../../auth/session.guard';
import { PermissionError } from '../../../errors/errors';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { MulterFile } from '../../../media/multer-file.interface';
import { Note } from '../../../notes/note.entity';
@ -74,7 +74,7 @@ export class MediaController {
{
code: 201,
description: 'The file was uploaded successfully',
dto: MediaUploadDto,
schema: MediaUploadSchema,
},
400,
403,

View file

@ -1,8 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
ChangeNoteOwnerDto,
MediaUploadDto,
NoteDto,
NoteGroupPermissionEntryDto,
NoteGroupPermissionUpdateDto,
NoteMediaDeletionDto,
NoteMetadataDto,
NotePermissionsDto,
NoteUserPermissionEntryDto,
NoteUserPermissionUpdateDto,
RevisionDto,
RevisionMetadataDto,
} from '@hedgedoc/commons';
import {
BadRequestException,
Body,
@ -22,24 +36,16 @@ import { NotInDBError } from '../../../errors/errors';
import { GroupsService } from '../../../groups/groups.service';
import { HistoryService } from '../../../history/history.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
import { NotePermissionsDto } from '../../../notes/note-permissions.dto';
import { NoteDto } from '../../../notes/note.dto';
import { Note } from '../../../notes/note.entity';
import { NoteMediaDeletionDto } from '../../../notes/note.media-deletion.dto';
import { NotesService } from '../../../notes/notes.service';
import { PermissionsGuard } from '../../../permissions/permissions.guard';
import { PermissionsService } from '../../../permissions/permissions.service';
import { RequirePermission } from '../../../permissions/require-permission.decorator';
import { RequiredPermission } from '../../../permissions/required-permission.enum';
import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto';
import { RevisionDto } from '../../../revisions/revision.dto';
import { RevisionsService } from '../../../revisions/revisions.service';
import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
import { MarkdownBody } from '../../utils/markdown-body.decorator';
import { OpenApi } from '../../utils/openapi.decorator';
@ -197,15 +203,15 @@ export class NotesController {
);
}
@Put(':noteIdOrAlias/metadata/permissions/users/:userName')
@Put(':noteIdOrAlias/metadata/permissions/users/:username')
@OpenApi(200, 403, 404)
@UseInterceptors(GetNoteInterceptor)
@RequirePermission(RequiredPermission.OWNER)
async setUserPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('userName') username: Username,
@Body('canEdit') canEdit: boolean,
@Param('username') username: NoteUserPermissionUpdateDto['username'],
@Body('canEdit') canEdit: NoteUserPermissionUpdateDto['canEdit'],
): Promise<NotePermissionsDto> {
const permissionUser = await this.userService.getUserByUsername(username);
const returnedNote = await this.permissionService.setUserPermission(
@ -218,11 +224,11 @@ export class NotesController {
@UseInterceptors(GetNoteInterceptor)
@RequirePermission(RequiredPermission.OWNER)
@Delete(':noteIdOrAlias/metadata/permissions/users/:userName')
@Delete(':noteIdOrAlias/metadata/permissions/users/:username')
async removeUserPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('userName') username: Username,
@Param('username') username: NoteUserPermissionEntryDto['username'],
): Promise<NotePermissionsDto> {
try {
const permissionUser = await this.userService.getUserByUsername(username);
@ -247,8 +253,8 @@ export class NotesController {
async setGroupPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('groupName') groupName: string,
@Body('canEdit') canEdit: boolean,
@Param('groupName') groupName: NoteGroupPermissionUpdateDto['groupName'],
@Body('canEdit') canEdit: NoteGroupPermissionUpdateDto['canEdit'],
): Promise<NotePermissionsDto> {
const permissionGroup = await this.groupService.getGroupByName(groupName);
const returnedNote = await this.permissionService.setGroupPermission(
@ -266,7 +272,7 @@ export class NotesController {
async removeGroupPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('groupName') groupName: string,
@Param('groupName') groupName: NoteGroupPermissionEntryDto['groupName'],
): Promise<NotePermissionsDto> {
const permissionGroup = await this.groupService.getGroupByName(groupName);
const returnedNote = await this.permissionService.removeGroupPermission(
@ -282,9 +288,11 @@ export class NotesController {
async changeOwner(
@RequestUser() user: User,
@RequestNote() note: Note,
@Body('newOwner') newOwner: Username,
@Body() changeNoteOwnerDto: ChangeNoteOwnerDto,
): Promise<NoteDto> {
const owner = await this.userService.getUserByUsername(newOwner);
const owner = await this.userService.getUserByUsername(
changeNoteOwnerDto.owner,
);
return await this.noteService.toNoteDto(
await this.permissionService.changeOwner(note, owner),
);

View file

@ -1,8 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
ApiTokenCreateDto,
ApiTokenDto,
ApiTokenWithSecretDto,
} from '@hedgedoc/commons';
import {
Body,
Controller,
@ -15,11 +20,6 @@ import {
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
ApiTokenCreateDto,
ApiTokenDto,
ApiTokenWithSecretDto,
} from '../../../api-token/api-token.dto';
import { ApiTokenService } from '../../../api-token/api-token.service';
import { SessionGuard } from '../../../auth/session.guard';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';

View file

@ -1,19 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
UserInfoDto,
UsernameCheckDto,
UsernameCheckResponseDto,
} from '@hedgedoc/commons';
import { Body, Controller, Get, HttpCode, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { UserInfoDto } from '../../../users/user-info.dto';
import {
UsernameCheckDto,
UsernameCheckResponseDto,
} from '../../../users/username-check.dto';
import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { OpenApi } from '../../utils/openapi.decorator';
@ApiTags('users')
@ -41,7 +40,7 @@ export class UsersController {
@Get('profile/:username')
@OpenApi(200)
async getUser(@Param('username') username: Username): Promise<UserInfoDto> {
async getUser(@Param('username') username: string): Promise<UserInfoDto> {
return this.userService.toUserDto(
await this.userService.getUserByUsername(username),
);

View file

@ -1,8 +1,14 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
AliasCreateDto,
AliasDto,
AliasSchema,
AliasUpdateDto,
} from '@hedgedoc/commons';
import {
BadRequestException,
Body,
@ -18,9 +24,6 @@ import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiTokenGuard } from '../../../api-token/api-token.guard';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { AliasCreateDto } from '../../../notes/alias-create.dto';
import { AliasUpdateDto } from '../../../notes/alias-update.dto';
import { AliasDto } from '../../../notes/alias.dto';
import { AliasService } from '../../../notes/alias.service';
import { NotesService } from '../../../notes/notes.service';
import { PermissionsService } from '../../../permissions/permissions.service';
@ -48,7 +51,7 @@ export class AliasController {
{
code: 200,
description: 'The new alias',
dto: AliasDto,
schema: AliasSchema,
},
403,
404,
@ -75,7 +78,7 @@ export class AliasController {
{
code: 200,
description: 'The updated alias',
dto: AliasDto,
schema: AliasSchema,
},
403,
404,
@ -110,7 +113,7 @@ export class AliasController {
)
async removeAlias(
@RequestUser() user: User,
@Param('alias') alias: string,
@Param('alias') alias: AliasDto['name'],
): Promise<void> {
const note = await this.noteService.getNoteByIdOrAlias(alias);
if (!(await this.permissionsService.isOwner(user, note))) {

View file

@ -1,8 +1,16 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
FullUserInfoDto,
FullUserInfoSchema,
MediaUploadDto,
MediaUploadSchema,
NoteMetadataDto,
NoteMetadataSchema,
} from '@hedgedoc/commons';
import {
Body,
Controller,
@ -19,12 +27,9 @@ import { HistoryEntryUpdateDto } from '../../../history/history-entry-update.dto
import { HistoryEntryDto } from '../../../history/history-entry.dto';
import { HistoryService } from '../../../history/history.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
import { Note } from '../../../notes/note.entity';
import { NotesService } from '../../../notes/notes.service';
import { FullUserInfoDto } from '../../../users/user-info.dto';
import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service';
import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
@ -52,7 +57,7 @@ export class MeController {
@OpenApi({
code: 200,
description: 'The user information',
dto: FullUserInfoDto,
schema: FullUserInfoSchema,
})
getMe(@RequestUser() user: User): FullUserInfoDto {
return this.usersService.toFullUserDto(user);
@ -63,7 +68,6 @@ export class MeController {
code: 200,
description: 'The history entries of the user',
isArray: true,
dto: HistoryEntryDto,
})
async getUserHistory(@RequestUser() user: User): Promise<HistoryEntryDto[]> {
const foundEntries = await this.historyService.getEntriesByUser(user);
@ -78,7 +82,6 @@ export class MeController {
{
code: 200,
description: 'The history entry of the user which points to the note',
dto: HistoryEntryDto,
},
404,
)
@ -96,7 +99,6 @@ export class MeController {
{
code: 200,
description: 'The updated history entry',
dto: HistoryEntryDto,
},
404,
)
@ -125,7 +127,7 @@ export class MeController {
code: 200,
description: 'Metadata of all notes of the user',
isArray: true,
dto: NoteMetadataDto,
schema: NoteMetadataSchema,
})
async getMyNotes(@RequestUser() user: User): Promise<NoteMetadataDto[]> {
const notes = this.notesService.getUserNotes(user);
@ -139,7 +141,7 @@ export class MeController {
code: 200,
description: 'All media uploads of the user',
isArray: true,
dto: MediaUploadDto,
schema: MediaUploadSchema,
})
async getMyMedia(@RequestUser() user: User): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByUser(user);

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MediaUploadDto, MediaUploadSchema } from '@hedgedoc/commons';
import {
BadRequestException,
Controller,
@ -28,7 +29,6 @@ import { Response } from 'express';
import { ApiTokenGuard } from '../../../api-token/api-token.guard';
import { PermissionError } from '../../../errors/errors';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { MulterFile } from '../../../media/multer-file.interface';
import { Note } from '../../../notes/note.entity';
@ -77,7 +77,7 @@ export class MediaController {
{
code: 201,
description: 'The file was uploaded successfully',
dto: MediaUploadDto,
schema: MediaUploadSchema,
},
400,
403,

View file

@ -1,14 +1,14 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ServerStatusDto, ServerStatusSchema } from '@hedgedoc/commons';
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiTokenGuard } from '../../../api-token/api-token.guard';
import { MonitoringService } from '../../../monitoring/monitoring.service';
import { ServerStatusDto } from '../../../monitoring/server-status.dto';
import { OpenApi } from '../../utils/openapi.decorator';
@UseGuards(ApiTokenGuard)
@ -24,7 +24,7 @@ export class MonitoringController {
{
code: 200,
description: 'The server info',
dto: ServerStatusDto,
schema: ServerStatusSchema,
},
403,
)

View file

@ -1,8 +1,24 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
MediaUploadDto,
MediaUploadSchema,
NoteDto,
NoteMediaDeletionDto,
NoteMetadataDto,
NoteMetadataSchema,
NotePermissionsDto,
NotePermissionsSchema,
NotePermissionsUpdateDto,
NoteSchema,
RevisionDto,
RevisionMetadataDto,
RevisionMetadataSchema,
RevisionSchema,
} from '@hedgedoc/commons';
import {
BadRequestException,
Body,
@ -22,27 +38,16 @@ import { NotInDBError } from '../../../errors/errors';
import { GroupsService } from '../../../groups/groups.service';
import { HistoryService } from '../../../history/history.service';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
import { MediaService } from '../../../media/media.service';
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
import {
NotePermissionsDto,
NotePermissionsUpdateDto,
} from '../../../notes/note-permissions.dto';
import { NoteDto } from '../../../notes/note.dto';
import { Note } from '../../../notes/note.entity';
import { NoteMediaDeletionDto } from '../../../notes/note.media-deletion.dto';
import { NotesService } from '../../../notes/notes.service';
import { PermissionsGuard } from '../../../permissions/permissions.guard';
import { PermissionsService } from '../../../permissions/permissions.service';
import { RequirePermission } from '../../../permissions/require-permission.decorator';
import { RequiredPermission } from '../../../permissions/required-permission.enum';
import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto';
import { RevisionDto } from '../../../revisions/revision.dto';
import { RevisionsService } from '../../../revisions/revisions.service';
import { User } from '../../../users/user.entity';
import { UsersService } from '../../../users/users.service';
import { Username } from '../../../utils/username';
import { GetNoteInterceptor } from '../../utils/get-note.interceptor';
import { MarkdownBody } from '../../utils/markdown-body.decorator';
import { OpenApi } from '../../utils/openapi.decorator';
@ -88,7 +93,7 @@ export class NotesController {
{
code: 200,
description: 'Get information about the newly created note',
dto: NoteDto,
schema: NoteSchema,
},
403,
404,
@ -107,7 +112,7 @@ export class NotesController {
{
code: 201,
description: 'Get information about the newly created note',
dto: NoteDto,
schema: NoteSchema,
},
400,
403,
@ -155,7 +160,7 @@ export class NotesController {
{
code: 200,
description: 'The new, changed note',
dto: NoteDto,
schema: NoteSchema,
},
403,
404,
@ -197,7 +202,7 @@ export class NotesController {
{
code: 200,
description: 'The metadata of the note',
dto: NoteMetadataDto,
schema: NoteMetadataSchema,
},
403,
404,
@ -216,7 +221,7 @@ export class NotesController {
{
code: 200,
description: 'The updated permissions of the note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -238,7 +243,7 @@ export class NotesController {
{
code: 200,
description: 'Get the permissions for a note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -257,7 +262,7 @@ export class NotesController {
{
code: 200,
description: 'Set the permissions for a user on a note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -265,7 +270,7 @@ export class NotesController {
async setUserPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('userName') username: Username,
@Param('userName') username: string,
@Body('canEdit') canEdit: boolean,
): Promise<NotePermissionsDto> {
const permissionUser = await this.userService.getUserByUsername(username);
@ -284,7 +289,7 @@ export class NotesController {
{
code: 200,
description: 'Remove the permission for a user on a note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -292,7 +297,7 @@ export class NotesController {
async removeUserPermission(
@RequestUser() user: User,
@RequestNote() note: Note,
@Param('userName') username: Username,
@Param('userName') username: string,
): Promise<NotePermissionsDto> {
try {
const permissionUser = await this.userService.getUserByUsername(username);
@ -318,7 +323,7 @@ export class NotesController {
{
code: 200,
description: 'Set the permissions for a group on a note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -345,7 +350,7 @@ export class NotesController {
{
code: 200,
description: 'Remove the permission for a group on a note',
dto: NotePermissionsDto,
schema: NotePermissionsSchema,
},
403,
404,
@ -370,7 +375,7 @@ export class NotesController {
{
code: 200,
description: 'Changes the owner of the note',
dto: NoteDto,
schema: NoteSchema,
},
403,
404,
@ -378,7 +383,7 @@ export class NotesController {
async changeOwner(
@RequestUser() user: User,
@RequestNote() note: Note,
@Body('newOwner') newOwner: Username,
@Body('newOwner') newOwner: string,
): Promise<NoteDto> {
const owner = await this.userService.getUserByUsername(newOwner);
return await this.noteService.toNoteDto(
@ -394,7 +399,7 @@ export class NotesController {
code: 200,
description: 'Revisions of the note',
isArray: true,
dto: RevisionMetadataDto,
schema: RevisionMetadataSchema,
},
403,
404,
@ -418,7 +423,7 @@ export class NotesController {
{
code: 200,
description: 'Revision of the note for the given id or alias',
dto: RevisionDto,
schema: RevisionSchema,
},
403,
404,
@ -440,7 +445,7 @@ export class NotesController {
code: 200,
description: 'All media uploads of the note',
isArray: true,
dto: MediaUploadDto,
schema: MediaUploadSchema,
})
async getNotesMedia(
@RequestUser() user: User,

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -14,10 +14,12 @@ import {
ApiNotFoundResponse,
ApiOkResponse,
ApiProduces,
ApiResponseNoStatusOptions,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { zodToOpenAPI } from 'nestjs-zod';
import { ZodSchema } from 'zod';
import { BaseDto } from '../../utils/base.dto.';
import {
badRequestDescription,
conflictDescription,
@ -57,7 +59,7 @@ export interface HttpStatusCodeWithExtraInformation {
code: HttpStatusCodes;
description?: string;
isArray?: boolean;
dto?: BaseDto;
schema?: ZodSchema;
mimeType?: string;
}
@ -89,7 +91,8 @@ export const OpenApi = (
let code: HttpStatusCodes = 200;
let description: string | undefined = undefined;
let isArray: boolean | undefined = undefined;
let dto: BaseDto | undefined = undefined;
let schema: ZodSchema | undefined = undefined;
if (typeof entry == 'number') {
code = entry;
} else {
@ -97,7 +100,7 @@ export const OpenApi = (
code = entry.code;
description = entry.description;
isArray = entry.isArray;
dto = entry.dto;
schema = entry.schema;
if (entry.mimeType) {
decoratorsToApply.push(
ApiProduces(entry.mimeType),
@ -105,22 +108,32 @@ export const OpenApi = (
);
}
}
let defaultResponseObject: ApiResponseNoStatusOptions = {
description: description ?? createdDescription,
isArray: isArray,
};
if (schema) {
defaultResponseObject = {
...defaultResponseObject,
schema: zodToOpenAPI(schema),
};
}
switch (code) {
case 200:
decoratorsToApply.push(
ApiOkResponse({
...defaultResponseObject,
description: description ?? okDescription,
isArray: isArray,
type: dto ? (): BaseDto => dto : undefined,
}),
);
break;
case 201:
decoratorsToApply.push(
ApiCreatedResponse({
...defaultResponseObject,
description: description ?? createdDescription,
isArray: isArray,
type: dto ? (): BaseDto => dto : undefined,
}),
HttpCode(201),
);

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ProviderType } from '@hedgedoc/commons';
import {
Column,
CreateDateColumn,
@ -13,7 +14,6 @@ import {
} from 'typeorm';
import { User } from '../users/user.entity';
import { ProviderType } from './provider-type.enum';
/**
* The identity represents a single way for a user to login.

View file

@ -1,8 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
FullUserInfoDto,
PendingUserConfirmationDto,
ProviderType,
} from '@hedgedoc/commons';
import {
Inject,
Injectable,
@ -14,12 +19,9 @@ import { DataSource, Repository } from 'typeorm';
import AuthConfiguration, { AuthConfig } from '../config/auth.config';
import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { FullUserInfoDto } from '../users/user-info.dto';
import { User } from '../users/user.entity';
import { UsersService } from '../users/users.service';
import { Identity } from './identity.entity';
import { PendingUserConfirmationDto } from './pending-user-confirmation.dto';
import { ProviderType } from './provider-type.enum';
@Injectable()
export class IdentityService {

View file

@ -1,13 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class LdapLoginDto {
@IsString()
username: string; // This is not of type Username, because LDAP server may use mixed case usernames
@IsString()
password: string;
}

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FullUserInfoWithIdDto } from '@hedgedoc/commons';
import {
Inject,
Injectable,
@ -18,8 +19,6 @@ import authConfiguration, {
LDAPConfig,
} from '../../config/auth.config';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { FullUserInfoWithIdDto } from '../../users/user-info.dto';
import { Username } from '../../utils/username';
const LDAP_ERROR_MAP: Record<string, string> = {
/* eslint-disable @typescript-eslint/naming-convention */
@ -96,7 +95,7 @@ export class LdapService {
return reject(new UnauthorizedException(LDAP_ERROR_MAP['default']));
}
let email: string | undefined = undefined;
let email: string | null = null;
if (userInfo['mail']) {
if (Array.isArray(userInfo['mail'])) {
email = userInfo['mail'][0] as string;
@ -107,10 +106,10 @@ export class LdapService {
return resolve({
email,
username: username as Username,
username: username,
id: userInfo[ldapConfig.userIdField],
displayName: userInfo[ldapConfig.displayNameField] ?? username,
photoUrl: undefined, // TODO LDAP stores images as binaries,
photoUrl: null, // TODO LDAP stores images as binaries,
// we need to convert them into a data-URL or alike
});
},

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ProviderType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
@ -32,7 +33,6 @@ import { User } from '../../users/user.entity';
import { checkPassword, hashPassword } from '../../utils/password';
import { Identity } from '../identity.entity';
import { IdentityService } from '../identity.service';
import { ProviderType } from '../provider-type.enum';
@Injectable()
export class LocalService {

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Type } from 'class-transformer';
import { IsLowercase, IsString } from 'class-validator';
import { Username } from '../../utils/username';
export class LoginDto {
@Type(() => String)
@IsString()
@IsLowercase()
username: Username;
@IsString()
password: string;
}

View file

@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Type } from 'class-transformer';
import { IsLowercase, IsString } from 'class-validator';
import { Username } from '../../utils/username';
export class RegisterDto {
@Type(() => String)
@IsString()
@IsLowercase()
username: Username;
@IsString()
displayName: string;
@IsString()
password: string;
}

View file

@ -1,13 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class UpdatePasswordDto {
@IsString()
currentPassword: string;
@IsString()
newPassword: string;
}

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FullUserInfoDto, ProviderType } from '@hedgedoc/commons';
import {
ForbiddenException,
Inject,
@ -20,10 +21,8 @@ import authConfiguration, {
} from '../../config/auth.config';
import { NotInDBError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { FullUserInfoDto } from '../../users/user-info.dto';
import { Identity } from '../identity.entity';
import { IdentityService } from '../identity.service';
import { ProviderType } from '../provider-type.enum';
import { RequestWithSession } from '../session.guard';
interface OidcClientConfigEntry {
@ -227,8 +226,8 @@ export class OidcService {
const newUserData = {
username,
displayName,
photoUrl,
email,
photoUrl: photoUrl ?? null,
email: email ?? null,
};
request.session.providerUserId = userId;
request.session.newUserData = newUserData;

View file

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsOptional, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class PendingUserConfirmationDto extends BaseDto {
@IsString()
username: string;
@IsString()
displayName: string;
@IsOptional()
@IsString()
profilePicture: string | undefined;
}

View file

@ -1,8 +1,10 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ProviderType } from '@hedgedoc/commons';
import { GuestAccess } from '@hedgedoc/commons';
import {
CanActivate,
ExecutionContext,
@ -13,13 +15,11 @@ import {
import { Request } from 'express';
import { CompleteRequest } from '../api/utils/request.type';
import { GuestAccess } from '../config/guest_access.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { SessionState } from '../sessions/session.service';
import { UsersService } from '../users/users.service';
import { ProviderType } from './provider-type.enum';
export type RequestWithSession = Request & {
session: SessionState;

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -10,24 +10,24 @@ import { buildErrorMessage } from './utils';
export interface CustomizationConfig {
branding: {
customName: string;
customLogo: string;
customName: string | null;
customLogo: string | null;
};
specialUrls: {
privacy: string;
termsOfUse: string;
imprint: string;
privacy: string | null;
termsOfUse: string | null;
imprint: string | null;
};
}
const schema = Joi.object({
branding: Joi.object({
customName: Joi.string().optional().label('HD_CUSTOM_NAME'),
customName: Joi.string().allow(null).label('HD_CUSTOM_NAME'),
customLogo: Joi.string()
.uri({
scheme: [/https?/],
})
.optional()
.allow(null)
.label('HD_CUSTOM_LOGO'),
}),
specialUrls: Joi.object({
@ -35,19 +35,19 @@ const schema = Joi.object({
.uri({
scheme: /https?/,
})
.optional()
.allow(null)
.label('HD_PRIVACY_URL'),
termsOfUse: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.allow(null)
.label('HD_TERMS_OF_USE_URL'),
imprint: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.allow(null)
.label('HD_IMPRINT_URL'),
}),
});
@ -56,13 +56,13 @@ export default registerAs('customizationConfig', () => {
const customizationConfig = schema.validate(
{
branding: {
customName: process.env.HD_CUSTOM_NAME,
customLogo: process.env.HD_CUSTOM_LOGO,
customName: process.env.HD_CUSTOM_NAME ?? null,
customLogo: process.env.HD_CUSTOM_LOGO ?? null,
},
specialUrls: {
privacy: process.env.HD_PRIVACY_URL,
termsOfUse: process.env.HD_TERMS_OF_USE_URL,
imprint: process.env.HD_IMPRINT_URL,
privacy: process.env.HD_PRIVACY_URL ?? null,
termsOfUse: process.env.HD_TERMS_OF_USE_URL ?? null,
imprint: process.env.HD_IMPRINT_URL ?? null,
},
},
{

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -9,7 +9,7 @@ import * as Joi from 'joi';
import { buildErrorMessage } from './utils';
export interface ExternalServicesConfig {
plantUmlServer: string;
plantUmlServer: string | null;
imageProxy: string;
}
@ -18,7 +18,7 @@ const schema = Joi.object({
.uri({
scheme: /https?/,
})
.optional()
.allow(null)
.label('HD_PLANTUML_SERVER'),
imageProxy: Joi.string()
.uri({
@ -36,7 +36,7 @@ export default registerAs('externalServicesConfig', () => {
}
const externalConfig = schema.validate(
{
plantUmlServer: process.env.HD_PLANTUML_SERVER,
plantUmlServer: process.env.HD_PLANTUML_SERVER ?? null,
imageProxy: process.env.HD_IMAGE_PROXY,
},
{

View file

@ -1,13 +1,13 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess } from '@hedgedoc/commons';
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { DefaultAccessLevel } from '../default-access-level.enum';
import { GuestAccess } from '../guest_access.enum';
import { NoteConfig } from '../note.config';
export function createDefaultMockNoteConfig(): NoteConfig {

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess } from '@hedgedoc/commons';
import mockedEnv from 'mocked-env';
import { DefaultAccessLevel } from './default-access-level.enum';
import { GuestAccess } from './guest_access.enum';
import noteConfig from './note.config';
describe('noteConfig', () => {

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess } from '@hedgedoc/commons';
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
@ -10,7 +11,6 @@ import {
DefaultAccessLevel,
getDefaultAccessLevelOrdinal,
} from './default-access-level.enum';
import { GuestAccess } from './guest_access.enum';
import { buildErrorMessage, parseOptionalNumber, toArrayConfig } from './utils';
export interface NoteConfig {

View file

@ -1,194 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsNumber,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from 'class-validator';
import { URL } from 'url';
import { ProviderType } from '../auth/provider-type.enum';
import { GuestAccess } from '../config/guest_access.enum';
import { ServerVersion } from '../monitoring/server-status.dto';
import { BaseDto } from '../utils/base.dto.';
export type AuthProviderTypeWithCustomName =
| ProviderType.LDAP
| ProviderType.OIDC;
export type AuthProviderTypeWithoutCustomName = ProviderType.LOCAL;
export class AuthProviderWithoutCustomNameDto extends BaseDto {
/**
* The type of the auth provider.
*/
@IsString()
@Type(() => String)
type: AuthProviderTypeWithoutCustomName;
}
export class AuthProviderWithCustomNameDto extends BaseDto {
/**
* The type of the auth provider.
*/
@IsString()
@Type(() => String)
type: AuthProviderTypeWithCustomName;
/**
* The identifier with which the auth provider can be called
* @example gitlab-fsorg
*/
@IsString()
identifier: string;
/**
* The name given to the auth provider
* @example GitLab fachschaften.org
*/
@IsString()
providerName: string;
/**
* The theme to apply for the login button.
* @example gitlab
*/
@IsOptional()
@IsString()
theme?: string;
}
export type AuthProviderDto =
| AuthProviderWithCustomNameDto
| AuthProviderWithoutCustomNameDto;
export class BrandingDto extends BaseDto {
/**
* The name to be displayed next to the HedgeDoc logo
* @example ACME Corp
*/
@IsString()
@IsOptional()
name?: string;
/**
* The logo to be displayed next to the HedgeDoc logo
* @example https://md.example.com/logo.png
*/
@IsUrl()
@IsOptional()
@Type(() => URL)
logo?: URL;
}
export class SpecialUrlsDto extends BaseDto {
/**
* A link to the privacy notice
* @example https://md.example.com/n/privacy
*/
@IsUrl()
@IsOptional()
@Type(() => URL)
privacy?: URL;
/**
* A link to the terms of use
* @example https://md.example.com/n/termsOfUse
*/
@IsUrl()
@IsOptional()
@Type(() => URL)
termsOfUse?: URL;
/**
* A link to the imprint
* @example https://md.example.com/n/imprint
*/
@IsUrl()
@IsOptional()
@Type(() => URL)
imprint?: URL;
}
export class FrontendConfigDto extends BaseDto {
/**
* Maximum access level for guest users
*/
@IsString()
guestAccess: GuestAccess;
/**
* Are users allowed to register on this instance?
*/
@IsBoolean()
allowRegister: boolean;
/**
* Are users allowed to edit their profile information?
*/
@IsBoolean()
allowProfileEdits: boolean;
/**
* Are users allowed to choose their username when signing up via OIDC?
*/
@IsBoolean()
allowChooseUsername: boolean;
/**
* Which auth providers are enabled and how are they configured?
*/
// eslint-disable-next-line @darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator
@IsArray()
@ValidateNested({ each: true })
authProviders: AuthProviderDto[];
/**
* Individual branding information
*/
@ValidateNested()
@Type(() => BrandingDto)
branding: BrandingDto;
/**
* Is an image proxy enabled?
*/
@IsBoolean()
useImageProxy: boolean;
/**
* Links to some special pages
*/
@ValidateNested()
@Type(() => SpecialUrlsDto)
specialUrls: SpecialUrlsDto;
/**
* The version of HedgeDoc
*/
@ValidateNested()
@Type(() => ServerVersion)
version: ServerVersion;
/**
* The plantUML server that should be used to render.
*/
@IsUrl()
@IsOptional()
@Type(() => URL)
plantUmlServer?: URL;
/**
* The maximal length of each document
*/
@IsNumber()
maxDocumentLength: number;
}

View file

@ -1,19 +1,18 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess, ProviderType } from '@hedgedoc/commons';
import { ConfigModule, registerAs } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { URL } from 'url';
import { ProviderType } from '../auth/provider-type.enum';
import { AppConfig } from '../config/app.config';
import { AuthConfig } from '../config/auth.config';
import { CustomizationConfig } from '../config/customization.config';
import { DefaultAccessLevel } from '../config/default-access-level.enum';
import { ExternalServicesConfig } from '../config/external-services.config';
import { GuestAccess } from '../config/guest_access.enum';
import { Loglevel } from '../config/loglevel.enum';
import { NoteConfig } from '../config/note.config';
import { LoggerModule } from '../logger/logger.module';
@ -174,14 +173,11 @@ describe('FrontendConfigService', () => {
const customName = 'Test Branding Name';
let index = 1;
for (const customLogo of [undefined, 'https://example.com/logo.png']) {
for (const privacyLink of [undefined, 'https://example.com/privacy']) {
for (const termsOfUseLink of [undefined, 'https://example.com/terms']) {
for (const imprintLink of [undefined, 'https://example.com/imprint']) {
for (const plantUmlServer of [
undefined,
'https://plantuml.example.com',
]) {
for (const customLogo of [null, 'https://example.com/logo.png']) {
for (const privacyLink of [null, 'https://example.com/privacy']) {
for (const termsOfUseLink of [null, 'https://example.com/terms']) {
for (const imprintLink of [null, 'https://example.com/imprint']) {
for (const plantUmlServer of [null, 'https://plantuml.example.com']) {
it(`combination #${index} works`, async () => {
const appConfig: AppConfig = {
baseUrl: domain,
@ -255,20 +251,24 @@ describe('FrontendConfigService', () => {
expect(config.guestAccess).toEqual(noteConfig.guestAccess);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo ? new URL(customLogo) : undefined,
customLogo !== null ? new URL(customLogo).toString() : null,
);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.plantUmlServer).toEqual(
plantUmlServer ? new URL(plantUmlServer) : undefined,
plantUmlServer !== null
? new URL(plantUmlServer).toString()
: null,
);
expect(config.specialUrls.imprint).toEqual(
imprintLink ? new URL(imprintLink) : undefined,
imprintLink !== null ? new URL(imprintLink).toString() : null,
);
expect(config.specialUrls.privacy).toEqual(
privacyLink ? new URL(privacyLink) : undefined,
privacyLink !== null ? new URL(privacyLink).toString() : null,
);
expect(config.specialUrls.termsOfUse).toEqual(
termsOfUseLink ? new URL(termsOfUseLink) : undefined,
termsOfUseLink !== null
? new URL(termsOfUseLink).toString()
: null,
);
expect(config.useImageProxy).toEqual(!!imageProxy);
expect(config.version).toEqual(

View file

@ -1,12 +1,18 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
BrandingDto,
FrontendConfigDto,
ProviderType,
SpecialUrlDto,
} from '@hedgedoc/commons';
import { AuthProviderDto } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { URL } from 'url';
import { ProviderType } from '../auth/provider-type.enum';
import appConfiguration, { AppConfig } from '../config/app.config';
import authConfiguration, { AuthConfig } from '../config/auth.config';
import customizationConfiguration, {
@ -18,12 +24,6 @@ import externalServicesConfiguration, {
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { getServerVersionFromPackageJson } from '../utils/serverVersion';
import {
AuthProviderDto,
BrandingDto,
FrontendConfigDto,
SpecialUrlsDto,
} from './frontend-config.dto';
@Injectable()
export class FrontendConfigService {
@ -53,8 +53,8 @@ export class FrontendConfigService {
branding: this.getBranding(),
maxDocumentLength: this.noteConfig.maxDocumentLength,
plantUmlServer: this.externalServicesConfig.plantUmlServer
? new URL(this.externalServicesConfig.plantUmlServer)
: undefined,
? new URL(this.externalServicesConfig.plantUmlServer).toString()
: null,
specialUrls: this.getSpecialUrls(),
useImageProxy: !!this.externalServicesConfig.imageProxy,
version: await getServerVersionFromPackageJson(),
@ -73,6 +73,7 @@ export class FrontendConfigService {
type: ProviderType.LDAP,
providerName: ldapEntry.providerName,
identifier: ldapEntry.identifier,
theme: null,
});
});
this.authConfig.oidc.forEach((openidConnectEntry) => {
@ -80,7 +81,7 @@ export class FrontendConfigService {
type: ProviderType.OIDC,
providerName: openidConnectEntry.providerName,
identifier: openidConnectEntry.identifier,
theme: openidConnectEntry.theme,
theme: openidConnectEntry.theme ?? null,
});
});
return providers;
@ -89,23 +90,23 @@ export class FrontendConfigService {
private getBranding(): BrandingDto {
return {
logo: this.customizationConfig.branding.customLogo
? new URL(this.customizationConfig.branding.customLogo)
: undefined,
? new URL(this.customizationConfig.branding.customLogo).toString()
: null,
name: this.customizationConfig.branding.customName,
};
}
private getSpecialUrls(): SpecialUrlsDto {
private getSpecialUrls(): SpecialUrlDto {
return {
imprint: this.customizationConfig.specialUrls.imprint
? new URL(this.customizationConfig.specialUrls.imprint)
: undefined,
? new URL(this.customizationConfig.specialUrls.imprint).toString()
: null,
privacy: this.customizationConfig.specialUrls.privacy
? new URL(this.customizationConfig.specialUrls.privacy)
: undefined,
? new URL(this.customizationConfig.specialUrls.privacy).toString()
: null,
termsOfUse: this.customizationConfig.specialUrls.termsOfUse
? new URL(this.customizationConfig.specialUrls.termsOfUse)
: undefined,
? new URL(this.customizationConfig.specialUrls.termsOfUse).toString()
: null,
};
}
}

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class GroupInfoDto extends BaseDto {
/**
* Name of the group
* @example "superheroes"
*/
@IsString()
@ApiProperty()
name: string;
/**
* Display name of this group
* @example "Superheroes"
*/
@IsString()
@ApiProperty()
displayName: string;
/**
* True if this group must be specially handled
* Used for e.g. "everybody", "all logged in users"
* @example false
*/
@IsBoolean()
@ApiProperty()
special: boolean;
}

View file

@ -1,15 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GroupInfoDto } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { GroupInfoDto } from './group-info.dto';
import { Group } from './group.entity';
import { SpecialGroup } from './groups.special';

View file

@ -1,57 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDate, IsLowercase, IsOptional, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class MediaUploadDto extends BaseDto {
/**
* The uuid of the media file.
* @example "7697582e-0020-4188-9758-2e00207188ca"
*/
@IsString()
@ApiProperty()
uuid: string;
/**
* The original filename of the media upload.
* @example "example.png"
*/
@IsString()
@ApiProperty()
fileName: string;
/**
* The publicId of the note to which the uploaded file is linked to.
* @example "b604x5885k9k01bq7tsmawvnp0"
*/
@IsString()
@IsOptional()
@ApiProperty()
noteId: string | null;
/**
* The date when the upload objects was created.
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
createdAt: Date;
/**
* The username of the user which uploaded the media file.
* @example "testuser5"
*/
@IsString()
@IsLowercase()
@IsOptional()
@ApiProperty()
username: Username | null;
}

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MediaUploadDto } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
@ -22,7 +23,6 @@ import { ImgurBackend } from './backends/imgur-backend';
import { S3Backend } from './backends/s3-backend';
import { WebdavBackend } from './backends/webdav-backend';
import { MediaBackend } from './media-backend.interface';
import { MediaUploadDto } from './media-upload.dto';
import { MediaUpload } from './media-upload.entity';
@Injectable()
@ -268,7 +268,7 @@ export class MediaService {
uuid: mediaUpload.uuid,
fileName: mediaUpload.fileName,
noteId: (await mediaUpload.note)?.publicId ?? null,
createdAt: mediaUpload.createdAt,
createdAt: mediaUpload.createdAt.toISOString(),
username: user?.username ?? null,
};
}

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ServerStatusDto } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { getServerVersionFromPackageJson } from '../utils/serverVersion';
import { ServerStatusDto } from './server-status.dto';
@Injectable()
export class MonitoringService {

View file

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { BaseDto } from '../utils/base.dto.';
export class ServerVersion {
@ApiProperty()
major: number;
@ApiProperty()
minor: number;
@ApiProperty()
patch: number;
@ApiPropertyOptional()
preRelease?: string;
@ApiPropertyOptional()
commit?: string;
@ApiProperty()
fullString: string;
}
export class ServerStatusDto extends BaseDto {
@ApiProperty()
serverVersion: ServerVersion;
@ApiProperty()
onlineNotes: number;
@ApiProperty()
onlineUsers: number;
@ApiProperty()
distinctOnlineUsers: number;
@ApiProperty()
notesCount: number;
@ApiProperty()
registeredUsers: number;
@ApiProperty()
onlineRegisteredUsers: number;
@ApiProperty()
distinctOnlineRegisteredUsers: number;
@ApiProperty()
isConnectionBusy: boolean;
@ApiProperty()
connectionSocketQueueLength: number;
@ApiProperty()
isDisconnectBusy: boolean;
@ApiProperty()
disconnectSocketQueueLength: number;
}

View file

@ -12,7 +12,7 @@ exports[`NotesService toNoteDto works 1`] = `
"primaryAlias": true,
},
],
"createdAt": undefined,
"createdAt": "2019-02-04T20:34:12.000Z",
"description": "mockDescription",
"editedBy": [
"hardcoded",
@ -39,7 +39,7 @@ exports[`NotesService toNoteDto works 1`] = `
],
"title": "mockTitle",
"updateUsername": "hardcoded",
"updatedAt": 2019-02-04T20:34:12.000Z,
"updatedAt": "2019-02-04T20:34:12.000Z",
"version": undefined,
"viewCount": 1337,
},
@ -55,7 +55,7 @@ exports[`NotesService toNoteMetadataDto works 1`] = `
"primaryAlias": true,
},
],
"createdAt": undefined,
"createdAt": "2019-02-04T20:34:12.000Z",
"description": "mockDescription",
"editedBy": [
"hardcoded",
@ -82,7 +82,7 @@ exports[`NotesService toNoteMetadataDto works 1`] = `
],
"title": "mockTitle",
"updateUsername": "hardcoded",
"updatedAt": 2019-02-04T20:34:12.000Z,
"updatedAt": "2019-02-04T20:34:12.000Z",
"version": undefined,
"viewCount": 1337,
}

View file

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class AliasCreateDto extends BaseDto {
/**
* The note id or alias, which identifies the note the alias should be added to
*/
@IsString()
@ApiProperty()
noteIdOrAlias: string;
/**
* The new alias
*/
@IsString()
@ApiProperty()
newAlias: string;
}

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class AliasUpdateDto extends BaseDto {
/**
* Whether the alias should become the primary alias or not
*/
@IsBoolean()
@ApiProperty()
primaryAlias: boolean;
}

View file

@ -1,32 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class AliasDto extends BaseDto {
/**
* The name of the alias
*/
@IsString()
@ApiProperty()
name: string;
/**
* Is the alias the primary alias or not
*/
@IsBoolean()
@ApiProperty()
primaryAlias: boolean;
/**
* The public id of the note the alias is associated with
*/
@IsString()
@ApiProperty()
noteId: string;
}

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AliasDto } from '@hedgedoc/commons';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@ -12,7 +13,6 @@ import {
PrimaryAliasDeletionForbiddenError,
} from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { AliasDto } from './alias.dto';
import { Alias } from './alias.entity';
import { Note } from './note.entity';
import { NotesService } from './notes.service';

View file

@ -1,158 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsDate,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { AliasDto } from './alias.dto';
import { NotePermissionsDto } from './note-permissions.dto';
export class NoteMetadataDto extends BaseDto {
/**
* ID of the note
*/
@IsString()
@ApiProperty()
id: string;
/**
* All aliases of the note (including the primaryAlias)
*/
@Type(() => AliasDto)
@IsArray()
@ValidateNested({ each: true })
@ApiProperty({ isArray: true, type: AliasDto })
aliases: AliasDto[];
/**
* The primary adress of the note
* If at least one alias is set, this is the primary alias
* If no alias is set, this is the note's ID
*/
@IsString()
@ApiProperty()
primaryAddress: string;
/**
* Title of the note
* Does not contain any markup but might be empty
* @example "Shopping List"
*/
@IsString()
@ApiProperty()
title: string;
/**
* Description of the note
* Does not contain any markup but might be empty
* @example Everything I want to buy
*/
@IsString()
@ApiProperty()
description: string;
/**
* List of tags assigned to this note
* @example "['shopping', 'personal']
*/
@IsArray()
@IsString({ each: true })
@ApiProperty({ isArray: true, type: String })
tags: string[];
@IsNumber()
@ApiProperty()
version: number;
/**
* Datestring of the last time this note was updated
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
updatedAt: Date;
/**
* User that last edited the note
*/
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality
@ApiPropertyOptional()
@IsString()
@IsOptional()
updateUsername: string | null;
/**
* Counts how many times the published note has been viewed
* @example 42
*/
@IsNumber()
@ApiProperty()
viewCount: number;
/**
* Datestring of the time this note was created
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
createdAt: Date;
/**
* List of usernames that edited the note
* @example "['john.smith', 'jane.smith']"
*/
@IsArray()
@IsString({ each: true })
@ApiProperty({ isArray: true, type: String })
editedBy: string[];
/**
* Permissions currently in effect for the note
*/
@ValidateNested()
@Type(() => NotePermissionsDto)
@ApiProperty({ type: NotePermissionsDto })
permissions: NotePermissionsDto;
}
export class NoteMetadataUpdateDto {
/**
* New title of the note
* Can not contain any markup and might be empty
* @example "Shopping List"
*/
@IsString()
@ApiProperty()
title: string;
/**
* New description of the note
* Can not contain any markup but might be empty
* @example Everything I want to buy
*/
@IsString()
@ApiProperty()
description: string;
/**
* New list of tags assigned to this note
* @example "['shopping', 'personal']
*/
@IsArray()
@IsString({ each: true })
@ApiProperty({ isArray: true, type: String })
tags: string[];
}

View file

@ -1,143 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsLowercase,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class NoteUserPermissionEntryDto extends BaseDto {
/**
* Username of the User this permission applies to
*/
@Type(() => String)
@IsString()
@IsLowercase()
@ApiProperty()
username: Username;
/**
* True if the user is allowed to edit the note
* @example false
*/
@IsBoolean()
@ApiProperty()
canEdit: boolean;
}
export class NoteUserPermissionUpdateDto {
/**
* Username of the user this permission should apply to
* @example "john.smith"
*/
@Type(() => String)
@IsString()
@IsLowercase()
@ApiProperty()
username: Username;
/**
* True if the user should be allowed to edit the note
* @example false
*/
@IsBoolean()
@ApiProperty()
canEdit: boolean;
}
export class NoteGroupPermissionEntryDto {
/**
* Name of the Group this permission applies to
*/
@IsString()
@ApiProperty()
groupName: string;
/**
* True if the group members are allowed to edit the note
* @example false
*/
@IsBoolean()
@ApiProperty()
canEdit: boolean;
}
export class NoteGroupPermissionUpdateDto {
/**
* Name of the group this permission should apply to
* @example "superheroes"
*/
@IsString()
@ApiProperty()
groupName: string;
/**
* True if the group members should be allowed to edit the note
* @example false
*/
@IsBoolean()
@ApiProperty()
canEdit: boolean;
}
export class NotePermissionsDto {
/**
* Username of the User this permission applies to
*/
// nestjs-typed does not detect '| null' types as optional
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality
@Type(() => String)
@IsString()
@ApiPropertyOptional()
@IsOptional()
owner: string | null;
/**
* List of users the note is shared with
*/
@ValidateNested({ each: true })
@IsArray()
@Type(() => NoteUserPermissionEntryDto)
@ApiProperty({ isArray: true, type: NoteUserPermissionEntryDto })
sharedToUsers: NoteUserPermissionEntryDto[];
/**
* List of groups the note is shared with
*/
@ValidateNested({ each: true })
@IsArray()
@Type(() => NoteGroupPermissionEntryDto)
@ApiProperty({ isArray: true, type: NoteGroupPermissionEntryDto })
sharedToGroups: NoteGroupPermissionEntryDto[];
}
export class NotePermissionsUpdateDto {
/**
* List of users the note should be shared with
*/
@IsArray()
@ValidateNested({ each: true })
@Type(() => NoteUserPermissionUpdateDto)
@ApiProperty({ isArray: true, type: NoteUserPermissionUpdateDto })
sharedToUsers: NoteUserPermissionUpdateDto[];
/**
* List of groups the note should be shared with
*/
@IsArray()
@ValidateNested({ each: true })
@Type(() => NoteGroupPermissionUpdateDto)
@ApiProperty({ isArray: true, type: NoteGroupPermissionUpdateDto })
sharedToGroups: NoteGroupPermissionUpdateDto[];
}

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsString, ValidateNested } from 'class-validator';
import { EditDto } from '../revisions/edit.dto';
import { BaseDto } from '../utils/base.dto.';
import { NoteMetadataDto } from './note-metadata.dto';
export class NoteDto extends BaseDto {
/**
* Markdown content of the note
* @example "# I am a heading"
*/
@IsString()
@ApiProperty()
content: string;
/**
* Metadata of the note
*/
@Type(() => NoteMetadataDto)
@ValidateNested()
@ApiProperty({ type: NoteMetadataDto })
metadata: NoteMetadataDto;
/**
* Edit information of this note
*/
@IsArray()
@ValidateNested({ each: true })
@Type(() => EditDto)
@ApiProperty({ isArray: true, type: EditDto })
editedByAtPosition: EditDto[];
}

View file

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
export class NoteMediaDeletionDto extends BaseDto {
/**
* Should the associated mediaUploads be keept
* @default false
* @example false
*/
@IsBoolean()
@ApiProperty()
keepMedia: boolean;
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -271,6 +271,7 @@ describe('NotesService', () => {
const note = Mock.of<Note>({
revisions: Promise.resolve([revision]),
aliases: Promise.resolve([]),
createdAt: new Date(1549312452000),
});
mockRevisionService(note, revision);

View file

@ -1,8 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoteDto,
NoteMetadataDto,
NotePermissionsDto,
} from '@hedgedoc/commons';
import { Optional } from '@mrdrogdrog/optional';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@ -30,9 +35,6 @@ import { User } from '../users/user.entity';
import { UsersService } from '../users/users.service';
import { Alias } from './alias.entity';
import { AliasService } from './alias.service';
import { NoteMetadataDto } from './note-metadata.dto';
import { NotePermissionsDto } from './note-permissions.dto';
import { NoteDto } from './note.dto';
import { Note } from './note.entity';
import { Tag } from './tag.entity';
import { getPrimaryAlias } from './utils';
@ -427,11 +429,11 @@ export class NotesService {
title: latestRevision.title,
description: latestRevision.description,
tags: (await latestRevision.tags).map((tag) => tag.name),
createdAt: note.createdAt,
createdAt: note.createdAt.toISOString(),
editedBy: (await this.getAuthorUsers(note)).map((user) => user.username),
permissions: await this.toNotePermissionsDto(note),
version: note.version,
updatedAt: latestRevision.createdAt,
updatedAt: latestRevision.createdAt.toISOString(),
updateUsername: updateUser ? updateUser.username : null,
viewCount: note.viewCount,
};

View file

@ -1,8 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
GuestAccess,
NoteGroupPermissionUpdateDto,
NoteUserPermissionUpdateDto,
} from '@hedgedoc/commons';
import { ConfigModule } from '@nestjs/config';
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
@ -14,7 +19,6 @@ import { ApiToken } from '../api-token/api-token.entity';
import { Identity } from '../auth/identity.entity';
import { Author } from '../authors/author.entity';
import { DefaultAccessLevel } from '../config/default-access-level.enum';
import { GuestAccess } from '../config/guest_access.enum';
import appConfigMock from '../config/mock/app.config.mock';
import authConfigMock from '../config/mock/auth.config.mock';
import databaseConfigMock from '../config/mock/database.config.mock';
@ -31,10 +35,6 @@ import { GroupsService } from '../groups/groups.service';
import { LoggerModule } from '../logger/logger.module';
import { MediaUpload } from '../media/media-upload.entity';
import { Alias } from '../notes/alias.entity';
import {
NoteGroupPermissionUpdateDto,
NoteUserPermissionUpdateDto,
} from '../notes/note-permissions.dto';
import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity';
@ -461,12 +461,15 @@ describe('PermissionsService', () => {
});
describe('updateNotePermissions', () => {
const userPermissionUpdate = new NoteUserPermissionUpdateDto();
userPermissionUpdate.username = 'hardcoded';
userPermissionUpdate.canEdit = true;
const groupPermissionUpdate = new NoteGroupPermissionUpdateDto();
groupPermissionUpdate.groupName = 'testGroup';
groupPermissionUpdate.canEdit = false;
const userPermissionUpdate: NoteUserPermissionUpdateDto = {
username: 'hardcoded',
canEdit: true,
};
const groupPermissionUpdate: NoteGroupPermissionUpdateDto = {
groupName: 'testGroup',
canEdit: false,
};
const user = User.create(userPermissionUpdate.username, 'Testy') as User;
const group = Group.create(
groupPermissionUpdate.groupName,

View file

@ -1,14 +1,14 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess, NotePermissionsUpdateDto } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GuestAccess } from '../config/guest_access.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { PermissionsUpdateInconsistentError } from '../errors/errors';
import { NoteEvent, NoteEventMap } from '../events';
@ -16,7 +16,6 @@ import { Group } from '../groups/group.entity';
import { GroupsService } from '../groups/groups.service';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { MediaUpload } from '../media/media-upload.entity';
import { NotePermissionsUpdateDto } from '../notes/note-permissions.dto';
import { Note } from '../notes/note.entity';
import { User } from '../users/user.entity';
import { UsersService } from '../users/users.service';

View file

@ -1,9 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess } from '../../config/guest_access.enum';
import { GuestAccess } from '@hedgedoc/commons';
import { NotePermission } from '../note-permission.enum';
import { convertGuestAccessToNotePermission } from './convert-guest-access-to-note-permission';

View file

@ -1,9 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess } from '../../config/guest_access.enum';
import { GuestAccess } from '@hedgedoc/commons';
import { NotePermission } from '../note-permission.enum';
/**

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -13,7 +13,6 @@ import { Mock } from 'ts-mockery';
import { Note } from '../../notes/note.entity';
import { User } from '../../users/user.entity';
import { Username } from '../../utils/username';
import * as NameRandomizerModule from './random-word-lists/name-randomizer';
import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note';
@ -40,7 +39,7 @@ describe('websocket connection', () => {
let mockedUser: User;
let mockedMessageTransporter: MessageTransporter;
const mockedUserName: Username = 'mocked-user-name';
const mockedUserName: string = 'mocked-user-name';
const mockedDisplayName = 'mockedDisplayName';
beforeEach(() => {

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,8 +11,6 @@ import {
} from '@hedgedoc/commons';
import { Listener } from 'eventemitter2';
import { Username } from '../../utils/username';
export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[];
/**
@ -22,7 +20,7 @@ export class RealtimeUserStatusAdapter {
private readonly realtimeUser: RealtimeUser;
constructor(
private readonly username: Username | null,
private readonly username: string | null,
private readonly displayName: string,
private collectOtherAdapters: OtherAdapterCollector,
private messageTransporter: MessageTransporter,

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -10,7 +10,6 @@ import {
import { Mock } from 'ts-mockery';
import { User } from '../../../users/user.entity';
import { Username } from '../../../utils/username';
import { RealtimeConnection } from '../realtime-connection';
import { RealtimeNote } from '../realtime-note';
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
@ -22,13 +21,13 @@ enum RealtimeUserState {
WITH_READONLY,
}
const MOCK_FALLBACK_USERNAME: Username = 'mock';
const MOCK_FALLBACK_USERNAME: string = 'mock';
/**
* Creates a mocked {@link RealtimeConnection realtime connection}.
*/
export class MockConnectionBuilder {
private username: Username | null;
private username: string | null;
private displayName: string | undefined;
private includeRealtimeUserStatus: RealtimeUserState =
RealtimeUserState.WITHOUT;
@ -51,7 +50,7 @@ export class MockConnectionBuilder {
*
* @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}.
*/
public withLoggedInUser(username?: Username): this {
public withLoggedInUser(username?: string): this {
const newUsername = username ?? MOCK_FALLBACK_USERNAME;
this.username = newUsername;
this.displayName = newUsername;

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -41,7 +41,6 @@ import { SessionService } from '../../sessions/session.service';
import { User } from '../../users/user.entity';
import { UsersModule } from '../../users/users.module';
import { UsersService } from '../../users/users.service';
import { Username } from '../../utils/username';
import * as websocketConnectionModule from '../realtime-note/realtime-connection';
import { RealtimeConnection } from '../realtime-note/realtime-connection';
import { RealtimeNote } from '../realtime-note/realtime-note';
@ -166,7 +165,7 @@ describe('Websocket gateway', () => {
),
);
const mockUsername: Username = 'mock-username';
const mockUsername: string = 'mock-username';
jest
.spyOn(sessionService, 'fetchUsernameForSessionId')
.mockImplementation((sessionId: string) =>

View file

@ -6,7 +6,6 @@
import {
DisconnectReason,
MessageTransporter,
NotePermissions,
userCanEdit,
} from '@hedgedoc/commons';
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
@ -92,10 +91,7 @@ export class WebsocketGateway implements OnGatewayConnection {
);
const permissions = await this.noteService.toNotePermissionsDto(note);
const acceptEdits: boolean = userCanEdit(
permissions as NotePermissions,
user?.username,
);
const acceptEdits: boolean = userCanEdit(permissions, user?.username);
const connection = new RealtimeConnection(
websocketTransporter,

View file

@ -77,14 +77,14 @@ exports[`RevisionsService toRevisionDto converts a revision 1`] = `
"mockusername",
],
"content": "mockContent",
"createdAt": 2020-05-20T09:58:00.000Z,
"createdAt": "2020-05-20T09:58:00.000Z",
"description": "mockDescription",
"edits": [
{
"createdAt": 2020-03-04T22:32:00.000Z,
"endPos": 93,
"startPos": 34,
"updatedAt": 2021-02-10T12:23:00.000Z,
"createdAt": "2020-03-04T22:32:00.000Z",
"endPosition": 93,
"startPosition": 34,
"updatedAt": "2021-02-10T12:23:00.000Z",
"username": "mockusername",
},
],
@ -104,7 +104,7 @@ exports[`RevisionsService toRevisionMetadataDto converts a revision 1`] = `
"authorUsernames": [
"mockusername",
],
"createdAt": 2020-05-20T09:58:00.000Z,
"createdAt": "2020-05-20T09:58:00.000Z",
"description": "mockDescription",
"id": 3246,
"length": 1854,

View file

@ -1,62 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator';
import { UserInfoDto } from '../users/user-info.dto';
import { BaseDto } from '../utils/base.dto.';
export class EditDto extends BaseDto {
/**
* Username of the user who authored this section
* Is `null` if the user is anonymous
* @example "john.smith"
*/
// nestjs-typed does not detect '| null' types as optional
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality
@IsString()
@IsOptional()
@ApiPropertyOptional()
username: UserInfoDto['username'] | null;
/**
* Character index of the start of this section
* @example 102
*/
@IsNumber()
@Min(0)
@ApiProperty()
startPos: number;
/**
* Character index of the end of this section
* Must be greater than {@link startPos}
* @example 154
*/
@IsNumber()
@Min(0)
@ApiProperty()
endPos: number;
/**
* Datestring of the time this section was created
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
createdAt: Date;
/**
* Datestring of the last time this section was edited
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
updatedAt: Date;
}

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditDto } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { EditDto } from './edit.dto';
import { Edit } from './edit.entity';
@Injectable()
@ -15,10 +15,10 @@ export class EditService {
return {
username: authorUser ? authorUser.username : null,
startPos: edit.startPos,
endPos: edit.endPos,
createdAt: edit.createdAt,
updatedAt: edit.updatedAt,
startPosition: edit.startPos,
endPosition: edit.endPos,
createdAt: edit.createdAt.toISOString(),
updatedAt: edit.updatedAt.toISOString(),
};
}
}

View file

@ -1,81 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsDate, IsNumber, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Revision } from './revision.entity';
export class RevisionMetadataDto extends BaseDto {
/**
* ID of this revision
* @example 13
*/
@Type(() => Number)
@IsNumber()
@ApiProperty()
id: Revision['id'];
/**
* Datestring of the time this revision was created
* @example "2020-12-01 12:23:34"
*/
@IsDate()
@Type(() => Date)
@ApiProperty()
createdAt: Date;
/**
* Number of characters in this revision
* @example 142
*/
@IsNumber()
@ApiProperty()
length: number;
/**
* List of the usernames that have contributed to this revision
* Does not include anonymous users
*/
@IsString()
@ApiProperty({ isArray: true, type: String })
authorUsernames: string[];
/**
* Count of anonymous users that have contributed to this revision
*/
@IsNumber()
@ApiProperty()
anonymousAuthorCount: number;
/**
* Title of the note
* Does not contain any markup but might be empty
* @example "Shopping List"
*/
@IsString()
@ApiProperty()
title: string;
/**
* Description of the note
* Does not contain any markup but might be empty
* @example Everything I want to buy
*/
@IsString()
@ApiProperty()
description: string;
/**
* List of tags assigned to this note
* @example "['shopping', 'personal']
*/
@IsArray()
@IsString({ each: true })
@ApiProperty({ isArray: true, type: String })
tags: string[];
}

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
import { EditDto } from './edit.dto';
import { RevisionMetadataDto } from './revision-metadata.dto';
export class RevisionDto extends RevisionMetadataDto {
/**
* Markdown content of the revision
* @example "# I am a heading"
*/
@IsString()
@ApiProperty()
content: string;
/**
* Patch from the preceding revision to this one
*/
@IsString()
@ApiProperty()
patch: string;
/**
* All edit objects which are used in the revision.
*/
@Type(() => EditDto)
@ValidateNested({ each: true })
@ApiProperty({ isArray: true, type: EditDto })
edits: EditDto[];
}

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RevisionDto, RevisionMetadataDto } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import { Cron, Timeout } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
@ -15,8 +16,6 @@ import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Note } from '../notes/note.entity';
import { Tag } from '../notes/tag.entity';
import { EditService } from './edit.service';
import { RevisionMetadataDto } from './revision-metadata.dto';
import { RevisionDto } from './revision.dto';
import { Revision } from './revision.entity';
import { extractRevisionMetadataFromContent } from './utils/extract-revision-metadata-from-content';
@ -136,7 +135,7 @@ export class RevisionsService {
return {
id: revision.id,
length: revision.length,
createdAt: revision.createdAt,
createdAt: revision.createdAt.toISOString(),
authorUsernames: revisionUserInfo.usernames,
anonymousAuthorCount: revisionUserInfo.anonymousUserCount,
title: revision.title,
@ -151,7 +150,7 @@ export class RevisionsService {
id: revision.id,
content: revision.content,
length: revision.length,
createdAt: revision.createdAt,
createdAt: revision.createdAt.toISOString(),
title: revision.title,
tags: (await revision.tags).map((tag) => tag.name),
description: revision.description,

View file

@ -1,13 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ProviderType } from '@hedgedoc/commons';
import { DataSource } from 'typeorm';
import { ApiToken } from './api-token/api-token.entity';
import { Identity } from './auth/identity.entity';
import { ProviderType } from './auth/provider-type.enum';
import { Author } from './authors/author.entity';
import { Group } from './groups/group.entity';
import { HistoryEntry } from './history/history-entry.entity';

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FullUserInfoDto, ProviderType } from '@hedgedoc/commons';
import { Optional } from '@mrdrogdrog/optional';
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
@ -12,16 +13,13 @@ import { unsign } from 'cookie-signature';
import { IncomingMessage } from 'http';
import { Repository } from 'typeorm';
import { ProviderType } from '../auth/provider-type.enum';
import authConfiguration, { AuthConfig } from '../config/auth.config';
import { DatabaseType } from '../config/database-type.enum';
import databaseConfiguration, {
DatabaseConfig,
} from '../config/database.config';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { FullUserInfoDto } from '../users/user-info.dto';
import { HEDGEDOC_SESSION } from '../utils/session';
import { Username } from '../utils/username';
import { Session } from './session.entity';
export interface SessionState {
@ -29,7 +27,7 @@ export interface SessionState {
cookie: unknown;
/** Contains the username if logged in completely, is undefined when not being logged in */
username?: Username;
username?: string;
/** The auth provider that is used for the current login or pending login */
authProviderType?: ProviderType;
@ -87,7 +85,7 @@ export class SessionService {
* @param sessionId The session id for which the owning user should be found
* @return A Promise that either resolves with the username or rejects with an error
*/
fetchUsernameForSessionId(sessionId: string): Promise<Username | undefined> {
fetchUsernameForSessionId(sessionId: string): Promise<string | undefined> {
return new Promise((resolve, reject) => {
this.logger.debug(
`Fetching username for sessionId ${sessionId}`,
@ -102,7 +100,7 @@ export class SessionService {
'fetchUsernameForSessionId',
);
if (error) return reject(error);
return resolve(result?.username as Username);
return resolve(result?.username);
},
);
});

View file

@ -1,77 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsLowercase, IsOptional, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class UserInfoDto extends BaseDto {
/**
* The username
* @example "john.smith"
*/
@Type(() => String)
@IsString()
@IsLowercase()
@ApiProperty()
username: Username;
/**
* The display name
* @example "John Smith"
*/
@IsString()
@ApiProperty()
displayName: string;
/**
* URL of the profile picture
* @example "https://hedgedoc.example.com/uploads/johnsmith.png"
*/
@ApiPropertyOptional({
format: 'uri',
})
@IsOptional()
@IsString()
photoUrl?: string;
}
/**
* This DTO contains all attributes of the standard UserInfoDto
* in addition to the email address.
*/
export class FullUserInfoDto extends UserInfoDto {
/**
* Email address of the user
* @example "john.smith@example.com"
*/
@ApiPropertyOptional({
format: 'email',
})
@IsOptional()
@IsString()
email?: string;
}
export class FullUserInfoWithIdDto extends FullUserInfoDto {
/**
* The user's ID
* @example 42
*/
@IsString()
id: string;
}
export class UserLoginInfoDto extends UserInfoDto {
/**
* Identifier of the auth provider that was used to log in
*/
@ApiProperty()
@IsString()
authProvider: string;
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -20,7 +20,6 @@ import { Group } from '../groups/group.entity';
import { HistoryEntry } from '../history/history-entry.entity';
import { MediaUpload } from '../media/media-upload.entity';
import { Note } from '../notes/note.entity';
import { Username } from '../utils/username';
@Entity()
export class User {
@ -30,7 +29,7 @@ export class User {
@Column({
unique: true,
})
username: Username;
username: string;
@Column()
displayName: string;
@ -78,7 +77,7 @@ export class User {
private constructor() {}
public static create(
username: Username,
username: string,
displayName: string,
email?: string,
photoUrl?: string,

View file

@ -1,21 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsBoolean, IsLowercase, IsString } from 'class-validator';
import { BaseDto } from '../utils/base.dto.';
import { Username } from '../utils/username';
export class UsernameCheckDto extends BaseDto {
// eslint-disable-next-line @darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator
@IsString()
@IsLowercase()
username: Username;
}
export class UsernameCheckResponseDto extends BaseDto {
@IsBoolean()
usernameAvailable: boolean;
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -54,7 +54,7 @@ describe('UsersService', () => {
.mockImplementationOnce(async (user: User): Promise<User> => user);
});
it('successfully creates a user', async () => {
const user = await service.createUser(username, displayname);
const user = await service.createUser(username, displayname, null, null);
expect(user.username).toEqual(username);
expect(user.displayName).toEqual(displayname);
});
@ -64,11 +64,11 @@ describe('UsersService', () => {
throw new Error();
});
// create first user with username
await service.createUser(username, displayname);
await service.createUser(username, displayname, null, null);
// attempt to create second user with username
await expect(service.createUser(username, displayname)).rejects.toThrow(
AlreadyInDBError,
);
await expect(
service.createUser(username, displayname, null, null),
).rejects.toThrow(AlreadyInDBError);
});
});
@ -134,7 +134,7 @@ describe('UsersService', () => {
expect(photoUrl).toEqual(photo);
});
it('works if a user no photoUrl', () => {
user.photo = undefined;
user.photo = null;
const photoUrl = service.getPhotoUrl(user);
expect(photoUrl).toEqual('');
});

View file

@ -1,9 +1,15 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { REGEX_USERNAME } from '@hedgedoc/commons';
import {
FullUserInfoDto,
LoginUserInfoDto,
ProviderType,
REGEX_USERNAME,
UserInfoDto,
} from '@hedgedoc/commons';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@ -11,12 +17,6 @@ import { Repository } from 'typeorm';
import AuthConfiguration, { AuthConfig } from '../config/auth.config';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Username } from '../utils/username';
import {
FullUserInfoDto,
UserInfoDto,
UserLoginInfoDto,
} from './user-info.dto';
import { UserRelationEnum } from './user-relation.enum';
import { User } from './user.entity';
@ -34,7 +34,7 @@ export class UsersService {
/**
* @async
* Create a new user with a given username and displayName
* @param {Username} username - the username the new user shall have
* @param {string} username - the username the new user shall have
* @param {string} displayName - the display name the new user shall have
* @param {string} [email] - the email the new user shall have
* @param {string} [photoUrl] - the photoUrl the new user shall have
@ -43,17 +43,22 @@ export class UsersService {
* @throws {AlreadyInDBError} the username is already taken.
*/
async createUser(
username: Username,
username: string,
displayName: string,
email?: string,
photoUrl?: string,
email: string | null,
photoUrl: string | null,
): Promise<User> {
if (!REGEX_USERNAME.test(username)) {
throw new BadRequestException(
`The username '${username}' is not a valid username.`,
);
}
const user = User.create(username, displayName, email, photoUrl);
const user = User.create(
username,
displayName,
email || undefined,
photoUrl || undefined,
);
try {
return await this.userRepository.save(user);
} catch {
@ -123,7 +128,7 @@ export class UsersService {
* @param username - the username to check
* @return {boolean} true if the user exists, false otherwise
*/
async checkIfUserExists(username: Username): Promise<boolean> {
async checkIfUserExists(username: string): Promise<boolean> {
const user = await this.userRepository.findOne({
where: { username: username },
});
@ -133,12 +138,12 @@ export class UsersService {
/**
* @async
* Get the user specified by the username
* @param {Username} username the username by which the user is specified
* @param {string} username the username by which the user is specified
* @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
* @return {User} the specified user
*/
async getUserByUsername(
username: Username,
username: string,
withRelations: UserRelationEnum[] = [],
): Promise<User> {
const user = await this.userRepository.findOne({
@ -191,7 +196,7 @@ export class UsersService {
};
}
toUserLoginInfoDto(user: User, authProvider: string): UserLoginInfoDto {
return { ...this.toUserDto(user), authProvider };
toLoginUserInfoDto(user: User, authProvider: ProviderType): LoginUserInfoDto {
return { ...this.toFullUserDto(user), authProvider };
}
}

View file

@ -1,30 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ServerVersionDto } from '@hedgedoc/commons';
import { Optional } from '@mrdrogdrog/optional';
import { promises as fs } from 'fs';
import { join as joinPath } from 'path';
import { ServerVersion } from '../monitoring/server-status.dto';
let versionCache: ServerVersion | undefined = undefined;
let versionCache: ServerVersionDto | undefined = undefined;
/**
* Reads the HedgeDoc version from the root package.json. This is done only once per run.
*
* @return {Promise<ServerVersion>} A Promise that contains the parsed server version.
* @return {Promise<ServerVersionDto>} A Promise that contains the parsed server version.
* @throws {Error} if the package.json couldn't be found or doesn't contain a correct version.
*/
export async function getServerVersionFromPackageJson(): Promise<ServerVersion> {
export async function getServerVersionFromPackageJson(): Promise<ServerVersionDto> {
if (!versionCache) {
versionCache = await parseVersionFromPackageJson();
}
return versionCache;
}
async function parseVersionFromPackageJson(): Promise<ServerVersion> {
async function parseVersionFromPackageJson(): Promise<ServerVersionDto> {
const rawFileContent: string = await fs.readFile(
joinPath(__dirname, '../../../package.json'),
{ encoding: 'utf8' },

View file

@ -1,7 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type TimestampMillis = number;

View file

@ -1,11 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type Username = Lowercase<string>;
export function makeUsernameLowercase(username: string): Username {
return username.toLowerCase() as Username;
}

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AliasCreateDto, AliasUpdateDto } from '@hedgedoc/commons';
import request from 'supertest';
import { AliasCreateDto } from '../../src/notes/alias-create.dto';
import { AliasUpdateDto } from '../../src/notes/alias-update.dto';
import { Note } from '../../src/notes/note.entity';
import { User } from '../../src/users/user.entity';
import {
password1,
@ -183,11 +183,12 @@ describe('Alias', () => {
});
});
it('if the property primaryAlias is false', async () => {
changeAliasDto.primaryAlias = false;
await agent1
.put(`/api/private/alias/${newAlias}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.send({
primaryAlias: false,
})
.expect(400);
});
it('if the user is not an owner', async () => {
@ -204,7 +205,7 @@ describe('Alias', () => {
describe('DELETE /alias/{alias}', () => {
const testAlias = 'aliasTest3';
const newAlias = 'normalAlias3';
let note;
let note: Note;
beforeEach(async () => {
note = await testSetup.notesService.createNote(

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -8,21 +8,18 @@
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
*/
import { LoginDto, RegisterDto, UpdatePasswordDto } from '@hedgedoc/commons';
import request from 'supertest';
import { LoginDto } from '../../src/auth/local/login.dto';
import { RegisterDto } from '../../src/auth/local/register.dto';
import { UpdatePasswordDto } from '../../src/auth/local/update-password.dto';
import { NotInDBError } from '../../src/errors/errors';
import { UserRelationEnum } from '../../src/users/user-relation.enum';
import { checkPassword } from '../../src/utils/password';
import { Username } from '../../src/utils/username';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Auth', () => {
let testSetup: TestSetup;
let username: Username;
let username: string;
let displayName: string;
let password: string;
@ -75,6 +72,8 @@ describe('Auth', () => {
const conflictingUser = await testSetup.userService.createUser(
conflictingUserName,
displayName,
null,
null,
);
const registrationDto: RegisterDto = {
displayName: displayName,

View file

@ -1,12 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess, LoginDto } from '@hedgedoc/commons';
import request from 'supertest';
import { LoginDto } from '../../src/auth/local/login.dto';
import { GuestAccess } from '../../src/config/guest_access.enum';
import { createDefaultMockNoteConfig } from '../../src/config/mock/note.config.mock';
import { NoteConfig } from '../../src/config/note.config';
import {

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -41,7 +41,7 @@ describe('History', () => {
historyService = moduleRef.get(HistoryService);
const userService = moduleRef.get(UsersService);
localIdentityService = moduleRef.get(LocalService);
user = await userService.createUser(username, 'Testy');
user = await userService.createUser(username, 'Testy', null, null);
await localIdentityService.createLocalIdentity(user, password);
const notesService = moduleRef.get(NotesService);
note = await notesService.createNote(content, user, 'note');

View file

@ -1,14 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { LoginUserInfoDto, ProviderType } from '@hedgedoc/commons';
import { promises as fs } from 'fs';
import request from 'supertest';
import { NotInDBError } from '../../src/errors/errors';
import { Note } from '../../src/notes/note.entity';
import { UserLoginInfoDto } from '../../src/users/user-info.dto';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
@ -32,7 +32,12 @@ describe('Me', () => {
const password = 'AHardcodedStrongP@ssword123';
await testSetup.app.init();
user = await testSetup.userService.createUser(username, 'Testy');
user = await testSetup.userService.createUser(
username,
'Testy',
null,
null,
);
await testSetup.localIdentityService.createLocalIdentity(user, password);
content = 'This is a test note.';
@ -51,12 +56,15 @@ describe('Me', () => {
});
it('GET /me', async () => {
const userInfo = testSetup.userService.toUserLoginInfoDto(user, 'local');
const userInfo = testSetup.userService.toLoginUserInfoDto(
user,
ProviderType.LOCAL,
);
const response = await agent
.get('/api/private/me')
.expect('Content-Type', /json/)
.expect(200);
const gotUser = response.body as UserLoginInfoDto;
const gotUser = response.body as LoginUserInfoDto;
expect(gotUser).toEqual(userInfo);
});
@ -126,15 +134,15 @@ describe('Me', () => {
await fs.rmdir(uploadPath);
});
it('POST /me/profile', async () => {
it('PUT /me/profile', async () => {
const newDisplayName = 'Another name';
expect(user.displayName).not.toEqual(newDisplayName);
await agent
.post('/api/private/me/profile')
.put('/api/private/me/profile')
.send({
displayName: newDisplayName,
})
.expect(201);
.expect(200);
const dbUser = await testSetup.userService.getUserByUsername('hardcoded');
expect(dbUser.displayName).toEqual(newDisplayName);
});

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -39,9 +39,19 @@ describe('Notes', () => {
const password2 = 'AHardcodedStrongP@ssword12';
const groupname1 = 'groupname1';
user1 = await testSetup.userService.createUser(username1, 'Testy');
user1 = await testSetup.userService.createUser(
username1,
'Testy',
null,
null,
);
await testSetup.localIdentityService.createLocalIdentity(user1, password1);
user2 = await testSetup.userService.createUser(username2, 'Max Mustermann');
user2 = await testSetup.userService.createUser(
username2,
'Max Mustermann',
null,
null,
);
await testSetup.localIdentityService.createLocalIdentity(user2, password2);
group1 = await testSetup.groupService.createGroup(groupname1, 'Group 1');
@ -668,7 +678,7 @@ describe('Notes', () => {
.put(`/api/private/notes/${alias}/metadata/permissions/owner`)
.expect('Content-Type', /json/)
.expect(200)
.send({ newOwner: user2.username });
.send({ owner: user2.username });
expect(response.body.metadata.permissions.owner).toBe(user2.username);
});
});

View file

@ -1,12 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { LoginDto, RegisterDto } from '@hedgedoc/commons';
import request from 'supertest';
import { LoginDto } from '../../src/auth/local/login.dto';
import { RegisterDto } from '../../src/auth/local/register.dto';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Register and Login', () => {

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AliasUpdateDto } from '@hedgedoc/commons';
import request from 'supertest';
import { AliasUpdateDto } from '../../src/notes/alias-update.dto';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Alias', () => {
@ -166,13 +166,13 @@ describe('Alias', () => {
.expect(401);
});
it('if the property primaryAlias is false', async () => {
changeAliasDto.primaryAlias = false;
await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.send({
primaryAlias: false,
})
.expect(400);
});
});

View file

@ -1,15 +1,15 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteMetadataDto } from '@hedgedoc/commons';
import { promises as fs } from 'fs';
import { join } from 'path';
import { HistoryEntryDto } from 'src/history/history-entry.dto';
import request from 'supertest';
import { HistoryEntryUpdateDto } from '../../src/history/history-entry-update.dto';
import { HistoryEntryDto } from '../../src/history/history-entry.dto';
import { NoteMetadataDto } from '../../src/notes/note-metadata.dto';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
@ -25,7 +25,12 @@ describe('Me', () => {
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
user = await testSetup.userService.createUser('hardcoded', 'Testy');
user = await testSetup.userService.createUser(
'hardcoded',
'Testy',
null,
null,
);
await testSetup.app.init();
});

View file

@ -1,14 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NotePermissionsUpdateDto } from '@hedgedoc/commons';
import { promises as fs } from 'fs';
import { join } from 'path';
import request from 'supertest';
import { NotInDBError } from '../../src/errors/errors';
import { NotePermissionsUpdateDto } from '../../src/notes/note-permissions.dto';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Notes', () => {
@ -219,14 +219,15 @@ describe('Notes', () => {
testSetup.users[0],
'deleteTest3',
);
const updateNotePermission = new NotePermissionsUpdateDto();
updateNotePermission.sharedToUsers = [
{
username: testSetup.users[0].username,
canEdit: true,
},
];
updateNotePermission.sharedToGroups = [];
const updateNotePermission: NotePermissionsUpdateDto = {
sharedToUsers: [
{
username: testSetup.users[0].username,
canEdit: true,
},
],
sharedToGroups: [],
};
await testSetup.permissionsService.updateNotePermissions(
note,
updateNotePermission,

View file

@ -1,8 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiTokenWithSecretDto } from '@hedgedoc/commons';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RouterModule, Routes } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter';
@ -11,7 +12,6 @@ import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Connection, createConnection } from 'typeorm';
import { ApiTokenWithSecretDto } from '../src/api-token/api-token.dto';
import { ApiTokenGuard } from '../src/api-token/api-token.guard';
import { ApiTokenModule } from '../src/api-token/api-token.module';
import { ApiTokenService } from '../src/api-token/api-token.service';
@ -389,13 +389,28 @@ export class TestSetupBuilder {
this.setupPostCompile.push(async () => {
// Create users
this.testSetup.users.push(
await this.testSetup.userService.createUser(username1, 'Test User 1'),
await this.testSetup.userService.createUser(
username1,
'Test User 1',
null,
null,
),
);
this.testSetup.users.push(
await this.testSetup.userService.createUser(username2, 'Test User 2'),
await this.testSetup.userService.createUser(
username2,
'Test User 2',
null,
null,
),
);
this.testSetup.users.push(
await this.testSetup.userService.createUser(username3, 'Test User 3'),
await this.testSetup.userService.createUser(
username3,
'Test User 3',
null,
null,
),
);
// Create identities for login
@ -415,10 +430,12 @@ export class TestSetupBuilder {
// create auth tokens
this.testSetup.authTokens = await Promise.all(
this.testSetup.users.map(async (user) => {
const validUntil = new Date();
validUntil.setTime(validUntil.getTime() + 60 * 60 * 1000);
return await this.testSetup.publicAuthTokenService.addToken(
user,
'test',
new Date().getTime() + 60 * 60 * 1000,
validUntil,
);
}),
);