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;