This commit is contained in:
Bastian Wagner
2024-09-06 10:24:45 +02:00
parent d5850c38b1
commit bda298bb97
26 changed files with 2753 additions and 75 deletions

View File

@@ -13,4 +13,15 @@ JWT_EXPIRES_IN=10m
JWT_REFRESH_EXPIRES_IN=1w JWT_REFRESH_EXPIRES_IN=1w
SESSION_SECRET=MYSESSIONSECRET2024 SESSION_SECRET=MYSESSIONSECRET2024
APPLICATIONPORT=5000 APPLICATIONPORT=5000
# Mail
MAILER_HOST=smtp.smtp.de
MAILER_PORT=465
MAILER_SECURE=true
MAILER_USERNAME=xxxx
MAILER_PASSWORD=xxxxx
MAILER_FROM='"No Reply" <noreply@example.com>'
# Client
CLIENT_URL=http://localhost:4200

2444
idp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3", "@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",

View File

@@ -9,6 +9,7 @@ import { LoggerModule } from './core/logger.module';
import { SessionMiddleware } from './core/session.middleware'; import { SessionMiddleware } from './core/session.middleware';
import { ClientModule } from './client/client.module'; import { ClientModule } from './client/client.module';
import { ApplicationModule } from './application/application.module'; import { ApplicationModule } from './application/application.module';
import { MailModule } from './application/mail/mail.module';
@Module({ @Module({
imports: [ imports: [
AuthModule, AuthModule,
@@ -37,6 +38,7 @@ import { ApplicationModule } from './application/application.module';
}), }),
}), }),
ApplicationModule, ApplicationModule,
MailModule,
// TypeOrmModule.forRoot({ // TypeOrmModule.forRoot({
// type: 'mysql', // type: 'mysql',
// host: '85.215.137.185', // MySQL Hostname // host: '85.215.137.185', // MySQL Hostname

View File

@@ -3,10 +3,11 @@ import { ApplicationController } from './application.controller';
import { UserModule } from './user/user.module'; import { UserModule } from './user/user.module';
import { LoggerModule } from 'src/core/logger.module'; import { LoggerModule } from 'src/core/logger.module';
import { SecureModule } from 'src/core/secure/secure.module'; import { SecureModule } from 'src/core/secure/secure.module';
import { MailModule } from './mail/mail.module';
@Module({ @Module({
controllers: [ApplicationController], controllers: [ApplicationController],
providers: [], providers: [],
imports: [LoggerModule, UserModule, SecureModule], imports: [LoggerModule, UserModule, SecureModule, MailModule],
}) })
export class ApplicationModule {} export class ApplicationModule {}

View File

@@ -0,0 +1,41 @@
import { Module } from '@nestjs/common';
import { DatabaseModule } from 'src/core/database/database.module';
import { MailerModule } from '@nestjs-modules/mailer';
import { join } from 'path';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailService } from './mail.service';
@Module({
imports: [
DatabaseModule,
MailerModule.forRootAsync({
imports: [ConfigModule],
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 {}

View File

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

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MailerService } from '@nestjs-modules/mailer';
import { ResetPWMailConfig } from '../model/mailconfig.interface';
@Injectable()
export class MailService {
constructor(
private mailerService: MailerService,
private readonly configService: ConfigService,
) {
// this.sendMail();
}
sendResetMail(config: ResetPWMailConfig) {
let baseUrl = this.configService.get<string>('CLIENT_URL');
if (baseUrl.endsWith('/'))
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
baseUrl += '/' + config.url + '?resetcode=' + config.code;
this.mailerService.sendMail({
to: 'mail@bastian-wagner.de',
from: this.configService.get<string>('MAILER_FROM'),
subject: 'Passwort zurücksetzen',
template: './pw-reset',
context: {
name: config.name,
resetLink: baseUrl,
},
});
}
}

View File

@@ -0,0 +1,6 @@
export interface ResetPWMailConfig {
to: string;
code: string;
url: string;
name: string;
}

View File

@@ -1,11 +1,20 @@
import { Controller, Get, UseGuards } from '@nestjs/common'; import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/core/auth.guard'; import { UserService } from './user.service';
import { AuthGuard, Roles, RolesGuard } from 'src/core/secure/guards';
@UseGuards(AuthGuard) @UseGuards(AuthGuard, RolesGuard)
@Controller('app/user') @Controller('app/user')
export class UserController { export class UserController {
constructor(private userService: UserService) {}
@Get() @Get()
getIt() { getIt() {
return 'secure'; return 'secure';
} }
@Roles('admin')
@Get('clients')
getClients(@Req() req: any) {
return this.userService.getUserClients(req['user']);
}
} }

View File

@@ -1,9 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { SecureModule } from 'src/core/secure/secure.module'; import { SecureModule } from 'src/core/secure/secure.module';
import { UserService } from './user.service';
import { ClientRepository } from 'src/model/client.entity';
@Module({ @Module({
controllers: [UserController], controllers: [UserController],
imports: [SecureModule], imports: [SecureModule],
providers: [UserService, ClientRepository],
}) })
export class UserModule {} export class UserModule {}

View File

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

View File

@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ClientRepository } from 'src/model/client.entity';
import { User } from 'src/model/user.entity';
@Injectable()
export class UserService {
constructor(private clientRepository: ClientRepository) {}
getUserClients(user: User) {
console.log(user.id);
return this.clientRepository.find();
}
}

View File

@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
AuthorizationCode,
AuthorizationCodeRepository,
} from 'src/model/auth-code.entity';
import { Client, ClientRepository } from 'src/model/client.entity';
import { RedirectUri } from 'src/model/redirect-uri.entity';
import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity';
import { User, UserRepository } from 'src/model/user.entity';
const ENTITIES = [User, Client, RedirectUri, AuthorizationCode, SessionKey];
const REPOSITORIES = [
UserRepository,
ClientRepository,
AuthorizationCodeRepository,
SessionKeyRepository,
];
@Module({
imports: [TypeOrmModule.forFeature(ENTITIES)],
controllers: [],
providers: [...REPOSITORIES],
exports: [...REPOSITORIES],
})
export class DatabaseModule {}

View File

@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { Log } from 'src/model/log.entity'; import { Log } from 'src/model/log.entity';
import { CustomLogger } from './custom.logger'; import { CustomLogger } from './custom.logger';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseModule } from './database/database.module';
@Module({ @Module({
providers: [CustomLogger], providers: [CustomLogger],
controllers: [], controllers: [],
imports: [TypeOrmModule.forFeature([Log])], imports: [TypeOrmModule.forFeature([Log]), DatabaseModule],
exports: [CustomLogger], exports: [CustomLogger],
}) })
export class LoggerModule {} export class LoggerModule {}

View File

@@ -0,0 +1,3 @@
export * from './role.decorator';
export * from './auth.guard';
export * from './roles.guard';

View File

@@ -0,0 +1,3 @@
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

View File

@@ -0,0 +1,18 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { User } from 'src/model/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user: User = request.user;
return roles.includes(user.role.name);
}
}

View File

@@ -1,20 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsFormDataModule } from 'nestjs-form-data'; import { NestjsFormDataModule } from 'nestjs-form-data';
import { ClientService } from 'src/client/client.service'; import { ClientService } from 'src/client/client.service';
import {
AuthorizationCode,
AuthorizationCodeRepository,
} from 'src/model/auth-code.entity';
import { Client, ClientRepository } from 'src/model/client.entity';
import { RedirectUri } from 'src/model/redirect-uri.entity';
import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity';
import { User, UserRepository } from 'src/model/user.entity';
import { UsersService } from 'src/users/users.service'; import { UsersService } from 'src/users/users.service';
import { AuthGuard } from '../auth.guard';
import { LoggerModule } from '../logger.module'; import { LoggerModule } from '../logger.module';
import { AuthGuard } from './guards/auth.guard';
import { DatabaseModule } from '../database/database.module';
import { RolesGuard } from './guards/roles.guard';
@Module({ @Module({
imports: [ imports: [
@@ -27,24 +20,10 @@ import { LoggerModule } from '../logger.module';
}), }),
}), }),
NestjsFormDataModule, NestjsFormDataModule,
TypeOrmModule.forFeature([ DatabaseModule,
User,
Client,
RedirectUri,
AuthorizationCode,
SessionKey,
]),
LoggerModule, LoggerModule,
], ],
providers: [ providers: [UsersService, ClientService, AuthGuard, RolesGuard],
UsersService, exports: [JwtModule, UsersService, AuthGuard, RolesGuard],
ClientService,
UserRepository,
ClientRepository,
AuthorizationCodeRepository,
SessionKeyRepository,
AuthGuard,
],
exports: [JwtModule, UsersService, AuthGuard],
}) })
export class SecureModule {} export class SecureModule {}

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Passwort zurücksetzen</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
background: #007bff;
color: #fff;
padding: 20px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.header h1 {
margin: 0;
}
.content {
padding: 20px;
}
.content p {
line-height: 1.6;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 20px 0;
font-size: 16px;
color: #fff;
background-color: #007bff;
text-decoration: none;
border-radius: 5px;
text-align: center;
}
.link {
word-break: break-all;
color: #007bff;
}
.footer {
text-align: center;
font-size: 0.9em;
color: #666;
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #ddd;
}
.social-icons {
margin: 10px 0;
}
.social-icons img {
width: 30px;
height: 30px;
margin: 0 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Passwort zurücksetzen</h1>
</div>
<div class="content">
<p>Hallo {{name}},</p>
<p>du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt. Bitte klicke auf den folgenden Button, um dein Passwort zurückzusetzen:</p>
<p style="text-align: center;">
<a href="{{resetLink}}" class="btn">Passwort zurücksetzen</a>
</p>
<p style="margin-top: 12px;">Falls der Button nicht funktioniert, kannst du auch den folgenden Link in deinen Browser kopieren:</p>
<p class="link">{{resetLink}}</p>
<p>Dieser Link ist 24 Stunden gültig. Wenn du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail bitte. Dein Passwort bleibt unverändert.</p>
</div>
</div>
</body>
</html>

View File

@@ -2,9 +2,10 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideHotToastConfig } from '@ngxpert/hot-toast'; import { provideHotToastConfig } from '@ngxpert/hot-toast';
import { authInterceptor } from './core/interceptor/auth.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), provideHotToastConfig(),] providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), provideHotToastConfig(),]
}; };

View File

@@ -7,6 +7,6 @@ import { SessionKeyGuard } from './core/guards/session-key.guard';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent, canActivate: [SessionKeyGuard] }, { path: 'login', component: LoginComponent, canActivate: [SessionKeyGuard] },
{ path: 'register', component: RegisterComponent }, { path: 'register', component: RegisterComponent },
{ path: 'dashboard', component: DashboardComponent }, { path: 'dashboard', component: DashboardComponent, canActivate: [SessionKeyGuard] },
{ path: '', component: LoginComponent, canActivate: [SessionKeyGuard] }, { path: '', component: LoginComponent, canActivate: [SessionKeyGuard] },
]; ];

View File

@@ -19,6 +19,8 @@ export class SessionKeyGuard {
private client_id; private client_id;
async canActivate(route: ActivatedRouteSnapshot): async canActivate(route: ActivatedRouteSnapshot):
Promise<boolean> { Promise<boolean> {
if (this.userService.user) { return true; }
this.isLoading = true; this.isLoading = true;
const success = await this.loginWithSessionId(route); const success = await this.loginWithSessionId(route);
@@ -68,7 +70,7 @@ export class SessionKeyGuard {
} }
} else if (data["id"] != null) { } else if (data["id"] != null) {
this.userService.user = data as User; this.userService.user = data as User;
resolve(false); resolve(true);
this.navigateToDashboard(); this.navigateToDashboard();
} }
} }

View File

@@ -0,0 +1,20 @@
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from "@angular/common/http";
import { inject } from "@angular/core";
import { Observable } from "rxjs";
import { UserService } from "../../auth/user.service";
export const authInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn
): Observable<HttpEvent<any>> => {
const userService = inject(UserService);
const token = userService.user?.accessToken;
if (token) {
const cloned = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next(cloned);
} else {
return next(req);
}
};

View File

@@ -1,6 +1,7 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { UserService } from '../auth/user.service'; import { UserService } from '../auth/user.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@@ -13,12 +14,24 @@ export class DashboardComponent implements OnInit {
private userService: UserService = inject(UserService); private userService: UserService = inject(UserService);
private router: Router = inject(Router); private router: Router = inject(Router);
private http: HttpClient = inject(HttpClient);
ngOnInit(): void { ngOnInit(): void {
console.log("ONINIT")
if (!this.userService.user) { if (!this.userService.user) {
console.log("REDIRECT")
this.router.navigateByUrl("/login"); this.router.navigateByUrl("/login");
return;
} }
this.load();
}
load() {
this.http.get('api/app/user/clients').subscribe(res => {
console.log(res)
})
} }