add jwt & crypto service

This commit is contained in:
Paul Coral
2025-08-16 23:02:44 +02:00
parent 1f0a27a2e1
commit 3b304e4158
9 changed files with 177 additions and 30 deletions

View File

@@ -27,6 +27,7 @@
"@nestjs/core": "^11.0.1",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/passport-local": "^1.0.38",
"argon2": "^0.44.0",
"dayjs": "^1.11.13",
@@ -34,6 +35,7 @@
"ejs": "^3.1.10",
"i18next": "^25.3.2",
"i18next-fs-backend": "^2.6.0",
"jsonwebtoken": "^9.0.2",
"kysely": "^0.28.3",
"nodemailer": "^7.0.5",
"passport": "^0.7.0",

View File

@@ -1,20 +1,23 @@
import { Injectable } from '@nestjs/common';
import { AuthRegistry } from './auth-registry';
import { generateSecureToken } from '../../../lib/crypto';
import * as dayjs from 'dayjs';
import { isDefined, ResetAuth } from '@my-monorepo/common';
import { CryptoService } from '../../../services/core/crypto/crypto.service';
const RESET_TOKEN_VALIDITY_MINUTES = 15;
@Injectable()
export class AuthCoreService {
constructor(private authRegistry: AuthRegistry) {}
constructor(
private authRegistry: AuthRegistry,
private cryptoService: CryptoService,
) {}
async addNewResetToken(
userId: string,
): Promise<{ token: string; user_id: string }> {
const res = await this.authRegistry.addResetToken({
token: generateSecureToken(),
token: this.cryptoService.generateSecureToken(),
userId,
validUntil: dayjs().add(RESET_TOKEN_VALIDITY_MINUTES, 'minutes'),
});

View File

@@ -6,13 +6,14 @@ import {
import { UsersCoreService } from '../../users/users-core/users-core.service';
import { AuthCoreService } from '../auth-core/auth-core.service';
import { ResetAuth } from '@my-monorepo/common';
import { hashPassword } from '../../../lib/crypto';
import { CryptoService } from '../../../services/core/crypto/crypto.service';
@Injectable()
export class AuthControllerService {
constructor(
private userCoreService: UsersCoreService,
private authCoreService: AuthCoreService,
private cryptoService: CryptoService,
) {}
async verifyCredentials() {
@@ -30,7 +31,7 @@ export class AuthControllerService {
}
await this.userCoreService.setHashPassword(
resetData.userId,
await hashPassword(resetData.newPassword),
await this.cryptoService.hashPassword(resetData.newPassword),
);
}
}

View File

@@ -1,17 +0,0 @@
import * as crypto from 'crypto';
import * as argon2 from 'argon2';
export function generateSecureToken(length = 32): string {
return crypto.randomBytes(length).toString('hex');
}
export function hashPassword(password: string): Promise<string> {
return argon2.hash(password);
}
export function verifyPassword(
passwordHash: string,
clearTextPassword: string,
): Promise<boolean> {
return argon2.verify(passwordHash, clearTextPassword);
}

View File

@@ -6,6 +6,7 @@ import { LoggerService } from './logger/logger.service';
import { MailerService } from './mailer/mailer.service';
import { TemplateRendererService } from './template-renderer/template-renderer.service';
import { LinkBuilderService } from './link-builder/link-builder.service';
import { CryptoService } from './crypto/crypto.service';
const DECLARATIONS = [
DatabaseService,
@@ -15,6 +16,7 @@ const DECLARATIONS = [
MailerService,
TemplateRendererService,
LinkBuilderService,
CryptoService,
];
@Global()

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CryptoService } from './crypto.service';
describe('CryptoService', () => {
let service: CryptoService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CryptoService],
}).compile();
service = module.get<CryptoService>(CryptoService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import * as argon2 from 'argon2';
import * as jwt from 'jsonwebtoken';
import { EnvService } from '../env/env.service';
import { isDefined } from '@my-monorepo/common';
@Injectable()
export class CryptoService {
constructor(private envService: EnvService) {}
generateSecureToken(length = 32): string {
return crypto.randomBytes(length).toString('hex');
}
hashPassword(password: string): Promise<string> {
return argon2.hash(password);
}
verifyPassword(
passwordHash: string,
clearTextPassword: string,
): Promise<boolean> {
return argon2.verify(passwordHash, clearTextPassword);
}
createJwt(payload: string | object): string {
return jwt.sign(payload, this.envService.config.secrets.authSign);
}
async verifyJwt(token: string): Promise<string | object | undefined> {
return await new Promise<string | object | undefined>((resolve) => {
jwt.verify(
token,
this.envService.config.secrets.authSign,
(err, decoded) =>
resolve(!isDefined(err) && isDefined(decoded) ? decoded : undefined),
);
});
}
}

View File

@@ -14,6 +14,7 @@ const envParser = z
MAIL_USER: z.string().email(),
MAIL_PASSWORD: z.string(),
APP_FRONTEND_URL: z.string(),
SECRET_AUTH_SIGN: z.string(),
})
.transform((parsed) => ({
database: {
@@ -32,6 +33,9 @@ const envParser = z
app: {
frontendUrl: parsed.APP_FRONTEND_URL,
},
secrets: {
authSign: parsed.SECRET_AUTH_SIGN,
},
}));
export type EnvConfig = Readonly<z.infer<typeof envParser>>;