init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user