This commit is contained in:
2026-08-28 17:31:02 +02:00
commit 2b30e8bd39
694 changed files with 49243 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthController } from './controllers/AuthController';
import { AuthService } from './services/AuthService';
@Module({
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService]
})
export class AuthModule {}
@@ -0,0 +1,34 @@
import { Body, Controller, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import type { Request, Response } from 'express';
import { LoginDto } from '../dto/LoginDto';
import { AuthService } from '../services/AuthService';
import { ConfigService } from '@nestjs/config';
import { Config } from '../../../types/Config';
import { throttleProfiles } from '../../../config/throttleProfiles';
import { shouldUseSecureCookie } from '../../../utils/shouldUseSecureCookie';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService
) {}
@Post('login')
@Throttle(throttleProfiles.cmsLogin)
login(@Body() { password }: LoginDto, @Res() res: Response, @Req() req: Request) {
const { bearerCookie, cookieExpires } = this.authService.login(password);
const { nodeEnv } = this.configService.get('app') as Config['app'];
res.cookie('bearer_token', bearerCookie, {
httpOnly: true,
sameSite: 'strict',
secure: shouldUseSecureCookie(nodeEnv, req),
expires: cookieExpires
});
return res.sendStatus(HttpStatus.OK);
}
}
+7
View File
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
password: string;
}
@@ -0,0 +1,47 @@
import { UnauthorizedException } from '@nestjs/common';
import type { ConfigService } from '@nestjs/config';
import { AuthService } from './AuthService';
jest.mock('jsonwebtoken', () => ({
sign: jest.fn(() => 'signed-token')
}));
describe('AuthService', () => {
let service: AuthService;
let configService: {
get: jest.Mock;
};
beforeEach(() => {
configService = {
get: jest.fn((key: string) => {
if (key === 'app') {
return { cmsPassword: 'secret-password' };
}
if (key === 'jwt') {
return { secret: 'jwt-secret', expiresInMs: 3_600_000 };
}
return undefined;
})
};
service = new AuthService(configService as unknown as ConfigService);
});
it('rejects invalid passwords', () => {
expect(() => service.verifyPassword('wrong')).toThrow(new UnauthorizedException('Invalid credentials'));
});
it('accepts the configured cms password', () => {
expect(() => service.verifyPassword('secret-password')).not.toThrow();
});
it('returns a bearer cookie and expiry when login succeeds', () => {
const result = service.login('secret-password');
expect(result.bearerCookie).toBe('Bearer signed-token');
expect(result.cookieExpires).toBeInstanceOf(Date);
});
});
@@ -0,0 +1,31 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import dayjs from '../../../plugins/dayjs';
import * as jwt from 'jsonwebtoken';
import { Config } from '../../../types/Config';
@Injectable()
export class AuthService {
constructor(private readonly configService: ConfigService) {}
verifyPassword(password: string): void {
const { cmsPassword } = this.configService.get('app') as Config['app'];
if (password !== cmsPassword) {
throw new UnauthorizedException('Invalid credentials');
}
}
login(password: string): { bearerCookie: string; cookieExpires: Date } {
this.verifyPassword(password);
const { secret, expiresInMs } = this.configService.get('jwt') as Config['jwt'];
const token = jwt.sign({}, secret, { expiresIn: Math.floor(expiresInMs / 1000) });
return {
bearerCookie: `Bearer ${token}`,
cookieExpires: dayjs().add(expiresInMs, 'millisecond').toDate()
};
}
}