Mailservice

This commit is contained in:
Bastian Wagner
2025-01-20 13:17:00 +01:00
parent abb703f592
commit add2fd0240
19 changed files with 3071 additions and 67 deletions

View File

@@ -16,4 +16,13 @@ SSO_CLIENT_ID=
# SECURITY
JWT_SECRET=
JWT_EXPIRES_IN=10m
JWT_EXPIRES_IN=10m
# Mail
MAILER_HOST=
MAILER_PORT=
MAILER_SECURE=
MAILER_USERNAME=
MAILER_PASSWORD=
MAILER_FROM=''

View File

@@ -3,6 +3,9 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": true,
"assets": [
{ "include": "./templates/**", "outDir": "dist", "watchAssets": true }
]
}
}

2695
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
},
"dependencies": {
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^3.0.3",
"@nestjs/cache-manager": "^2.3.0",
"@nestjs/common": "^10.0.0",

View File

@@ -13,6 +13,8 @@ import { CylinderModule } from './modules/cylinder/cylinder.module';
import { SystemModule } from './modules/system/system.module';
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { MailModule } from './modules/mail/mail.module';
import { LogModule } from './modules/log/log.module';
@Module({
imports: [
@@ -29,6 +31,8 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
CustomerModule,
CylinderModule,
SystemModule,
MailModule,
LogModule,
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,33 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { KeySystem } from '../system.entity';
import { EmailEvent } from 'src/modules/log/log.service';
@Entity()
export class EmailLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@Column({ type: 'text'})
to: string;
@Column({ type: 'text'})
message: string;
@Column({ name: 'type', type: 'text',})
type: EmailEvent;
@ManyToOne(() => KeySystem, { nullable: true })
system: KeySystem;
@Column({type: 'boolean'})
success: boolean;
}

View File

@@ -0,0 +1 @@
export * from './email.log.entity';

View File

@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Repository, DataSource } from 'typeorm';
import { EmailLog } from 'src/model/entitites/log';
@Injectable()
export class EmailLogRepository extends Repository<EmailLog> {
constructor(dataSource: DataSource) {
super(EmailLog, dataSource.createEntityManager());
}
}

View File

@@ -0,0 +1 @@
export * from './email.log.repository';

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { LogService } from './log.service';
import { DatabaseModule } from 'src/shared/database/database.module';
@Module({
imports: [DatabaseModule],
providers: [LogService],
exports: [LogService]
})
export class LogModule {}

View File

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

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { EmailLogRepository } from 'src/model/repositories/log';
@Injectable()
export class LogService {
constructor(private readonly emailLogRepo: EmailLogRepository) {}
log(type: LogType, data: any) {
if (type == LogType.Mail) {
return this.logEmail(data);
}
}
private async logEmail(data: EmailLogDto) {
const log = this.emailLogRepo.create(data);
const logEntry = await this.emailLogRepo.save(log);
console.log(logEntry);
}
}
export enum LogType {
Mail
}
export enum EmailEvent {
GrantSystemAccess,
RemoveSystemAccess
}
export interface EmailLogDto {
success: boolean;
message: string;
to: string;
type: EmailEvent;
}

View File

@@ -0,0 +1,45 @@
import { MailerModule } from '@nestjs-modules/mailer';
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';
import { MailService } from './mail.service';
import { LogModule } from '../log/log.module';
@Module({
imports: [
LogModule,
MailerModule.forRootAsync({
imports: [ ],
inject: [ ConfigService ],
useFactory: async (config: ConfigService) => ({
transport: {
host: config.get('MAILER_HOST'),
secure: config.get('MAILER_SECURE'),
port: config.get('MAILER_PORT'),
auth: {
user: config.get('MAILER_USERNAME'),
pass: config.get('MAILER_PASSWORD'),
},
},
defaults: {
from: config.get('MAILER_FROM'),
},
template: {
dir: join(__dirname, '../../../templates'),
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
options: {
strict: true,
},
},
})
})
],
providers: [MailService],
exports: [MailService]
})
export class MailModule {
constructor() {
console.log(join(__dirname, '../../../templates'))
}
}

View File

@@ -0,0 +1,68 @@
import { MailerService } from "@nestjs-modules/mailer";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { EmailEvent, LogService, LogType } from "../log/log.service";
@Injectable()
export class MailService {
constructor(
private mailserService: MailerService,
private readonly configService: ConfigService,
private readonly logService: LogService,
) {
}
async sendAccessGrantedMail({to, firstName, systemName}: {to: string, firstName: string, systemName: string}) {
this.mailserService.sendMail({
template: './access',
to: to,
from: this.configService.get<string>('MAILER_FROM'),
subject: 'Zugriff gewährt',
context: {
firstName,
systemName
}
}).then(v => {
this.logService.log(LogType.Mail, {
to,
success: true,
message: v.response,
type: EmailEvent.GrantSystemAccess
})
}).catch(e => {
this.logService.log(LogType.Mail, {
to,
success: false,
message: e.response,
type: EmailEvent.GrantSystemAccess
})
})
}
sendAccessRemovedMail({to, firstName, systemName}: {to: string, firstName: string, systemName: string}) {
this.mailserService.sendMail({
template: './access-removed',
to: to,
from: this.configService.get<string>('MAILER_FROM'),
subject: 'Zugriff entzogen',
context: {
firstName,
systemName
}
}).then(v => {
this.logService.log(LogType.Mail, {
to,
success: true,
message: v.response,
type: EmailEvent.RemoveSystemAccess
})
}).catch(e => {
this.logService.log(LogType.Mail, {
to,
success: false,
message: e.response,
type: EmailEvent.RemoveSystemAccess
})
})
}
}

View File

@@ -3,10 +3,11 @@ import { SystemService } from './system.service';
import { SystemController } from './system.controller';
import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from 'src/shared/database/database.module';
import { MailModule } from '../mail/mail.module';
@Module({
controllers: [SystemController],
providers: [SystemService],
imports: [AuthModule, DatabaseModule],
imports: [AuthModule, DatabaseModule, MailModule],
})
export class SystemModule {}

View File

@@ -4,6 +4,7 @@ import { UpdateSystemDto } from './dto/update-system.dto';
import { ActivityRepository, KeySystemRepository, UserRepository } from 'src/model/repositories';
import { User } from 'src/model/entitites';
import { IUser } from 'src/model/interface';
import { MailService } from '../mail/mail.service';
@Injectable()
export class SystemService {
@@ -11,6 +12,7 @@ export class SystemService {
private systemRepo: KeySystemRepository,
private userRepo: UserRepository,
private systemActivityRepo: ActivityRepository,
private mailService: MailService
) {}
async create(user: User, createSystemDto: CreateSystemDto) {
@@ -80,6 +82,7 @@ export class SystemService {
sys.managers = sys.managers.filter( m => m.username != manageObject.email);
await this.systemRepo.save(sys);
this.mailService.sendAccessRemovedMail({to: manageObject.email, firstName: manageObject.email, systemName: sys.name})
return sys.managers;
}
@@ -94,6 +97,7 @@ export class SystemService {
sys.managers.push(user);
await this.systemRepo.save(sys);
this.mailService.sendAccessGrantedMail({to: user.username, firstName: user.firstName, systemName: sys.name})
return sys.managers;
}

View File

@@ -10,6 +10,7 @@ import {
SSOUser,
User,
} from 'src/model/entitites';
import { EmailLog } from 'src/model/entitites/log';
import { KeySystem } from 'src/model/entitites/system.entity';
import {
ActivityRepository,
@@ -22,6 +23,7 @@ import {
UserRepository,
} from 'src/model/repositories';
import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository';
import { EmailLogRepository } from 'src/model/repositories/log';
const ENTITIES = [
User,
@@ -33,6 +35,7 @@ const ENTITIES = [
Customer,
KeyHandout,
Activity,
EmailLog,
];
const REPOSITORIES = [
UserRepository,
@@ -44,6 +47,7 @@ const REPOSITORIES = [
CustomerRepository,
KeyHandoutRepository,
ActivityRepository,
EmailLogRepository
];
@Module({

View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zugriff entzogen</title>
<style>
/* General styles */
body {
font-family: 'Roboto', Arial, sans-serif;
background-color: #f5f5f5;
color: #424242;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background: #ffffff;
border-radius: 12px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background-color: #2196f3; /* Freundliches Blau */
color: #ffffff;
padding: 20px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 1.5rem;
}
.content {
padding: 24px 20px;
line-height: 1.6;
color: #424242;
}
.content p {
margin: 0 0 16px;
}
.button-container {
text-align: center;
margin: 24px 0;
}
.btn {
display: inline-block;
padding: 12px 24px;
font-size: 16px;
color: #ffffff;
background-color: #2196f3; /* Gleicher Blauton */
text-decoration: none;
border-radius: 24px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.btn:hover {
background-color: #1769aa; /* Dunkleres Blau für Hover */
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
}
.footer {
background-color: #f5f5f5;
text-align: center;
padding: 16px;
font-size: 0.875rem;
color: #757575;
}
.footer a {
color: #2196f3;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Von Schließanlage entfernt</h1>
</div>
<div class="content">
<p>Hallo {{firstName}},</p>
<p>Du wurdest als Verwalter folgender Schließanlage entfernt:</p>
<div class="button-container">
<a href="#" class="btn">{{systemName}}</a>
</div>
</div>
<div class="footer">
<p>Falls du Fragen hast, kontaktiere uns bitte <a href="mailto:support@example.com">hier</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zugriff gewährt</title>
<style>
/* General styles */
body {
font-family: 'Roboto', Arial, sans-serif;
background-color: #f5f5f5;
color: #424242;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background: #ffffff;
border-radius: 12px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background-color: #2196f3; /* Freundliches Blau */
color: #ffffff;
padding: 20px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 1.5rem;
}
.content {
padding: 24px 20px;
line-height: 1.6;
color: #424242;
}
.content p {
margin: 0 0 16px;
}
.button-container {
text-align: center;
margin: 24px 0;
}
.btn {
display: inline-block;
padding: 12px 24px;
font-size: 16px;
color: #ffffff;
background-color: #2196f3; /* Gleicher Blauton */
text-decoration: none;
border-radius: 24px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.btn:hover {
background-color: #1769aa; /* Dunkleres Blau für Hover */
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
}
.footer {
background-color: #f5f5f5;
text-align: center;
padding: 16px;
font-size: 0.875rem;
color: #757575;
}
.footer a {
color: #2196f3;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Zu Schließanlage hinzugefügt</h1>
</div>
<div class="content">
<p>Hallo {{firstName}},</p>
<p>Du wurdest als Verwalter zu folgender Schließanlage hinzugefügt:</p>
<div class="button-container">
<a href="#" class="btn">{{systemName}}</a>
</div>
</div>
<div class="footer">
<p>Falls du Fragen hast, kontaktiere uns bitte <a href="mailto:support@example.com">hier</a>.</p>
</div>
</div>
</body>
</html>