improve mail & structure
This commit is contained in:
@@ -81,7 +81,7 @@
|
||||
|
||||
<p style="text-align: center; margin: 30px 0">
|
||||
<a
|
||||
href="https://google.com"
|
||||
href="<%= resetLink %>"
|
||||
class="button"
|
||||
style="
|
||||
display: inline-block;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"argon2": "^0.44.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from './app/auth/auth.module';
|
||||
import { UsersModule } from './app/users/users.module';
|
||||
import { AuthFeatureModule } from './app/auth/auth-feature/auth.module';
|
||||
import { UsersFeatureModule } from './app/users/user-feature/users-feature.module';
|
||||
import { CoreModule } from './services/core/core.module';
|
||||
|
||||
@Module({
|
||||
imports: [CoreModule, AuthModule, UsersModule],
|
||||
imports: [CoreModule, AuthFeatureModule, UsersFeatureModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
10
apps/backend/src/app/auth/auth-core/auth-core.module.ts
Normal file
10
apps/backend/src/app/auth/auth-core/auth-core.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthCoreService } from './auth-core.service';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
|
||||
const PROVIDERS = [AuthCoreService, AuthRegistry];
|
||||
@Module({
|
||||
providers: PROVIDERS,
|
||||
exports: PROVIDERS,
|
||||
})
|
||||
export class AuthCoreModule {}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
import { generateSecureToken } from '../../lib/crypto';
|
||||
import { generateSecureToken } from '../../../lib/crypto';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
const RESET_TOKEN_VALIDITY_MINUTES = 15;
|
||||
@@ -9,7 +9,9 @@ const RESET_TOKEN_VALIDITY_MINUTES = 15;
|
||||
export class AuthCoreService {
|
||||
constructor(private authRegistry: AuthRegistry) {}
|
||||
|
||||
async addNewResetToken(userId: string): Promise<void> {
|
||||
async addNewResetToken(
|
||||
userId: string,
|
||||
): Promise<{ token: string; user_id: string }> {
|
||||
return this.authRegistry.addResetToken({
|
||||
token: generateSecureToken(),
|
||||
userId,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../services/core/database/database.service';
|
||||
import { DatabaseService } from '../../../services/core/database/database.service';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
export interface CreateResetToken {
|
||||
@@ -15,7 +15,9 @@ export class AuthRegistry {
|
||||
}
|
||||
constructor(private databaseService: DatabaseService) {}
|
||||
|
||||
async addResetToken(resetAuth: CreateResetToken): Promise<void> {
|
||||
async addResetToken(
|
||||
resetAuth: CreateResetToken,
|
||||
): Promise<{ token: string; user_id: string }> {
|
||||
return this.database
|
||||
.insertInto('reset_tokens')
|
||||
.values({
|
||||
@@ -23,7 +25,7 @@ export class AuthRegistry {
|
||||
token: resetAuth.token,
|
||||
valid_until: resetAuth.validUntil.toDate(),
|
||||
})
|
||||
.execute()
|
||||
.then(() => {});
|
||||
.returning(['user_id', 'token'])
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { UsersCoreService } from '../users/users-core.service';
|
||||
import { UsersCoreService } from '../../users/users-core/users-core.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthControllerService {
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
import { ZodValidationPipe } from '../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { ZodValidationPipe } from '../../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { ResetAuth, resetAuthParser } from '@my-monorepo/common';
|
||||
|
||||
@Controller('auth')
|
||||
12
apps/backend/src/app/auth/auth-feature/auth.module.ts
Normal file
12
apps/backend/src/app/auth/auth-feature/auth.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersCoreModule } from '../../users/users-core/users-core.module';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthCoreModule } from '../auth-core/auth-core.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersCoreModule, AuthCoreModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthControllerService],
|
||||
})
|
||||
export class AuthFeatureModule {}
|
||||
@@ -1,14 +0,0 @@
|
||||
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 {}
|
||||
@@ -3,12 +3,13 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
LOCALIZATION_SERVICE,
|
||||
LocalizationService,
|
||||
} 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';
|
||||
} 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/auth-core.service';
|
||||
import { UsersCoreService } from '../users-core/users-core.service';
|
||||
import { LinkBuilderService } from '../../../services/core/link-builder/link-builder.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersControllerService {
|
||||
@@ -19,16 +20,25 @@ export class UsersControllerService {
|
||||
private localizationService: LocalizationService,
|
||||
private templateRendererService: TemplateRendererService,
|
||||
private authCoreService: AuthCoreService,
|
||||
private linkBuilderService: LinkBuilderService,
|
||||
) {}
|
||||
|
||||
async createUser(newUser: CreateUser): Promise<void> {
|
||||
// FIXME : return result once auth is done
|
||||
const createUserResult = await this.userCoreService.addUser(newUser);
|
||||
if (createUserResult.ok) {
|
||||
const createdUser = createUserResult.data;
|
||||
await this.authCoreService.addNewResetToken(createdUser.id);
|
||||
if (!createUserResult.ok) {
|
||||
return;
|
||||
}
|
||||
const createdUser = createUserResult.data;
|
||||
const resetToken = await this.authCoreService.addNewResetToken(
|
||||
createdUser.id,
|
||||
);
|
||||
const resetLink = this.linkBuilderService.buildLinkToFrontend(['reset'], {
|
||||
userId: resetToken.user_id,
|
||||
token: resetToken.token,
|
||||
});
|
||||
|
||||
// build mail
|
||||
const mailTitle = this.localizationService.translate(
|
||||
'mail.create-user.title',
|
||||
);
|
||||
@@ -41,6 +51,7 @@ export class UsersControllerService {
|
||||
{
|
||||
title: mailTitle,
|
||||
bodyTitle: mailBodyTitle,
|
||||
resetLink: resetLink,
|
||||
},
|
||||
);
|
||||
void this.mailerService.sendEmail({
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersControllerService } from './users-controller.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersCoreModule } from '../users-core/users-core.module';
|
||||
import { AuthCoreModule } from '../../auth/auth-core/auth-core.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersCoreModule, AuthCoreModule],
|
||||
providers: [UsersControllerService],
|
||||
controllers: [UsersController],
|
||||
})
|
||||
export class UsersFeatureModule {}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CreateUser, createUserParser } from '@my-monorepo/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ZodValidationPipe } from '../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { ZodValidationPipe } from '../../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { UsersControllerService } from './users-controller.service';
|
||||
|
||||
@Controller('users')
|
||||
10
apps/backend/src/app/users/users-core/users-core.module.ts
Normal file
10
apps/backend/src/app/users/users-core/users-core.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersCoreService } from './users-core.service';
|
||||
import { UsersRegistry } from './users-registry';
|
||||
|
||||
const PROVIDERS = [UsersCoreService, UsersRegistry];
|
||||
@Module({
|
||||
providers: PROVIDERS,
|
||||
exports: PROVIDERS,
|
||||
})
|
||||
export class UsersCoreModule {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppError } from '../../lib/app-error';
|
||||
import { AppError } from '../../../lib/app-error';
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
export class UserEmailAlreadyExistError extends AppError {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../services/core/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';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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 {}
|
||||
@@ -5,6 +5,7 @@ import { LOCALIZATION_SERVICE_PROVIDER } from './localization/localization.servi
|
||||
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';
|
||||
|
||||
const DECLARATIONS = [
|
||||
DatabaseService,
|
||||
@@ -13,6 +14,7 @@ const DECLARATIONS = [
|
||||
LoggerService,
|
||||
MailerService,
|
||||
TemplateRendererService,
|
||||
LinkBuilderService,
|
||||
];
|
||||
|
||||
@Global()
|
||||
|
||||
@@ -13,6 +13,7 @@ const envParser = z
|
||||
MAIL_PORT: z.coerce.number(),
|
||||
MAIL_USER: z.string().email(),
|
||||
MAIL_PASSWORD: z.string(),
|
||||
APP_FRONTEND_URL: z.string(),
|
||||
})
|
||||
.transform((parsed) => ({
|
||||
database: {
|
||||
@@ -28,6 +29,9 @@ const envParser = z
|
||||
user: parsed.MAIL_USER,
|
||||
password: parsed.MAIL_PASSWORD,
|
||||
},
|
||||
app: {
|
||||
frontendUrl: parsed.APP_FRONTEND_URL,
|
||||
},
|
||||
}));
|
||||
|
||||
export type EnvConfig = Readonly<z.infer<typeof envParser>>;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LinkBuilderService } from './link-builder.service';
|
||||
|
||||
describe('LinkBuilderService', () => {
|
||||
let service: LinkBuilderService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [LinkBuilderService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LinkBuilderService>(LinkBuilderService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EnvService } from '../env/env.service';
|
||||
|
||||
@Injectable()
|
||||
export class LinkBuilderService {
|
||||
constructor(private envService: EnvService) {}
|
||||
|
||||
buildLinkToFrontend(
|
||||
path: string[],
|
||||
queryParams: Record<string, string> = {},
|
||||
) {
|
||||
const base = this.envService.config.app.frontendUrl;
|
||||
const segments = path.join('/');
|
||||
const queryParamsList = Object.entries(queryParams);
|
||||
const joinedQueryParam = queryParamsList
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&');
|
||||
const queryParamsStr =
|
||||
queryParamsList.length > 0 ? `?${joinedQueryParam}` : '';
|
||||
return `${base}/${segments}${queryParamsStr}`;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const MAIL_TEMPLATE_PATH_MAP = {
|
||||
export interface CreateAccountMailTemplateData {
|
||||
title: string;
|
||||
bodyTitle: string;
|
||||
resetLink: string;
|
||||
}
|
||||
|
||||
export function getMailTemplatePath(template: MailTemplateEnum): string {
|
||||
|
||||
Reference in New Issue
Block a user