mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 18:25:21 -04:00
refactor: replace TypeORM with knex.js
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
6e151c8a1b
commit
c0ce00b3f9
242 changed files with 4601 additions and 6871 deletions
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
import { CompleteRequest } from '../api/utils/request.type';
|
||||
import { NotInDBError, TokenNotValidError } from '../errors/errors';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { ApiTokenService } from './api-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApiTokenGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
private readonly apiTokenService: ApiTokenService,
|
||||
) {
|
||||
this.logger.setContext(ApiTokenGuard.name);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request: CompleteRequest = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return false;
|
||||
}
|
||||
const [method, token] = authHeader.trim().split(' ');
|
||||
if (method !== 'Bearer') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
request.user = await this.apiTokenService.validateToken(token.trim());
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (
|
||||
!(error instanceof TokenNotValidError || error instanceof NotInDBError)
|
||||
) {
|
||||
this.logger.error(
|
||||
`Error during API token validation: ${String(error)}`,
|
||||
'canActivate',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,17 +4,16 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { KnexModule } from 'nest-knexjs';
|
||||
|
||||
import { ApiTokenGuard } from '../api/utils/guards/api-token.guard';
|
||||
import { MockApiTokenGuard } from '../api/utils/guards/mock-api-token.guard';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { ApiToken } from './api-token.entity';
|
||||
import { ApiTokenGuard } from './api-token.guard';
|
||||
import { ApiTokenService } from './api-token.service';
|
||||
import { MockApiTokenGuard } from './mock-api-token.guard';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule, LoggerModule, TypeOrmModule.forFeature([ApiToken])],
|
||||
imports: [UsersModule, LoggerModule, KnexModule],
|
||||
providers: [ApiTokenService, ApiTokenGuard, MockApiTokenGuard],
|
||||
exports: [ApiTokenService, ApiTokenGuard],
|
||||
})
|
||||
|
|
|
@ -104,13 +104,13 @@ describe('ApiTokenService', () => {
|
|||
describe('getTokensByUser', () => {
|
||||
it('works', async () => {
|
||||
createQueryBuilderFunc.getMany = () => [apiToken];
|
||||
const tokens = await service.getTokensByUser(user);
|
||||
const tokens = await service.getTokensOfUserById(user);
|
||||
expect(tokens).toHaveLength(1);
|
||||
expect(tokens).toEqual([apiToken]);
|
||||
});
|
||||
it('should return empty array if token for user do not exists', async () => {
|
||||
jest.spyOn(apiTokenRepo, 'find').mockImplementationOnce(async () => []);
|
||||
const tokens = await service.getTokensByUser(user);
|
||||
const tokens = await service.getTokensOfUserById(user);
|
||||
expect(tokens).toHaveLength(0);
|
||||
expect(tokens).toEqual([]);
|
||||
});
|
||||
|
@ -153,13 +153,13 @@ describe('ApiTokenService', () => {
|
|||
);
|
||||
|
||||
expect(() =>
|
||||
service.checkToken(secret, accessToken as ApiToken),
|
||||
service.ensureTokenIsValid(secret, accessToken as ApiToken),
|
||||
).not.toThrow();
|
||||
});
|
||||
it('AuthToken has wrong hash', () => {
|
||||
const [accessToken] = service.createToken(user, 'TestToken', null);
|
||||
expect(() =>
|
||||
service.checkToken('secret', accessToken as ApiToken),
|
||||
service.ensureTokenIsValid('secret', accessToken as ApiToken),
|
||||
).toThrow(TokenNotValidError);
|
||||
});
|
||||
it('AuthToken has wrong validUntil Date', () => {
|
||||
|
@ -168,9 +168,9 @@ describe('ApiTokenService', () => {
|
|||
'Test',
|
||||
new Date(1549312452000),
|
||||
);
|
||||
expect(() => service.checkToken(secret, accessToken as ApiToken)).toThrow(
|
||||
TokenNotValidError,
|
||||
);
|
||||
expect(() =>
|
||||
service.ensureTokenIsValid(secret, accessToken as ApiToken),
|
||||
).toThrow(TokenNotValidError);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -222,7 +222,7 @@ describe('ApiTokenService', () => {
|
|||
.mockImplementationOnce(async (_, __): Promise<ApiToken> => {
|
||||
return apiToken;
|
||||
});
|
||||
const userByToken = await service.validateToken(
|
||||
const userByToken = await service.getUserIdForToken(
|
||||
`hd2.${apiToken.keyId}.${testSecret}`,
|
||||
);
|
||||
expect(userByToken).toEqual({
|
||||
|
@ -233,27 +233,27 @@ describe('ApiTokenService', () => {
|
|||
describe('fails:', () => {
|
||||
it('the prefix is missing', async () => {
|
||||
await expect(
|
||||
service.validateToken(`${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
service.getUserIdForToken(`${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
).rejects.toThrow(TokenNotValidError);
|
||||
});
|
||||
it('the prefix is wrong', async () => {
|
||||
await expect(
|
||||
service.validateToken(`hd1.${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
service.getUserIdForToken(`hd1.${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
).rejects.toThrow(TokenNotValidError);
|
||||
});
|
||||
it('the secret is missing', async () => {
|
||||
await expect(
|
||||
service.validateToken(`hd2.${apiToken.keyId}`),
|
||||
service.getUserIdForToken(`hd2.${apiToken.keyId}`),
|
||||
).rejects.toThrow(TokenNotValidError);
|
||||
});
|
||||
it('the secret is too long', async () => {
|
||||
await expect(
|
||||
service.validateToken(`hd2.${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
service.getUserIdForToken(`hd2.${apiToken.keyId}.${'a'.repeat(73)}`),
|
||||
).rejects.toThrow(TokenNotValidError);
|
||||
});
|
||||
it('the token contains sections after the secret', async () => {
|
||||
await expect(
|
||||
service.validateToken(
|
||||
service.getUserIdForToken(
|
||||
`hd2.${apiToken.keyId}.${'a'.repeat(73)}.extra`,
|
||||
),
|
||||
).rejects.toThrow(TokenNotValidError);
|
||||
|
|
|
@ -6,19 +6,23 @@
|
|||
import { ApiTokenDto, ApiTokenWithSecretDto } from '@hedgedoc/commons';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, Timeout } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
||||
import { Repository } from 'typeorm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectConnection } from 'nest-knexjs';
|
||||
|
||||
import { User } from '../database/user.entity';
|
||||
import { ApiToken, FieldNameApiToken, TableApiToken } from '../database/types';
|
||||
import { TypeInsertApiToken } from '../database/types/api-token';
|
||||
import {
|
||||
NotInDBError,
|
||||
TokenNotValidError,
|
||||
TooManyTokensError,
|
||||
} from '../errors/errors';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { bufferToBase64Url, checkTokenEquality } from '../utils/password';
|
||||
import { ApiToken } from './api-token.entity';
|
||||
import {
|
||||
bufferToBase64Url,
|
||||
checkTokenEquality,
|
||||
hashApiToken,
|
||||
} from '../utils/password';
|
||||
|
||||
export const AUTH_TOKEN_PREFIX = 'hd2';
|
||||
|
||||
|
@ -26,13 +30,22 @@ export const AUTH_TOKEN_PREFIX = 'hd2';
|
|||
export class ApiTokenService {
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@InjectRepository(ApiToken)
|
||||
private authTokenRepository: Repository<ApiToken>,
|
||||
|
||||
@InjectConnection()
|
||||
private readonly knex: Knex,
|
||||
) {
|
||||
this.logger.setContext(ApiTokenService.name);
|
||||
}
|
||||
|
||||
async validateToken(tokenString: string): Promise<User> {
|
||||
/**
|
||||
* Validates a given token string and returns the userId if the token is valid
|
||||
* The usage of this token is tracked in the database
|
||||
*
|
||||
* @param tokenString The token string to validate and parse
|
||||
* @return The userId associated with the token
|
||||
* @throws TokenNotValidError if the token is not valid
|
||||
*/
|
||||
async getUserIdForToken(tokenString: string): Promise<number> {
|
||||
const [prefix, keyId, secret, ...rest] = tokenString.split('.');
|
||||
if (!keyId || !secret || prefix !== AUTH_TOKEN_PREFIX || rest.length > 0) {
|
||||
throw new TokenNotValidError('Invalid API token format');
|
||||
|
@ -44,179 +57,202 @@ export class ApiTokenService {
|
|||
`API token '${tokenString}' has incorrect length`,
|
||||
);
|
||||
}
|
||||
const token = await this.getToken(keyId);
|
||||
this.checkToken(secret, token);
|
||||
await this.setLastUsedToken(keyId);
|
||||
return token.user;
|
||||
return await this.knex.transaction(async (transaction) => {
|
||||
const token = await transaction(TableApiToken)
|
||||
.select(
|
||||
FieldNameApiToken.secretHash,
|
||||
FieldNameApiToken.userId,
|
||||
FieldNameApiToken.validUntil,
|
||||
)
|
||||
.where(FieldNameApiToken.id, keyId)
|
||||
.first();
|
||||
if (token === undefined) {
|
||||
throw new TokenNotValidError('Token not found');
|
||||
}
|
||||
|
||||
const tokenHash = token[FieldNameApiToken.secretHash];
|
||||
const validUntil = token[FieldNameApiToken.validUntil];
|
||||
this.ensureTokenIsValid(secret, tokenHash, validUntil);
|
||||
|
||||
await transaction(TableApiToken)
|
||||
.update(FieldNameApiToken.lastUsedAt, this.knex.fn.now())
|
||||
.where(FieldNameApiToken.id, keyId);
|
||||
|
||||
return token[FieldNameApiToken.userId];
|
||||
});
|
||||
}
|
||||
|
||||
createToken(
|
||||
user: User,
|
||||
identifier: string,
|
||||
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();
|
||||
maximumTokenValidity.setTime(
|
||||
maximumTokenValidity.getTime() + 2 * 365 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const isTokenLimitedToMaximumValidity =
|
||||
!userDefinedValidUntil || userDefinedValidUntil > maximumTokenValidity;
|
||||
const validUntil = isTokenLimitedToMaximumValidity
|
||||
? maximumTokenValidity
|
||||
: userDefinedValidUntil;
|
||||
const token = ApiToken.create(
|
||||
keyId,
|
||||
user,
|
||||
identifier,
|
||||
accessTokenHash,
|
||||
new Date(validUntil),
|
||||
);
|
||||
return [token, secret];
|
||||
}
|
||||
|
||||
async addToken(
|
||||
user: User,
|
||||
identifier: string,
|
||||
validUntil: Date | null,
|
||||
/**
|
||||
* Creates a new API token for the given user
|
||||
*
|
||||
* @param userId The id of the user to create the token for
|
||||
* @param tokenLabel The label of the token
|
||||
* @param userDefinedValidUntil Maximum date until the token is valid, will be truncated to 2 years
|
||||
* @throws TooManyTokensError if the user already has 200 tokens
|
||||
* @returns The created token together with the secret
|
||||
*/
|
||||
async createToken(
|
||||
userId: number,
|
||||
tokenLabel: string,
|
||||
userDefinedValidUntil?: Date,
|
||||
): Promise<ApiTokenWithSecretDto> {
|
||||
user.apiTokens = this.getTokensByUser(user);
|
||||
return await this.knex.transaction(async (transaction) => {
|
||||
const existingTokensForUser = await transaction(TableApiToken)
|
||||
.select(FieldNameApiToken.id)
|
||||
.where(FieldNameApiToken.userId, userId);
|
||||
if (existingTokensForUser.length >= 200) {
|
||||
// This is a very high ceiling unlikely to hinder legitimate usage,
|
||||
// but should prevent possible attack vectors
|
||||
throw new TooManyTokensError(
|
||||
`User '${userId}' has already 200 API tokens and can't have more`,
|
||||
);
|
||||
}
|
||||
|
||||
if ((await user.apiTokens).length >= 200) {
|
||||
// This is a very high ceiling unlikely to hinder legitimate usage,
|
||||
// but should prevent possible attack vectors
|
||||
throw new TooManyTokensError(
|
||||
`User '${user.username}' has already 200 API tokens and can't have more`,
|
||||
const secret = bufferToBase64Url(randomBytes(64));
|
||||
const keyId = bufferToBase64Url(randomBytes(8));
|
||||
const accessTokenHash = hashApiToken(secret);
|
||||
// Tokens can only be valid for a maximum of 2 years
|
||||
const maximumTokenValidity = new Date();
|
||||
maximumTokenValidity.setTime(
|
||||
maximumTokenValidity.getTime() + 2 * 365 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const isTokenLimitedToMaximumValidity =
|
||||
!userDefinedValidUntil || userDefinedValidUntil > maximumTokenValidity;
|
||||
const validUntil = isTokenLimitedToMaximumValidity
|
||||
? maximumTokenValidity
|
||||
: userDefinedValidUntil;
|
||||
const token: TypeInsertApiToken = {
|
||||
[FieldNameApiToken.id]: keyId,
|
||||
[FieldNameApiToken.label]: tokenLabel,
|
||||
[FieldNameApiToken.userId]: userId,
|
||||
[FieldNameApiToken.secretHash]: accessTokenHash,
|
||||
[FieldNameApiToken.validUntil]: validUntil,
|
||||
[FieldNameApiToken.createdAt]: new Date(),
|
||||
};
|
||||
await this.knex(TableApiToken).insert(token);
|
||||
return this.toAuthTokenWithSecretDto(
|
||||
{
|
||||
...token,
|
||||
[FieldNameApiToken.lastUsedAt]: null,
|
||||
},
|
||||
secret,
|
||||
);
|
||||
}
|
||||
const [token, secret] = this.createToken(user, identifier, validUntil);
|
||||
const createdToken = (await this.authTokenRepository.save(
|
||||
token,
|
||||
)) as ApiToken;
|
||||
return this.toAuthTokenWithSecretDto(
|
||||
createdToken,
|
||||
`${AUTH_TOKEN_PREFIX}.${createdToken.keyId}.${secret}`,
|
||||
);
|
||||
}
|
||||
|
||||
async setLastUsedToken(keyId: string): Promise<void> {
|
||||
const token = await this.authTokenRepository.findOne({
|
||||
where: { keyId: keyId },
|
||||
});
|
||||
if (token === null) {
|
||||
throw new NotInDBError(`API token with id '${keyId}' not found`);
|
||||
}
|
||||
token.lastUsedAt = new Date();
|
||||
await this.authTokenRepository.save(token);
|
||||
}
|
||||
|
||||
async getToken(keyId: string): Promise<ApiToken> {
|
||||
const token = await this.authTokenRepository.findOne({
|
||||
where: { keyId: keyId },
|
||||
relations: ['user'],
|
||||
});
|
||||
if (token === null) {
|
||||
throw new NotInDBError(`API token with id '${keyId}' not found`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
checkToken(secret: string, token: ApiToken): void {
|
||||
if (!checkTokenEquality(secret, token.hash)) {
|
||||
// hashes are not the same
|
||||
/**
|
||||
* Ensures that the given token secret is valid for the given token
|
||||
* This method does not return any value but throws an error if the token is not valid
|
||||
*
|
||||
* @param secret The secret to compare against the hash from the database
|
||||
* @param tokenHash The hash from the database
|
||||
* @param validUntil Expiry of the API token
|
||||
* @throws TokenNotValidError if the token is invalid
|
||||
*/
|
||||
ensureTokenIsValid(
|
||||
secret: string,
|
||||
tokenHash: string,
|
||||
validUntil: Date,
|
||||
): void {
|
||||
// First, verify token expiry is not in the past (cheap operation)
|
||||
if (validUntil.getTime() < new Date().getTime()) {
|
||||
throw new TokenNotValidError(
|
||||
`Secret does not match Token ${token.label}.`,
|
||||
`Auth token is not valid since ${validUntil.toISOString()}`,
|
||||
);
|
||||
}
|
||||
if (token.validUntil && token.validUntil.getTime() < new Date().getTime()) {
|
||||
// tokens validUntil Date lies in the past
|
||||
throw new TokenNotValidError(
|
||||
`AuthToken '${
|
||||
token.label
|
||||
}' is not valid since ${token.validUntil.toISOString()}.`,
|
||||
);
|
||||
|
||||
// Second, verify the secret (costly operation)
|
||||
if (!checkTokenEquality(secret, tokenHash)) {
|
||||
throw new TokenNotValidError(`Secret does not match token hash`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTokensByUser(user: User): Promise<ApiToken[]> {
|
||||
const tokens = await this.authTokenRepository.find({
|
||||
where: { user: { id: user.id } },
|
||||
});
|
||||
if (tokens === null) {
|
||||
return [];
|
||||
}
|
||||
return tokens;
|
||||
/**
|
||||
* Returns all tokens of a user
|
||||
*
|
||||
* @param userId The id of the user to get the tokens for
|
||||
* @return The tokens of the user
|
||||
*/
|
||||
getTokensOfUserById(userId: number): Promise<ApiToken[]> {
|
||||
return this.knex(TableApiToken)
|
||||
.select()
|
||||
.where(FieldNameApiToken.userId, userId);
|
||||
}
|
||||
|
||||
async removeToken(keyId: string): Promise<void> {
|
||||
const token = await this.authTokenRepository.findOne({
|
||||
where: { keyId: keyId },
|
||||
});
|
||||
if (token === null) {
|
||||
throw new NotInDBError(`API token with id '${keyId}' not found`);
|
||||
/**
|
||||
* Removes a token from the database
|
||||
*
|
||||
* @param keyId The id of the token to remove
|
||||
* @param userId The id of the user who owns the token
|
||||
* @throws NotInDBError if the token is not found
|
||||
*/
|
||||
async removeToken(keyId: string, userId: number): Promise<void> {
|
||||
const numberOfDeletedTokens = await this.knex(TableApiToken)
|
||||
.where(FieldNameApiToken.id, keyId)
|
||||
.andWhere(FieldNameApiToken.userId, userId)
|
||||
.delete();
|
||||
if (numberOfDeletedTokens === 0) {
|
||||
throw new NotInDBError('Token not found');
|
||||
}
|
||||
await this.authTokenRepository.remove(token);
|
||||
}
|
||||
|
||||
toAuthTokenDto(authToken: ApiToken): ApiTokenDto {
|
||||
const tokenDto: ApiTokenDto = {
|
||||
label: authToken.label,
|
||||
keyId: authToken.keyId,
|
||||
createdAt: authToken.createdAt.toISOString(),
|
||||
validUntil: authToken.validUntil.toISOString(),
|
||||
lastUsedAt: null,
|
||||
/**
|
||||
* Converts an ApiToken to an ApiTokenDto
|
||||
*
|
||||
* @param apiToken The token to convert
|
||||
* @return The converted token
|
||||
*/
|
||||
toAuthTokenDto(apiToken: ApiToken): ApiTokenDto {
|
||||
return {
|
||||
label: apiToken[FieldNameApiToken.label],
|
||||
keyId: apiToken[FieldNameApiToken.id],
|
||||
createdAt: apiToken[FieldNameApiToken.createdAt].toISOString(),
|
||||
validUntil: apiToken[FieldNameApiToken.validUntil].toISOString(),
|
||||
lastUsedAt: apiToken[FieldNameApiToken.lastUsedAt]?.toISOString() ?? null,
|
||||
};
|
||||
|
||||
if (authToken.lastUsedAt) {
|
||||
tokenDto.lastUsedAt = new Date(authToken.lastUsedAt).toISOString();
|
||||
}
|
||||
|
||||
return tokenDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an ApiToken to an ApiTokenWithSecretDto
|
||||
*
|
||||
* @param apiToken The token to convert
|
||||
* @param secret The secret of the token
|
||||
* @return The converted token
|
||||
*/
|
||||
toAuthTokenWithSecretDto(
|
||||
authToken: ApiToken,
|
||||
apiToken: ApiToken,
|
||||
secret: string,
|
||||
): ApiTokenWithSecretDto {
|
||||
const tokenDto = this.toAuthTokenDto(authToken);
|
||||
const tokenDto = this.toAuthTokenDto(apiToken);
|
||||
const fullToken = `${AUTH_TOKEN_PREFIX}.${tokenDto.keyId}.${secret}`;
|
||||
return {
|
||||
...tokenDto,
|
||||
secret: secret,
|
||||
secret: fullToken,
|
||||
};
|
||||
}
|
||||
|
||||
// Delete all non valid tokens every sunday on 3:00 AM
|
||||
// Deletes all invalid tokens every sunday on 3:00 AM
|
||||
@Cron('0 0 3 * * 0')
|
||||
async handleCron(): Promise<void> {
|
||||
return await this.removeInvalidTokens();
|
||||
}
|
||||
|
||||
// Delete all non valid tokens 5 sec after startup
|
||||
@Timeout(5000)
|
||||
// Delete all invalid tokens 60 sec after startup
|
||||
@Timeout(60 * 1000)
|
||||
async handleTimeout(): Promise<void> {
|
||||
return await this.removeInvalidTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all expired tokens from the database
|
||||
* This method is called by the cron job and the timeout
|
||||
*/
|
||||
async removeInvalidTokens(): Promise<void> {
|
||||
const currentTime = new Date().getTime();
|
||||
const tokens: ApiToken[] = await this.authTokenRepository.find();
|
||||
let removedTokens = 0;
|
||||
for (const token of tokens) {
|
||||
if (token.validUntil && token.validUntil.getTime() <= currentTime) {
|
||||
this.logger.debug(
|
||||
`AuthToken '${token.keyId}' was removed`,
|
||||
'removeInvalidTokens',
|
||||
);
|
||||
await this.authTokenRepository.remove(token);
|
||||
removedTokens++;
|
||||
}
|
||||
}
|
||||
const numberOfDeletedTokens = await this.knex(TableApiToken)
|
||||
.where(FieldNameApiToken.validUntil, '<', new Date())
|
||||
.delete();
|
||||
this.logger.log(
|
||||
`${removedTokens} invalid AuthTokens were purged from the DB.`,
|
||||
`${numberOfDeletedTokens} invalid AuthTokens were purged from the DB.`,
|
||||
'removeInvalidTokens',
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
import { CompleteRequest } from '../api/utils/request.type';
|
||||
import { User } from '../database/user.entity';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class MockApiTokenGuard {
|
||||
private user: User;
|
||||
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req: CompleteRequest = context.switchToHttp().getRequest();
|
||||
if (!this.user) {
|
||||
// this assures that we can create the user 'hardcoded', if we need them before any calls are made or
|
||||
// create them on the fly when the first call to the api is made
|
||||
try {
|
||||
this.user = await this.usersService.getUserByUsername('hardcoded');
|
||||
} catch (e) {
|
||||
this.user = await this.usersService.createUser(
|
||||
'hardcoded',
|
||||
'Testy',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
req.user = this.user;
|
||||
return true;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue