refactor module & reset token

This commit is contained in:
Paul Coral
2025-08-10 22:41:34 +02:00
parent ba90bf221b
commit 4b8348e32f
39 changed files with 386 additions and 67 deletions

View File

@@ -25,7 +25,9 @@
"@my-monorepo/common": "workspace:*",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@types/passport-local": "^1.0.38",
"dayjs": "^1.11.13",
"dotenv": "^17.2.1",
"ejs": "^3.1.10",
@@ -33,6 +35,8 @@
"i18next-fs-backend": "^2.6.0",
"kysely": "^0.28.3",
"nodemailer": "^7.0.5",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",

View File

@@ -1,27 +1,9 @@
import { Module } from '@nestjs/common';
import { AuthController } from './app/auth/auth.controller';
import { DatabaseService } from './services/database/database.service';
import { AuthService } from './app/auth/auth.service';
import { UsersService } from './app/users/users.service';
import { EnvService } from './services/env/env.service';
import { UsersRegistry } from './app/users/users-registry';
import { UsersController } from './app/users/users.controller';
import { MailerService } from './services/mailer/mailer.service';
import { LOCALIZATION_SERVICE_PROVIDER } from './services/localization/localization.service';
import { TemplateRendererService } from './services/template-renderer/template-renderer.service';
import { AuthModule } from './app/auth/auth.module';
import { UsersModule } from './app/users/users.module';
import { CoreModule } from './services/core/core.module';
@Module({
imports: [],
controllers: [AuthController, UsersController],
providers: [
DatabaseService,
AuthService,
UsersService,
EnvService,
UsersRegistry,
MailerService,
LOCALIZATION_SERVICE_PROVIDER,
TemplateRendererService,
],
imports: [CoreModule, AuthModule, UsersModule],
})
export class AppModule {}

View File

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

View File

@@ -0,0 +1,14 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersCoreService } from '../users/users-core.service';
@Injectable()
export class AuthControllerService {
constructor(private userCoreService: UsersCoreService) {}
async verifyCredentials() {
const isValidCredential = await this.userCoreService.existsEmailPassword();
if (!isValidCredential) {
throw new UnauthorizedException('invalid credentials');
}
}
}

View File

@@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { AuthCoreService } from './auth-core.service';
describe('UsersService', () => {
let service: UsersService;
describe('AuthCoreService', () => {
let service: AuthCoreService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
providers: [AuthCoreService],
}).compile();
service = module.get<UsersService>(UsersService);
service = module.get<AuthCoreService>(AuthCoreService);
});
it('should be defined', () => {

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { AuthRegistry } from './auth-registry';
import { generateSecureToken } from '../../lib/crypto';
import * as dayjs from 'dayjs';
const RESET_TOKEN_VALIDITY_MINUTES = 15;
@Injectable()
export class AuthCoreService {
constructor(private authRegistry: AuthRegistry) {}
async addNewResetToken(userId: string): Promise<void> {
return this.authRegistry.addResetToken({
token: generateSecureToken(),
userId,
validUntil: dayjs().add(RESET_TOKEN_VALIDITY_MINUTES, 'minutes'),
});
}
}

View File

@@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { AuthRegistry } from './auth-registry';
describe('AuthService', () => {
let service: AuthService;
describe('AuthRegistry', () => {
let service: AuthRegistry;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
providers: [AuthRegistry],
}).compile();
service = module.get<AuthService>(AuthService);
service = module.get<AuthRegistry>(AuthRegistry);
});
it('should be defined', () => {

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../services/core/database/database.service';
import * as dayjs from 'dayjs';
export interface CreateResetToken {
userId: string;
token: string;
validUntil: dayjs.Dayjs;
}
@Injectable()
export class AuthRegistry {
get database() {
return this.databaseService.database;
}
constructor(private databaseService: DatabaseService) {}
async addResetToken(resetAuth: CreateResetToken): Promise<void> {
return this.database
.insertInto('reset_tokens')
.values({
user_id: resetAuth.userId,
token: resetAuth.token,
valid_until: resetAuth.validUntil.toDate(),
})
.execute()
.then(() => {});
}
}

View File

@@ -1,12 +1,26 @@
import { Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import {
Body,
Controller,
NotImplementedException,
Post,
} from '@nestjs/common';
import { AuthControllerService } from './auth-controller.service';
import { ZodValidationPipe } from '../../lib/nestjs/zod-validation/zod-validation.pipe';
import { ResetAuth, resetAuthParser } from '@my-monorepo/common';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
constructor(private authService: AuthControllerService) {}
@Post('credentials')
async postCredentials() {
return this.authService.verifyCredentials();
}
@Post('reset')
async postReset(
@Body(new ZodValidationPipe(resetAuthParser)) resetData: ResetAuth,
): Promise<void> {
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthRegistry } from './auth-registry';
import { AuthControllerService } from './auth-controller.service';
import { AuthCoreService } from './auth-core.service';
import { UsersModule } from '../users/users.module';
@Module({
controllers: [AuthController],
providers: [AuthRegistry, AuthControllerService, AuthCoreService],
imports: [UsersModule],
exports: [AuthCoreService, AuthRegistry],
})
export class AuthModule {}

View File

@@ -1,17 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersRegistry } from '../users/users-registry';
@Injectable()
export class AuthService {
constructor(private userRegistry: UsersRegistry) {}
async verifyCredentials() {
const isValidCredential = await this.userRegistry.existsEmailPassword(
'',
'',
);
if (!isValidCredential) {
throw new UnauthorizedException('invalid credentials');
}
}
}

View File

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

View File

@@ -1,27 +1,33 @@
import { CreateUser } from '@my-monorepo/common';
import { Inject, Injectable } from '@nestjs/common';
import { MailerService } from '../../services/mailer/mailer.service';
import { UsersRegistry } from './users-registry';
import {
LOCALIZATION_SERVICE,
LocalizationService,
} from '../../services/localization/localization.service';
import { TemplateRendererService } from '../../services/template-renderer/template-renderer.service';
import { MailTemplateEnum } from '../../services/template-renderer/mail-template-data';
} from '../../services/core/localization/localization.service';
import { MailerService } from '../../services/core/mailer/mailer.service';
import { MailTemplateEnum } from '../../services/core/template-renderer/mail-template-data';
import { TemplateRendererService } from '../../services/core/template-renderer/template-renderer.service';
import { AuthCoreService } from '../auth/auth-core.service';
import { UsersCoreService } from './users-core.service';
@Injectable()
export class UsersService {
export class UsersControllerService {
constructor(
private userRegistry: UsersRegistry,
private userCoreService: UsersCoreService,
private mailerService: MailerService,
@Inject(LOCALIZATION_SERVICE)
private localizationService: LocalizationService,
private templateRendererService: TemplateRendererService,
private authCoreService: AuthCoreService,
) {}
async createUser(newUser: CreateUser): Promise<void> {
// FIXME : return result once auth is done
await this.userRegistry.addUser(newUser);
const createUserResult = await this.userCoreService.addUser(newUser);
if (createUserResult.ok) {
const createdUser = createUserResult.data;
await this.authCoreService.addNewResetToken(createdUser.id);
}
const mailTitle = this.localizationService.translate(
'mail.create-user.title',

View File

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

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { UsersRegistry } from './users-registry';
import { CreateUser, Result, User } from '@my-monorepo/common';
import { UserEmailAlreadyExistError } from './users-error';
@Injectable()
export class UsersCoreService {
constructor(private userRegistry: UsersRegistry) {}
async existsEmailPassword() {
return await this.userRegistry.existsEmailPassword('', '');
}
async addUser(
newUser: CreateUser,
): Promise<Result<User, UserEmailAlreadyExistError>> {
return await this.userRegistry.addUser(newUser);
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../services/database/database.service';
import { DatabaseService } from '../../services/core/database/database.service';
import { CreateUser, isDefined, Result, User } from '@my-monorepo/common';
import { UserEmailAlreadyExistError } from './users-error';

View File

@@ -1,15 +1,16 @@
import { CreateUser, createUserParser } from '@my-monorepo/common';
import { Body, Controller, Post, UsePipes } from '@nestjs/common';
import { Body, Controller, Post } from '@nestjs/common';
import { ZodValidationPipe } from '../../lib/nestjs/zod-validation/zod-validation.pipe';
import { UsersService } from './users.service';
import { UsersControllerService } from './users-controller.service';
@Controller('users')
export class UsersController {
constructor(private userService: UsersService) {}
constructor(private userService: UsersControllerService) {}
@Post()
@UsePipes(new ZodValidationPipe(createUserParser))
async createUser(@Body() newUser: CreateUser) {
async createUser(
@Body(new ZodValidationPipe(createUserParser)) newUser: CreateUser,
) {
return this.userService.createUser(newUser);
}
}

View File

@@ -0,0 +1,14 @@
import { forwardRef, Module } from '@nestjs/common';
import { UsersRegistry } from './users-registry';
import { UsersController } from './users.controller';
import { UsersControllerService } from './users-controller.service';
import { UsersCoreService } from './users-core.service';
import { AuthModule } from '../auth/auth.module';
@Module({
providers: [UsersRegistry, UsersControllerService, UsersCoreService],
controllers: [UsersController],
imports: [forwardRef(() => AuthModule)],
exports: [UsersCoreService, UsersRegistry],
})
export class UsersModule {}

View File

@@ -13,6 +13,16 @@ export type Int8 = ColumnType<string, bigint | number | string, bigint | number
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export interface ResetTokens {
created_at: Generated<Timestamp>;
deleted_at: Timestamp | null;
id: Generated<Int8>;
token: string;
updated_at: Generated<Timestamp>;
user_id: Generated<Int8>;
valid_until: Timestamp;
}
export interface Users {
created_at: Generated<Timestamp>;
deleted_at: Timestamp | null;
@@ -25,5 +35,6 @@ export interface Users {
}
export interface DB {
reset_tokens: ResetTokens;
users: Users;
}

View File

@@ -0,0 +1,5 @@
import * as crypto from 'crypto';
export function generateSecureToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}

View File

@@ -0,0 +1,33 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('reset_tokens')
.addColumn('id', 'bigserial', (col) => col.primaryKey())
.addColumn('user_id', 'bigserial', (col) => col.notNull())
.addForeignKeyConstraint(
'fk_user_id_reset_tokens_users',
['user_id'],
'users',
['id'],
(col) => col.onDelete('cascade'),
)
.addColumn('token', 'text', (col) => col.notNull())
.addUniqueConstraint('uniq_user_id_tokens_reset_tokens', [
'user_id',
'token',
])
.addColumn('valid_until', 'timestamptz', (col) => col.notNull())
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('updated_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('deleted_at', 'timestamptz')
.execute();
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('reset_tokens').execute();
}

View File

@@ -0,0 +1,23 @@
import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database/database.service';
import { EnvService } from './env/env.service';
import { LOCALIZATION_SERVICE_PROVIDER } from './localization/localization.service';
import { LoggerService } from './logger/logger.service';
import { MailerService } from './mailer/mailer.service';
import { TemplateRendererService } from './template-renderer/template-renderer.service';
const DECLARATIONS = [
DatabaseService,
EnvService,
LOCALIZATION_SERVICE_PROVIDER,
LoggerService,
MailerService,
TemplateRendererService,
];
@Global()
@Module({
providers: DECLARATIONS,
exports: DECLARATIONS,
})
export class CoreModule {}

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';
import { DB } from '../../database/db';
import { DB } from '../../../database/db';
import { EnvService } from '../env/env.service';
@Injectable()