Added frontend

This commit is contained in:
Sven Heidemann 2022-02-20 19:07:41 +01:00
commit 4c21fa631a
155 changed files with 31276 additions and 0 deletions

18
.browserslistrc Normal file
View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

66
.gitignore vendored Normal file
View File

@ -0,0 +1,66 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
*/dist
*/tmp
*/out-tsc
# Only exists if Bazel was run
*/bazel-out
# dependencies
*node_modules*
# profiling files
*chrome-profiler-events*.json
*speed-measure-plugin*.json
# IDEs and editors
*/.idea
*.project
*.classpath
*.c9/
*.launch
*.settings/
*.sublime-workspace
# IDE - VSCode
*.vscode/*
!*.vscode/settings.json
!*.vscode/tasks.json
!*.vscode/launch.json
!*.vscode/extensions.json
*.history/*
# misc
*/.sass-cache
*/connect.lock
*/coverage
*/libpeerconnection.log
*npm-debug.log
*yarn-error.log
*testem.log
*/typings
# System Files
*.DS_Store
*Thumbs.db
# .Net Env
*Debug*
obj
logs
-nlog.txt
.vs
.vscode
# Python Env
*__pycache__*
*.pyc
*.idea*
*.log
# angular & .net
dist
bin
.angular
appsettings.*.json

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 sh-edraft.de
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# Web-App template

140
angular.json Normal file
View File

@ -0,0 +1,140 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"gswi": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "gswi",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/gswi",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.gswi.json",
"assets": [
"src/assets/images/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss",
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/themes/saga-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css"
],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "gswi:build"
},
"configurations": {
"production": {
"browserTarget": "gswi:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "gswi:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "gswi:serve"
},
"configurations": {
"production": {
"devServerTarget": "gswi:serve:production"
}
}
}
}
}
},
"defaultProject": "app",
"cli": {
"analytics": "dcc5d68a-d438-4579-bac1-5bc2d574bd1a"
}
}

36
e2e/protractor.conf.js Normal file
View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

23
e2e/src/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('app app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

14
e2e/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
karma.conf.js Normal file
View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/app'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

23382
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "gswi",
"version": "1.0.0",
"scripts": {
"ng": "ng",
"update-version": "ts-node -O '{\"module\": \"commonjs\"}' update-version.ts",
"#prestart": "npm run update-version",
"start": "ng serve",
"#prebuild": "npm run update-version",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.2.3",
"@angular/cdk": "^13.2.3",
"@angular/common": "~13.2.3",
"@angular/compiler": "~13.2.3",
"@angular/core": "~13.2.3",
"@angular/forms": "~13.2.3",
"@angular/platform-browser": "~13.2.3",
"@angular/platform-browser-dynamic": "~13.2.3",
"@angular/router": "~13.2.3",
"@aspnet/signalr": "^3.0.0-preview6.19307.2",
"@auth0/angular-jwt": "^5.0.2",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"primeicons": "^5.0.0",
"primeng": "^13.2.0",
"rxjs": "^7.5.4",
"ts-lint": "^4.5.1",
"tslib": "^2.3.1",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.2.4",
"@angular/cli": "~13.2.4",
"@angular/compiler-cli": "~13.2.3",
"@angular/localize": "^13.2.3",
"@types/jasmine": "^3.10.3",
"@types/jasminewd2": "^2.0.10",
"@types/node": "^17.0.18",
"codelyzer": "^6.0.2",
"jasmine-core": "~4.0.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.3.16",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.7.0",
"protractor": "~7.0.0",
"ts-node": "~10.5.0",
"tslint": "~6.1.0",
"typescript": "~4.5.5"
}
}

View File

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NotFoundComponent } from './components/error/not-found/not-found.component';
import { HomeComponent } from './components/home/home.component';
import { AuthRoles } from './models/auth/auth-roles.enum';
import { AuthGuard } from './modules/shared/guards/auth/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent, pathMatch: 'full' },
{ path: 'change-password', loadChildren: () => import('./modules/view/change-password/change-password.module').then(m => m.ChangePasswordModule), canActivate: [AuthGuard] },
{ path: 'user-settings', loadChildren: () => import('./modules/view/user-settings/user-settings.module').then(m => m.UserSettingsModule), canActivate: [AuthGuard] },
{ path: 'auth', loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule) },
{ path: 'admin/settings', loadChildren: () => import('./modules/admin/settings/settings.module').then(m => m.SettingsModule), canActivate: [AuthGuard], data: { role: AuthRoles.Admin } },
{ path: 'admin/users', loadChildren: () => import('./modules/admin/auth-users/auth-user.module').then(m => m.AuthUserModule), canActivate: [AuthGuard], data: { role: AuthRoles.Admin } },
{ path: '404', component: NotFoundComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,36 @@
<main [class]="themeService.themeName">
<ng-container *ngIf="authService.isLoggedIn; else login">
<app-header (isSidebarFullWidth)="themeService.setSideWidth($event)"></app-header>
<section class="app">
<div>
<section class="sidebar" [style.width]="themeService.sidebarWidth">
<app-sidebar [isSidebarOpen]="themeService.isSidebarOpen"></app-sidebar>
</section>
</div>
<div class="component-wrapper">
<section class="component">
<router-outlet></router-outlet>
</section>
</div>
</section>
<app-footer></app-footer>
</ng-container>
<ng-template #login>
<router-outlet></router-outlet>
</ng-template>
<app-spinner></app-spinner>
<p-confirmDialog #cd key="confirmConfirmationDialog" [baseZIndex]="10000">
<ng-template pTemplate="footer">
<div class="wrapper-right btn-wrapper">
<button pButton label="{{'dialog.abort' | translate}}" class="btn icon-btn danger-icon-btn" icon="pi pi-times-circle" (click)="cd.reject()"></button>
<button pButton label="{{'dialog.confirm' | translate}}" class="btn" icon="pi pi-check-circle" (click)="cd.accept()"></button>
</div>
</ng-template>
</p-confirmDialog>
<p-toast></p-toast>
</main>

View File

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'app'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('app');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('app app is running!');
});
});

24
src/app/app.component.ts Normal file
View File

@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { AuthService } from './services/auth/auth.service';
import { SignalRService } from './services/signalr/signalr.service';
import { ThemeService } from './services/theme/theme.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor(
public authService: AuthService,
public themeService: ThemeService,
private signalr: SignalRService,
) { }
ngOnInit(): void {
this.signalr.startSignalR();
this.themeService.loadTheme();
this.themeService.loadMenu();
}
}

83
src/app/app.module.ts Normal file
View File

@ -0,0 +1,83 @@
import { HttpClient } from '@angular/common/http';
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { JwtModule } from '@auth0/angular-jwt';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { ConfirmationService, MessageService } from 'primeng/api';
import { DialogService } from 'primeng/dynamicdialog';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { SpinnerComponent } from './components/spinner/spinner.component';
import { SharedModule } from './modules/shared/shared.module';
import { ErrorHandlerService } from './services/error-handler/error-handler.service';
import { SettingsService } from './services/settings/settings.service';
import { NotFoundComponent } from './components/error/not-found/not-found.component';
import { HomeComponent } from './components/home/home.component';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
SidebarComponent,
FooterComponent,
SpinnerComponent,
NotFoundComponent,
HomeComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
SharedModule,
JwtModule.forRoot({
config: {
tokenGetter,
allowedDomains: ['localhost:5000', 'localhost:5001'],
disallowedRoutes: []
}
}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: configurationFactory,
deps: [SettingsService],
multi: true
},
{
provide: ErrorHandler,
useClass: ErrorHandlerService
},
MessageService,
ConfirmationService,
DialogService
],
bootstrap: [AppComponent]
})
export class AppModule { }
export function configurationFactory(settingsService: SettingsService): () => Promise<unknown> {
return (): Promise<unknown> => {
return settingsService.loadSettings();
};
}
export function tokenGetter(): string {
return localStorage.getItem('jwt');
}
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
}

View File

@ -0,0 +1,15 @@
<div class="content-row">
<div class="content-column">
</div>
</div>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'common.error' | translate}}
</h2>
</div>
<div class="content">
{{'common.404' | translate}}
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NotFoundComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-not-found',
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.scss']
})
export class NotFoundComponent implements OnInit {
constructor(
public location: Location
) { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,26 @@
<footer>
<div class="left">
<div class="frontend-version">
<span>
{{'footer.frontend' | translate}}:
</span>
<span>
{{frontendVersion.getVersionString()}}
</span>
</div>
<span class="version-divider">
|
</span>
<div class="backend-version">
<span>
{{'footer.backend' | translate}}:
</span>
<span>
{{backendVersion.getVersionString()}}
</span>
</div>
</div>
<div class="right">
<a href="https://www.sh-edraft.de/Impressum" target="_blank">{{'footer.imprint' | translate}}</a>
</div>
</footer>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,46 @@
import { Component, OnInit } from '@angular/core';
import { type } from 'os';
import { catchError } from 'rxjs/operators';
import { SoftwareVersion } from 'src/app/models/config/software-version';
import { GuiService } from 'src/app/services/gui/gui.service';
import { SettingsService } from 'src/app/services/settings/settings.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
frontendVersion: SoftwareVersion;
backendVersion: SoftwareVersion;
constructor(
private settings: SettingsService,
private guiService: GuiService,
private spinnerService: SpinnerService
) { }
ngOnInit(): void {
this.frontendVersion = this.settings.getWebVersion();
this.spinnerService.showSpinner();
this.guiService.getApiVersion()
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
throw err;
}))
.subscribe(version => {
this.spinnerService.hideSpinner();
const webVersion = new SoftwareVersion(
version.major,
version.minor,
version.micro
);
this.backendVersion = webVersion;
});
}
}

View File

@ -0,0 +1,25 @@
<header>
<div class="logo-button-wrapper">
<button pButton type="button" icon="pi pi-bars" class="btn p-button-text" (click)="toggleMenu()"></button>
</div>
<div class="logo">
<h1 class="app-name">GSWI</h1>
</div>
<div class="header-menu logo-button-wrapper">
<div class="logo-button-wrapper">
<button type="button" pButton icon="pi pi-globe" class="btn icon-btn p-button-text"
(click)="langMenu.toggle($event)"></button>
<p-menu #langMenu [popup]="true" [model]="langList" class="lang-menu"></p-menu>
</div>
<div class="logo-button-wrapper">
<button type="button" pButton icon="pi pi-palette" class="btn icon-btn p-button-text"
(click)="themeMenu.toggle($event)"></button>
<p-menu #themeMenu [popup]="true" [model]="themeList" class="theme-menu"></p-menu>
</div>
<div class="logo-button-wrapper">
<button type="button" pButton icon="pi pi-user" class="btn icon-btn p-button-text"
(click)="userMenu.toggle($event)"></button>
<p-menu #userMenu [popup]="true" [model]="userMenuList" class="user-menu"></p-menu>
</div>
</div>
</header>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,168 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { MenuItem, PrimeNGConfig } from 'primeng/api';
import { catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/services/auth/auth.service';
import { SettingsService } from 'src/app/services/settings/settings.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
import { ThemeService } from 'src/app/services/theme/theme.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
@Output() isSidebarFullWidth: EventEmitter<boolean> = new EventEmitter<boolean>(this.themeService.isSidebarOpen);
langList: MenuItem[] = [];
themeList: MenuItem[] = [];
userMenuList: MenuItem[];
constructor(
private authService: AuthService,
private router: Router,
private themeService: ThemeService,
private spinnerService: SpinnerService,
private settings: SettingsService,
private translateService: TranslateService,
private config: PrimeNGConfig
) { }
ngOnInit(): void {
this.translateService.setDefaultLang('en');
this.initMenuLists();
this.loadLang();
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.initUserMenuList();
});
}
initUserMenuList(): void {
this.spinnerService.showSpinner();
const mail = this.authService.getEMailFromDecodedToken(this.authService.getDecodedToken());
this.authService.getUserByEMail(mail)
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
this.authService.logout();
throw err;
}))
.subscribe(user => {
this.spinnerService.hideSpinner();
this.userMenuList = [
{
label: `${user.firstName} ${user.lastName}`,
disabled: true
},
{
separator: true
},
{
label: this.translateService.instant('header.change_password'), command: () => {
this.changePassword();
},
icon: 'pi pi-key'
},
{
label: this.translateService.instant('header.settings'), command: () => {
this.userSettings();
},
icon: 'pi pi-cog'
},
{
label: this.translateService.instant('header.logout'), command: () => {
this.logout();
},
icon: 'pi pi-sign-out'
}
];
});
}
initMenuLists(): void {
this.langList = [
{
label: 'English', command: () => {
this.translate('en');
this.setLang('en');
},
},
{
label: 'Deutsch', command: () => {
this.translate('de');
this.setLang('de');
},
},
];
this.initUserMenuList();
this.settings.getThemes().forEach(theme => {
this.themeList.push({
label: theme.Label,
command: () => {
this.changeTheme(theme.Name);
}
});
});
}
toggleMenu(): void {
this.themeService.setIsMenuOpen(!this.themeService.isSidebarOpen);
this.isSidebarFullWidth.emit(this.themeService.isSidebarOpen);
}
changeTheme(name: string): void {
this.themeService.setTheme(name);
}
changePassword(): void {
this.router.navigate(['/change-password']);
}
userSettings(): void {
this.router.navigate(['/user-settings']);
}
logout(): void {
this.authService.logout();
}
translate(lang: string) {
this.translateService.use(lang);
this.translateService.get('primeng').subscribe(res => this.config.setTranslation(res));
}
loadLang(): void {
const token = this.authService.getDecodedToken();
const mail = this.authService.getEMailFromDecodedToken(token);
if (!mail) {
this.translate('en');
return;
}
let lang = localStorage.getItem(`${mail}_lang`);
if (!lang) {
lang = 'en';
this.setLang(lang);
}
this.translate(lang);
}
setLang(lang: string): void {
this.authService.isUserLoggedInAsync().then(result => {
if (!result) {
return;
}
const token = this.authService.getDecodedToken();
const mail = this.authService.getEMailFromDecodedToken(token);
localStorage.setItem(`${mail}_lang`, lang);
});
}
}

View File

@ -0,0 +1 @@
<p>home works!</p>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,3 @@
<div class="menu">
<p-menu [model]="menuItems"></p-menu>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SidebarComponent } from './sidebar.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SidebarComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,52 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { MenuItem } from 'primeng/api';
import { AuthRoles } from 'src/app/models/auth/auth-roles.enum';
import { AuthService } from 'src/app/services/auth/auth.service';
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent implements OnInit, OnChanges {
@Input() isSidebarOpen: boolean;
menuItems: MenuItem[];
constructor(
private authService: AuthService,
private translateService: TranslateService
) { }
ngOnInit(): void {
this.translateService.onLangChange.subscribe(async (event: LangChangeEvent) => {
await this.setMenu(this.isSidebarOpen);
});
}
async setMenu(isSidebarOpen: boolean) {
this.menuItems = [];
this.menuItems = [
{ label: isSidebarOpen ? this.translateService.instant('sidebar.home') : '', icon: 'pi pi-th-large', routerLink: 'home' },
];
if (await this.authService.hasUserPermission(AuthRoles.Admin)) {
this.menuItems.push(
{ separator: true },
{ label: isSidebarOpen ? this.translateService.instant('sidebar.config') : '', icon: 'pi pi-cog', routerLink: '/admin/settings' },
{ label: isSidebarOpen ? this.translateService.instant('sidebar.auth_user_list') : '', icon: 'pi pi-user-edit', routerLink: '/admin/users' },
);
this.menuItems = this.menuItems.slice();
}
}
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (!changes)
return;
await this.setMenu(changes.isSidebarOpen.currentValue);
}
}

View File

@ -0,0 +1,7 @@
<ng-container *ngIf="spinnerService.showSpinnerState">
<div class="spinner-component-wrapper">
<div class="spinner-wrapper">
<p-progressSpinner styleClass="custom-spinner" animationDuration=".8s"></p-progressSpinner>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SpinnerComponent } from './spinner.component';
describe('SpinnerComponent', () => {
let component: SpinnerComponent;
let fixture: ComponentFixture<SpinnerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SpinnerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SpinnerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
@Component({
selector: 'app-spinner',
templateUrl: './spinner.component.html',
styleUrls: ['./spinner.component.scss']
})
export class SpinnerComponent implements OnInit {
constructor(
public spinnerService: SpinnerService
) { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,7 @@
import { AuthUserDTO } from "./auth-user.dto";
export interface AdminUpdateUserDTO {
authUserDTO: AuthUserDTO;
newAuthUserDTO: AuthUserDTO;
changePassword: boolean;
}

View File

@ -0,0 +1,7 @@
export enum AuthErrorMessages {
UserIsEmpty = "User is empty",
UserNotFound = "User not found",
WrongPassword = "Wrong password",
UserAlreadyExists = "User already exists",
EMailNotConfirmed = "E-Mail not confirmed"
}

View File

@ -0,0 +1,4 @@
export enum AuthRoles {
Normal = 0,
Admin = 1
}

View File

@ -0,0 +1,12 @@
export class AuthUserAtrErrors {
firstName: AuthUserAtrErrorType = new AuthUserAtrErrorType();
lastName: AuthUserAtrErrorType = new AuthUserAtrErrorType();
email: AuthUserAtrErrorType = new AuthUserAtrErrorType();
password: AuthUserAtrErrorType = new AuthUserAtrErrorType();
}
export class AuthUserAtrErrorType {
wrongData: boolean = false;
required: boolean = false;
notConfirmed: boolean = false;
}

View File

@ -0,0 +1,11 @@
import { AuthRoles } from "./auth-roles.enum";
export interface AuthUserDTO {
id?: number;
firstName: string;
lastName: string;
eMail: string;
password: string;
isConfirmed?: boolean
authRole?: AuthRoles;
}

View File

@ -0,0 +1,3 @@
export interface EMailStringDTO {
email: string;
}

View File

@ -0,0 +1,4 @@
export enum RegisterErrorMessages {
InvalidEMail = "Invalid E-Mail",
UserAlreadyExists = "User already exists",
}

View File

@ -0,0 +1,6 @@
import { AuthUserDTO } from "./auth-user.dto";
export interface ResetPasswordDTO {
id: string;
password: string;
}

View File

@ -0,0 +1,4 @@
export interface TokenDTO {
token: string;
refreshToken: string;
}

View File

@ -0,0 +1,6 @@
import { AuthUserDTO } from "./auth-user.dto";
export interface UpdateUserDTO {
authUserDTO: AuthUserDTO;
newAuthUserDTO: AuthUserDTO;
}

View File

@ -0,0 +1,5 @@
export interface ApiVersion {
Major: string;
Minor: string;
Micro: string;
}

View File

@ -0,0 +1,8 @@
import { SoftwareVersion } from "./software-version";
import { Theme } from '../view/theme';
export interface Appsettings {
ApiURL: string;
WebVersion: SoftwareVersion;
Themes: Theme[];
}

View File

@ -0,0 +1,16 @@
export interface SettingsDTO {
webVersion: string;
apiVersion: string;
configPath: string;
webBaseURL: string;
apiBaseURL: string;
tokenExpireTime: number;
refreshTokenExpireTime: number;
mailUser: string;
mailPort: number;
mailHost: string;
mailTransceiver: string;
mailTransceiverAddress: string;
}

View File

@ -0,0 +1,5 @@
export interface SoftwareVersionDTO {
major: string;
minor: string;
micro: string
}

View File

@ -0,0 +1,19 @@
export class SoftwareVersion {
Major: string;
Minor: string;
Micro: string;
constructor(
major: string,
minor: string,
micro: string
) {
this.Major = major;
this.Minor = minor;
this.Micro = micro;
}
getVersionString(): string {
return `${this.Major}.${this.Minor}.${this.Micro}`;
}
}

View File

@ -0,0 +1,8 @@
import { ServiceErrorCode } from "./service-error-code.enum";
export class ErrorDTO {
errorCode: ServiceErrorCode;
message: string;
}

View File

@ -0,0 +1,16 @@
export enum ServiceErrorCode {
Unknown = 0,
InvalidDependencies = 1,
InvalidData = 2,
NotFound = 3,
DataAlreadyExists = 4,
UnableToAdd = 5,
UnableToDelete = 6,
InvalidUser = 7,
ConnectionFailed = 8,
Timeout = 9,
MailError = 10
}

View File

@ -0,0 +1,8 @@
import { SelectCriterion } from "../select-criterion.model";
export interface AuthUserSelectCriterion extends SelectCriterion {
firstName: string;
lastName: string;
eMail: string;
authRole?: number;
}

View File

@ -0,0 +1,6 @@
import { AuthUserDTO } from "../../auth/auth-user.dto";
export interface GetFilteredAuthUsersResultDTO {
users: AuthUserDTO[];
totalCount: number;
}

View File

@ -0,0 +1,6 @@
export interface SelectCriterion {
pageIndex: number;
pageSize: number;
sortDirection: string;
sortColumn: string;
}

View File

@ -0,0 +1,7 @@
export interface ConfirmationDialog {
key?: string;
header: string;
message: string;
accept?: () => void;
reject?: () => void;
}

View File

@ -0,0 +1,5 @@
export interface ToastOptions {
life?: number;
sticky?: boolean;
closable?: boolean;
}

View File

@ -0,0 +1,4 @@
export interface Theme {
Label: string;
Name: string;
}

View File

@ -0,0 +1,6 @@
export enum Themes {
DefaultLight = "default-light-theme",
DefaultDark = "default-dark-theme",
ShEdraftLight = "sh-edraft-light-theme",
ShEdraftDark = "sh-edraft-dark-theme",
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthUserComponent } from './components/auth-user/auth-user.component';
const routes: Routes = [
{path: '', component: AuthUserComponent}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthUserRoutingModule { }

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthUserRoutingModule } from './auth-user-routing.module';
import { AuthUserComponent } from './components/auth-user/auth-user.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [
AuthUserComponent
],
imports: [
CommonModule,
AuthUserRoutingModule,
SharedModule
]
})
export class AuthUserModule { }

View File

@ -0,0 +1,201 @@
<h1>
{{'admin.auth_users.header' | translate}}
</h1>
<div class="content-wrapper">
<div class="content">
<p-table #dt [value]="users" dataKey="id" editMode="row" [rowHover]="true" [rows]="10"
[rowsPerPageOptions]="[10,25,50]" [paginator]="true" [loading]="loading" [totalRecords]="totalRecords"
[lazy]="true" (onLazyLoad)="nextPage($event)">
<ng-template pTemplate="caption">
<div class="table-caption">
<div class="table-caption-text">
<ng-container *ngIf="!loading">{{users.length}} {{'admin.auth_users.of' | translate}}
{{dt.totalRecords}}
</ng-container>
{{'admin.auth_users.users' | translate}}
</div>
<div class="table-caption-btn-wrapper btn-wrapper">
<button pButton label="{{'admin.auth_users.add' | translate}}" class="icon-btn btn"
icon="pi pi-user-plus" (click)="addUser(dt)" [disabled]="isEditingNew">
</button>
<button pButton label="{{'admin.auth_users.reset_filters' | translate}}" icon="pi pi-undo"
class="icon-btn btn" (click)="resetFilters(dt)">
</button>
</div>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="firstName">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.first_name' | translate}}</div>
<p-sortIcon icon="sort" field="firstName" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th pSortableColumn="lastName">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.last_name' | translate}}</div>
<p-sortIcon field="lastName" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th pSortableColumn="eMail">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.e_mail' | translate}}</div>
<p-sortIcon field="eMail" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th class="table-header-actions" pSortableColumn="confirmationId">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.active' | translate}}</div>
<p-sortIcon field="confirmationId" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th class="table-header-small-dropdown" pSortableColumn="authRole">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.role' | translate}}</div>
<p-sortIcon field="authRole" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.password' | translate}}</div>
</div>
</th>
<th class="table-header-actions">
<div class="table-header-label">
<div class="table-header-text">{{'admin.auth_users.headers.actions' | translate}}</div>
</div>
</th>
</tr>
<tr>
<th>
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="firstName" placeholder="{{'admin.auth_users.headers.first_name' | translate}}">
</form>
</th>
<th>
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="lastName" placeholder="{{'admin.auth_users.headers.last_name' | translate}}">
</form>
</th>
<th>
<form [formGroup]="filterForm">
<input type="email" pInputText formControlName="eMail" placeholder="{{'admin.auth_users.headers.e_mail' | translate}}">
</form>
</th>
<th></th>
<th>
<form [formGroup]="filterForm">
<p-dropdown formControlName="authRole" [options]="authRoles"></p-dropdown>
</form>
</th>
<th></th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-user let-editing="editing" let-ri="rowIndex">
<tr [pEditableRow]="user">
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="user.firstName"
[ngClass]="{ 'invalid-feedback-input': isFirstNameInvalid}">
</ng-template>
<ng-template pTemplate="output">
{{user.firstName}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="user.lastName"
[ngClass]="{ 'invalid-feedback-input': isLastNameInvalid}">
</ng-template>
<ng-template pTemplate="output">
{{user.lastName}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="email" [(ngModel)]="user.eMail"
[ngClass]="{ 'invalid-feedback-input': isEMailInvalid}">
</ng-template>
<ng-template pTemplate="output">
{{user.eMail}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<p-checkbox [binary]="true" [(ngModel)]="user.isConfirmed"
[ngClass]="{ 'invalid-feedback-input': isEMailInvalid}" [disabled]="isEditingNew">
</p-checkbox>
</ng-template>
<ng-template pTemplate="output">
<p-checkbox [binary]="true" [ngModel]="user.isConfirmed" [disabled]="true">
</p-checkbox>
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<p-dropdown [options]="authRoles" [(ngModel)]="user.authRole"
[disabled]="user.eMail == loggedInUserEMail"></p-dropdown>
</ng-template>
<ng-template pTemplate="output">
{{user.authRole | authRole}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="password" [(ngModel)]="user.password"
[ngClass]="{ 'invalid-feedback-input': isPasswordInvalid}">
</ng-template>
<ng-template pTemplate="output">
</ng-template>
</p-cellEditor>
</td>
<td>
<div class="btn-wrapper">
<button *ngIf="!editing" pButton pInitEditableRow class="btn icon-btn" icon="pi pi-pencil"
(click)="onRowEditInit(dt, user, ri)"></button>
<button *ngIf="!editing" pButton class="btn icon-btn danger-icon-btn" icon="pi pi-trash"
(click)="deleteUser(user)"></button>
<button *ngIf="editing" pButton pSaveEditableRow class="btn icon-btn"
icon="pi pi-check-circle" (click)="onRowEditSave(dt, user, ri)"></button>
<button *ngIf="editing" pButton pCancelEditableRow class="btn icon-btn danger-icon-btn"
icon="pi pi-times-circle" (click)="onRowEditCancel(user, ri)"></button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="Invalidmessage">
<tr>
<td colspan="7">{{'admin.auth_users.no_entries_found' | translate}}</td>
</tr>
</ng-template>
<ng-template pTemplate="paginatorleft">
</ng-template>
</p-table>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthUserComponent } from './auth-user.component';
describe('AuthUserComponent', () => {
let component: AuthUserComponent;
let fixture: ComponentFixture<AuthUserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AuthUserComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AuthUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,325 @@
import { Component, OnInit } from '@angular/core';
import { catchError, debounceTime, last } from 'rxjs/operators';
import { AuthRoles } from 'src/app/models/auth/auth-roles.enum';
import { AuthUserDTO } from 'src/app/models/auth/auth-user.dto';
import { AuthService } from 'src/app/services/auth/auth.service';
import { ConfirmationDialogService } from 'src/app/services/confirmation-dialog/confirmation-dialog.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
import { ToastService } from 'src/app/services/toast/toast.service';
import { Table } from 'primeng/table';
import { ServiceErrorCode } from 'src/app/models/error/service-error-code.enum';
import { RegisterErrorMessages } from 'src/app/models/auth/register-error-messages.enum';
import { ErrorDTO } from 'src/app/models/error/error-dto';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthUserSelectCriterion } from 'src/app/models/selection/auth-user/auth-user-select-criterion.dto';
import { LazyLoadEvent } from 'primeng/api';
import { throwError } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-auth-user',
templateUrl: './auth-user.component.html',
styleUrls: ['./auth-user.component.scss']
})
export class AuthUserComponent implements OnInit {
users: AuthUserDTO[];
statuses: any[];
loading = true;
activityValues: number[] = [0, 100];
clonedUsers: { [s: string]: AuthUserDTO; } = {};
isEditingNew: boolean = false;
authRoles = [
{ label: AuthRoles[AuthRoles.Normal].toString(), value: AuthRoles.Normal },
{ label: AuthRoles[AuthRoles.Admin].toString(), value: AuthRoles.Admin }
]
newUserTemplate: AuthUserDTO = {
id: null,
firstName: "",
lastName: "",
eMail: "",
password: "",
authRole: AuthRoles.Normal
};
isFirstNameInvalid: boolean = false;
isLastNameInvalid: boolean = false;
isEMailInvalid: boolean = false;
isPasswordInvalid: boolean = false;
loggedInUserEMail: string = "";
filterForm: FormGroup;
searchCriterions: AuthUserSelectCriterion;
totalRecords: number;
constructor(
private authService: AuthService,
private spinnerService: SpinnerService,
private toastService: ToastService,
private confirmDialog: ConfirmationDialogService,
private fb: FormBuilder,
private translate: TranslateService
) { }
ngOnInit(): void {
this.loggedInUserEMail = this.authService.getEMailFromDecodedToken(this.authService.getDecodedToken());
this.searchCriterions = {
firstName: null,
lastName: null,
eMail: null,
authRole: null,
pageIndex: 0,
pageSize: 10,
sortColumn: null,
sortDirection: null
};
this.setFilterForm();
this.loadNextPage();
}
setFilterForm() {
this.filterForm = this.fb.group({
firstName: [null],
lastName: [null],
eMail: [null],
authRole: [null]
});
this.filterForm.valueChanges.pipe(
debounceTime(600)
).subscribe(changes => {
if (changes.firstName) {
this.searchCriterions.firstName = changes.firstName;
} else {
this.searchCriterions.firstName = undefined;
}
if (changes.lastName) {
this.searchCriterions.lastName = changes.lastName;
} else {
this.searchCriterions.lastName = undefined;
}
if (changes.eMail) {
this.searchCriterions.eMail = changes.eMail;
} else {
this.searchCriterions.eMail = undefined;
}
if (changes.authRole != null) {
this.searchCriterions.authRole = changes.authRole;
} else {
this.searchCriterions.authRole = undefined;
}
if (this.searchCriterions.pageSize)
this.searchCriterions.pageSize = 10;
if (this.searchCriterions.pageSize)
this.searchCriterions.pageIndex = 0;
this.loadNextPage();
});
}
loadNextPage() {
this.authService.getFilteredUsers(this.searchCriterions).pipe(catchError(err => {
this.loading = false;
return throwError(err);
})).subscribe(list => {
this.totalRecords = list.totalCount;
this.users = list.users;
this.loading = false;
});
}
nextPage(event: LazyLoadEvent) {
this.searchCriterions.pageSize = event.rows;
if (event.first != null && event.rows != null)
this.searchCriterions.pageIndex = event.first / event.rows;
this.searchCriterions.sortColumn = event.sortField;
this.searchCriterions.sortDirection = event.sortOrder === 1 ? 'asc' : event.sortOrder === -1 ? 'desc' : 'asc';
if (event.filters) {
// + "" => convert to string
this.searchCriterions.firstName = event.filters.firstName ? event.filters.firstName + "" : undefined;
this.searchCriterions.lastName = event.filters.lastName ? event.filters.lastName + "" : undefined;
this.searchCriterions.eMail = event.filters.eMail ? event.filters.eMail + "" : undefined;
this.searchCriterions.authRole = event.filters.authRole ? +event.filters.authRole : undefined;
}
this.loadNextPage();
}
resetFilters(table: Table) {
this.filterForm.reset();
}
initUserList(): void {
this.spinnerService.showSpinner();
this.authService.getAllUsers()
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
throw err;
}))
.subscribe(users => {
this.users = users;
this.spinnerService.hideSpinner();
});
}
onRowEditInit(table: Table, user: AuthUserDTO, index: number) {
this.clonedUsers[index] = { ...user };
}
onRowEditSave(table: Table, newUser: AuthUserDTO, index: number) {
const oldUser = this.clonedUsers[index];
delete this.clonedUsers[index];
if (JSON.stringify(oldUser) === JSON.stringify(newUser) && !this.isEditingNew) {
return;
}
if (this.isEditingNew && JSON.stringify(newUser) === JSON.stringify(this.newUserTemplate)) {
this.isEditingNew = false;
this.users.splice(index, 1);
return;
}
this.isFirstNameInvalid = newUser.firstName == "";
this.isLastNameInvalid = newUser.lastName == "";
this.isEMailInvalid = newUser.eMail == "";
this.isPasswordInvalid = newUser.password == "";
if (
this.isEditingNew && (
newUser.firstName == "" ||
newUser.lastName == "" ||
newUser.eMail == ""
)
) {
table.initRowEdit(newUser);
return;
}
if (this.isEditingNew) {
this.spinnerService.showSpinner();
this.authService.register(newUser).pipe(catchError(error => {
this.spinnerService.hideSpinner();
if (error.error !== null) {
const err: ErrorDTO = error.error;
if (err.errorCode === ServiceErrorCode.InvalidData && err.message === RegisterErrorMessages.InvalidEMail) {
this.isEMailInvalid = true;
this.toastService.error(this.translate.instant('admin.auth_users.message.invalid_email'), this.translate.instant('admin.auth_users.message.invalid_email_d', { eMail: newUser.eMail }));
} else if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === RegisterErrorMessages.UserAlreadyExists) {
this.isEMailInvalid = true;
this.toastService.error(this.translate.instant('admin.auth_users.message.user_already_exists'), this.translate.instant('admin.auth_users.message.user_already_exists_d', { eMail: newUser.eMail }));
}
error.error = null;
table.initRowEdit(newUser);
}
this.spinnerService.hideSpinner();
throw error;
}))
.subscribe(_ => {
this.initUserList();
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('admin.auth_users.message.user_added'), this.translate.instant('admin.auth_users.message.user_added_d', { eMail: newUser.eMail }));
this.isEditingNew = false;
});
this.triggerUserChangeDetection();
return;
}
this.spinnerService.showSpinner();
this.authService.updateUserAsAdmin({
authUserDTO: oldUser,
newAuthUserDTO: newUser,
changePassword: newUser.password != ""
}).pipe(catchError(err => {
this.spinnerService.hideSpinner();
this.toastService.error(this.translate.instant('admin.auth_users.message.user_change_failed'), this.translate.instant('admin.auth_users.message.user_change_failed_d', { eMail: newUser.eMail }));
this.initUserList();
throw err;
}))
.subscribe(_ => {
this.initUserList();
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('admin.auth_users.message.user_changed'), this.translate.instant('admin.auth_users.message.user_changed_d', { eMail: newUser.eMail }));
});
this.triggerUserChangeDetection();
}
onRowEditCancel(user: AuthUserDTO, index: number) {
this.isFirstNameInvalid = false;
this.isLastNameInvalid = false;
this.isEMailInvalid = false;
this.isPasswordInvalid = false;
if (this.isEditingNew) {
this.users.splice(index, 1);
this.triggerUserChangeDetection();
delete this.clonedUsers[index];
this.isEditingNew = false;
return;
}
this.users[index] = this.clonedUsers[index];
this.triggerUserChangeDetection();
delete this.clonedUsers[index];
}
deleteUser(user: AuthUserDTO) {
if (user.eMail == this.loggedInUserEMail) {
this.toastService.error(this.translate.instant('admin.auth_users.message.cannot_delete_user'), this.translate.instant('admin.auth_users.message.logon_with_another_user'));
return;
}
this.confirmDialog.confirmDialog(
this.translate.instant('admin.auth_users.message.user_delete'), this.translate.instant('admin.auth_users.message.user_delete_q', { eMail: user.eMail }),
() => {
this.spinnerService.showSpinner();
this.authService.deleteUserByMail(user.eMail)
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
throw err;
}))
.subscribe(_ => {
this.initUserList();
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('admin.auth_users.message.user_deleted'), this.translate.instant('admin.auth_users.message.user_deleted_d', { eMail: user.eMail }));
});
});
}
addUser(table: Table) {
const newUser = JSON.parse(JSON.stringify(this.newUserTemplate));
newUser.id = Math.max.apply(Math, this.users.map(function (u) { return u.id; })) + 1;
console.log(newUser);
this.users.push(newUser);
this.triggerUserChangeDetection();
table.initRowEdit(newUser);
const index = this.users.findIndex(u => u.eMail == newUser.eMail);
this.onRowEditInit(table, newUser, index);
this.isEditingNew = true;
}
triggerUserChangeDetection() {
// trigger change detection (https://github.com/primefaces/primeng/issues/2219)
this.users = this.users.slice();
}
}

View File

@ -0,0 +1,125 @@
<h1>
{{'admin.settings.header' | translate}}
</h1>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'admin.settings.website.header' | translate}}
</h2>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_version' | translate}}:</div>
<div class="content-data-value">{{data.webVersion}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_version' | translate}}:</div>
<div class="content-data-value">{{data.apiVersion}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.config_path' | translate}}:</div>
<div class="content-data-value">{{data.configPath}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.frontend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.webBaseURL}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.backend_base_url' | translate}}:</div>
<div class="content-data-value">{{data.apiBaseURL}}</div>
</div>
</div>
<div class="content-divider"></div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.tokenExpireTime}} {{'general.minutes' | translate}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.website.refresh_token_expire_time' | translate}}:</div>
<div class="content-data-value">{{data.refreshTokenExpireTime}} {{'general.days' | translate}}</div>
</div>
</div>
</div>
</div>
<div class="content-wrapper">
<div class="content-header">
<h2>
{{'admin.settings.e_mail.header' | translate}}
</h2>
</div>
<div class="content">
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.e_mail.user' | translate}}:</div>
<div class="content-data-value">{{data.mailUser}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.e_mail.host' | translate}}:</div>
<div class="content-data-value">{{data.mailHost}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.e_mail.port' | translate}}:</div>
<div class="content-data-value">{{data.mailPort}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.e_mail.transceiver' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiver}}</div>
</div>
</div>
<div class="content-row">
<div class="content-column">
<div class="content-data-name">{{'admin.settings.e_mail.e_mail_address' | translate}}:</div>
<div class="content-data-value">{{data.mailTransceiverAddress}}</div>
</div>
</div>
<div class="content-row">
<form [formGroup]="testMailForm" class="content-column">
<div class="content-data-name">
<div class="input-field content-input-field">
<input type="email" pInputText formControlName="mail" placeholder="{{'admin.settings.e_mail.e_mail' | translate}}" autocomplete="email">
</div>
</div>
<div class="content-data-value">
<div class="login-form-submit">
<button pButton icon="pi pi-save" label="{{'admin.settings.e_mail.send_e_mail' | translate}}" class="btn login-form-submit-btn"
(click)="testMail()" [disabled]="testMailForm.invalid"></button>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SettingsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,121 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { catchError } from 'rxjs/operators';
import { SettingsDTO } from 'src/app/models/config/settings.dto';
import { ErrorDTO } from 'src/app/models/error/error-dto';
import { ServiceErrorCode } from 'src/app/models/error/service-error-code.enum';
import { AuthService } from 'src/app/services/auth/auth.service';
import { GuiService } from 'src/app/services/gui/gui.service';
import { SettingsService } from 'src/app/services/settings/settings.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
import { ToastService } from 'src/app/services/toast/toast.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
testMailForm: FormGroup;
data: SettingsDTO = {
webVersion: '',
apiVersion: '',
configPath: '',
webBaseURL: '',
apiBaseURL: '',
tokenExpireTime: 0,
refreshTokenExpireTime: 0,
mailUser: '',
mailPort: null,
mailHost: '',
mailTransceiver: '',
mailTransceiverAddress: '',
};
constructor(
private settingsService: SettingsService,
private spinnerService: SpinnerService,
private guiService: GuiService,
private formBuilder: FormBuilder,
private toastService: ToastService,
private translate: TranslateService
) { }
ngOnInit(): void {
this.spinnerService.showSpinner();
this.initForms();
this.guiService.getSettings()
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
throw err;
}))
.subscribe(settings => {
this.spinnerService.hideSpinner();
this.data = settings;
this.data.webVersion = this.settingsService.getWebVersion().getVersionString();
this.data.apiBaseURL = this.settingsService.getApiURL();
if (!this.data.apiBaseURL.endsWith('/')) {
this.data.apiBaseURL += '/';
}
});
}
initForms(): void {
this.testMailForm = this.formBuilder.group({
mail: [null, [Validators.required, Validators.email]],
});
}
testMail(): void {
this.spinnerService.showSpinner();
const mail = this.testMailForm.value.mail;
if (!mail) {
this.spinnerService.hideSpinner();
return;
}
this.guiService.sendTestMail(mail)
.pipe(catchError(error => {
let header = this.translate.instant('admin.settings.message.error');
let message = this.translate.instant('admin.settings.message.could_not_send_mail');
if (error.error !== null) {
const err: ErrorDTO = error.error;
if (err.errorCode === ServiceErrorCode.ConnectionFailed) {
header = this.translate.instant('admin.settings.message.connection_failed');
message = this.translate.instant('admin.settings.message.connection_to_mail_failed');
error.error = null;
}
if (err.errorCode === ServiceErrorCode.InvalidUser) {
header = this.translate.instant('admin.settings.message.connection_failed');
message = this.translate.instant('admin.settings.message.mail_login_failed');
error.error = null;
}
if (err.errorCode === ServiceErrorCode.MailError) {
header = this.translate.instant('admin.settings.message.send_failed');
message = this.translate.instant('admin.settings.message.test_mail_not_send');
error.error = null;
}
}
this.spinnerService.hideSpinner();
this.toastService.error(header, message);
throw error;
}))
.subscribe(res => {
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('admin.settings.message.success'), this.translate.instant('admin.settings.message.send_mail'));
this.testMailForm.reset();
});
}
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SettingsComponent } from './components/settings/settings.component';
const routes: Routes = [
{path:'', component: SettingsComponent}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SettingsRoutingModule { }

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SettingsRoutingModule } from './settings-routing.module';
import { SettingsComponent } from './components/settings/settings.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [
SettingsComponent
],
imports: [
CommonModule,
SettingsRoutingModule,
SharedModule
]
})
export class SettingsModule { }

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ForgetPasswordComponent } from './components/forget-password/forget-password.component';
import { LoginComponent } from './components/login/login.component';
import { RegistrationComponent } from './components/registration/registration.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegistrationComponent },
{ path: 'register/:id', component: RegistrationComponent },
{ path: 'forgot-password', component: ForgetPasswordComponent },
{ path: 'forgot-password/:id', component: ForgetPasswordComponent },
{ path: 'forgot-password', component: ForgetPasswordComponent },
{ path: 'forgot-password/:id', component: ForgetPasswordComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule { }

View File

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthRoutingModule } from './auth-routing.module';
import { ForgetPasswordComponent } from './components/forget-password/forget-password.component';
import { LoginComponent } from './components/login/login.component';
import { RegistrationComponent } from './components/registration/registration.component';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [
ForgetPasswordComponent,
LoginComponent,
RegistrationComponent
],
imports: [
CommonModule,
AuthRoutingModule,
SharedModule
]
})
export class AuthModule { }

View File

@ -0,0 +1,57 @@
<section class="login-wrapper">
<div class="login-form-wrapper">
<div class="login-form">
<ng-container *ngIf="resetPasswordId === null; else resetPasswordForm">
<form [formGroup]="emailForm">
<h1>{{'auth.header' | translate}}</h1>
<div *ngIf="!ready" class="input-field">
<input type="email" pInputText formControlName="email" placeholder="{{'auth.forgot_password.e_mail' | translate}}"
autocomplete="username email">
</div>
<div *ngIf="ready" class="input-field-info-text">
{{'auth.forgot_password.send_confirmation_url' | translate}}
</div>
<div class="login-form-submit">
<button pButton label="{{'auth.forgot_password.reset_password' | translate}}" class="btn login-form-submit-btn"
(click)="forgotPassword()" [disabled]="emailForm.invalid || ready"></button>
</div>
<div class="login-form-sub-button-wrapper">
<div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.forgot_password.login' | translate}}" class="btn login-form-sub-btn" (click)="login()"></button>
</div>
<div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.forgot_password.register' | translate}}" class="btn login-form-sub-btn"
(click)="register()"></button>
</div>
</div>
</form>
</ng-container>
<ng-template #resetPasswordForm>
<form [formGroup]="passwordForm">
<h1>{{'auth.header' | translate}}</h1>
<div class="input-field">
<input type="password" pInputText formControlName="password" placeholder="{{'auth.forgot_password.password' | translate}}"
autocomplete="new-password">
</div>
<div class="input-field">
<input type="password" pInputText formControlName="passwordRepeat"
placeholder="{{'auth.forgot_password.repeat_password' | translate}}"
[ngClass]="{ 'invalid-feedback-input': submitted && repeatErrors.password}">
<div *ngIf="submitted" class="invalid-feedback">
<div *ngIf="repeatErrors.password">{{'auth.forgot_password.passwords_do_not_match' | translate}}</div>
</div>
</div>
<div class="login-form-submit">
<button pButton label="{{'auth.forgot_password.reset_password' | translate}}" class="btn login-form-submit-btn"
(click)="resetPassword()" [disabled]="passwordForm.invalid || ready"></button>
</div>
</form>
</ng-template>
</div>
</div>
</section>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ForgetPasswordComponent } from './forget-password.component';
describe('ForgetPasswordComponent', () => {
let component: ForgetPasswordComponent;
let fixture: ComponentFixture<ForgetPasswordComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ForgetPasswordComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ForgetPasswordComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,136 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { catchError } from 'rxjs/operators';
import { ResetPasswordDTO } from 'src/app/models/auth/reset-password.dto';
import { AuthService } from 'src/app/services/auth/auth.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
import { ToastService } from 'src/app/services/toast/toast.service';
@Component({
selector: 'app-forget-password',
templateUrl: './forget-password.component.html',
styleUrls: ['./forget-password.component.scss']
})
export class ForgetPasswordComponent implements OnInit {
emailForm: FormGroup;
passwordForm: FormGroup;
submitted = false;
ready = false;
repeatErrors = {
email: false,
password: false
};
resetPasswordId: string = null;
constructor(
private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router,
private spinnerService: SpinnerService,
private route: ActivatedRoute,
private toastService: ToastService,
private translate: TranslateService
) { }
ngOnInit(): void {
this.spinnerService.showSpinner();
this.authService.isUserLoggedInAsync().then(result => {
if (result) {
this.router.navigate(['/home']);
}
this.initForms();
this.checkResetPasswordId();
this.spinnerService.hideSpinner();
});
}
initForms(): void {
this.emailForm = this.formBuilder.group({
email: [null, [Validators.required, Validators.email]]
});
this.passwordForm = this.formBuilder.group({
password: [null, [Validators.required, Validators.minLength(8)]],
passwordRepeat: [null, [Validators.required, Validators.minLength(8)]]
});
}
login(): void {
this.router.navigate(['/auth/login']);
}
register(): void {
this.router.navigate(['/auth/register']);
}
forgotPassword(): void {
this.submitted = true;
if (this.emailForm.invalid) {
return;
}
this.spinnerService.showSpinner();
this.authService.forgotPassword(this.emailForm.value.email)
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
throw err;
})).subscribe(res => {
this.spinnerService.hideSpinner();
this.ready = true;
setTimeout(() => { this.router.navigate(['/home']); }, 5000);
});
}
checkResetPasswordId(): void {
const id = this.route.snapshot.params.id;
if (id) {
this.resetPasswordId = id;
this.spinnerService.showSpinner();
this.authService.getEMailFromforgotPasswordId(id)
.pipe(catchError(err => {
this.spinnerService.hideSpinner();
this.router.navigate(['/auth/forgot-password']);
throw err;
})).subscribe(email => {
this.spinnerService.hideSpinner();
if (email) {
this.emailForm.value.email = email;
} else {
this.router.navigate(['/auth/forgot-password']);
}
});
}
}
resetPassword(): void {
const id = this.route.snapshot.params.id;
if (this.emailForm.value.password !== this.emailForm.value.passwordRepeat) {
this.repeatErrors.password = true;
return;
}
this.spinnerService.showSpinner();
const resetPasswordDTO: ResetPasswordDTO = {
id,
password: this.passwordForm.value.password
};
this.authService.resetPassword(resetPasswordDTO)
.pipe(catchError(error => {
this.router.navigate(['/auth/login']);
this.spinnerService.hideSpinner();
throw error;
}))
.subscribe(resp => {
this.spinnerService.hideSpinner();
this.toastService.success(this.translate.instant('auth.forgot_password.message.reset_password'), this.translate.instant('auth.forgot_password.message.reset_password_d'));
this.router.navigate(['/auth/login']);
});
}
}

View File

@ -0,0 +1,58 @@
<section class="login-wrapper">
<div class="login-form-wrapper">
<div class="login-form">
<form [formGroup]="loginForm">
<h1>sh-edraft.de</h1>
<div class="input-field">
<input type="email" pInputText formControlName="email" placeholder="E-Mail" [ngClass]="{ 'invalid-feedback-input': submitted && (
(loginForm.controls.email.errors && loginForm.controls.email.errors.required || authUserAtrErrors.email.required) ||
(authUserAtrErrors.email.wrongData) ||
(authUserAtrErrors.email.notConfirmed)
)}" autocomplete="username email">
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.email.errors && loginForm.controls.email.errors.required || authUserAtrErrors.email.required">
E-Mail wird benötigt</div>
<div *ngIf="authUserAtrErrors.email.wrongData">Benutzer nicht gefunden</div>
<div *ngIf="authUserAtrErrors.email.notConfirmed">E-Mail wurde nicht bestätigt</div>
</div>
</div>
<div class="input-field">
<!--
!! WARNING !!
Bugfix from https://github.com/primefaces/primeng/issues/10788
styleClass="p-password p-component p-inputwrapper p-input-icon-right"
Remove after update!
-->
<p-password type="password" formControlName="password" placeholder="Passwort" [ngClass]="{ 'invalid-feedback-input': submitted && (
(loginForm.controls.password.errors && loginForm.controls.password.errors.required || authUserAtrErrors.password.required) ||
(authUserAtrErrors.password.wrongData)
)}" autocomplete="current-password" [toggleMask]="true" [feedback]="false"
styleClass="p-password p-component p-inputwrapper p-input-icon-right"
></p-password>
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.password.errors && loginForm.controls.password.errors.required || authUserAtrErrors.password.required">
Password wird benötigt</div>
<div *ngIf="authUserAtrErrors.password.wrongData">Falsches passwort</div>
</div>
</div>
<div class="login-form-submit">
<button pButton label="Anmelden" class="btn login-form-submit-btn" (click)="login()"
[disabled]="loginForm.invalid"></button>
</div>
<div class="login-form-sub-button-wrapper">
<div class="login-form-sub-btn-wrapper">
<button pButton label="Registrieren" class="btn login-form-sub-btn"
(click)="register()"></button>
</div>
<div class="login-form-sub-btn-wrapper">
<button pButton label="Passwort vergessen?"
class="btn login-form-sub-btn login-form-sub-login-btn p-button-text"
(click)="forgotPassword()"></button>
</div>
</div>
</form>
</div>
</div>
</section>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,111 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from 'src/app/services/auth/auth.service';
import { AuthUserDTO } from 'src/app/models/auth/auth-user.dto';
import { Router } from '@angular/router';
import { catchError } from 'rxjs/operators';
import { ErrorDTO } from 'src/app/models/error/error-dto';
import { AuthErrorMessages } from 'src/app/models/auth/auth-error-messages.enum';
import { ServiceErrorCode } from 'src/app/models/error/service-error-code.enum';
import { AuthUserAtrErrors } from 'src/app/models/auth/auth-user-atr-errors';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
import { ThemeService } from 'src/app/services/theme/theme.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;
submitted = false;
authUserAtrErrors: AuthUserAtrErrors;
constructor(
private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router,
private spinnerService: SpinnerService,
private themeService: ThemeService
) { }
ngOnInit(): void {
this.spinnerService.showSpinner();
this.authService.isUserLoggedInAsync().then(result => {
if (result) {
this.router.navigate(['/home']);
}
this.initLoginForm();
this.resetStateFlags();
this.spinnerService.hideSpinner();
});
}
resetStateFlags(): void {
this.authUserAtrErrors = new AuthUserAtrErrors();
}
initLoginForm(): void {
this.loginForm = this.formBuilder.group({
email: [null, [Validators.required, Validators.email]],
password: [null, [Validators.required, Validators.minLength(8)]]
});
}
register(): void {
this.router.navigate(['/auth/register']);
}
forgotPassword(): void {
this.router.navigate(['/auth/forgot-password']);
}
login(): void {
this.submitted = true;
this.resetStateFlags();
if (this.loginForm.invalid) {
return;
}
this.spinnerService.showSpinner();
const user: AuthUserDTO = {
firstName: '',
lastName: '',
eMail: this.loginForm.value.email,
password: this.loginForm.value.password
};
this.authService.login(user)
.pipe(catchError(error => {
if (error.error !== null) {
const err: ErrorDTO = error.error;
if (err.errorCode === ServiceErrorCode.InvalidData && err.message === AuthErrorMessages.UserIsEmpty) {
this.authUserAtrErrors.email.required = true;
this.authUserAtrErrors.password.required = true;
} else if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === AuthErrorMessages.UserNotFound) {
this.authUserAtrErrors.email.wrongData = true;
} else if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === AuthErrorMessages.WrongPassword) {
this.authUserAtrErrors.password.wrongData = true;
} else if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === AuthErrorMessages.EMailNotConfirmed) {
this.authUserAtrErrors.email.notConfirmed = true;
}
error.error = null;
}
this.spinnerService.hideSpinner();
throw error;
}))
.subscribe(token => {
this.authService.saveToken(token);
this.themeService.loadTheme();
this.themeService.loadMenu();
this.spinnerService.hideSpinner();
this.router.navigate(['/home']);
});
}
}

View File

@ -0,0 +1,83 @@
<section class="login-wrapper">
<div class="login-form-wrapper register-form-wrapper">
<div class="login-form">
<form [formGroup]="loginForm">
<h1>sh-edraft.de</h1>
<div class="input-field">
<input type="text" pInputText formControlName="firstName" placeholder="Vorname"
autocomplete="given-name">
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.firstName.errors && loginForm.controls.firstName.errors.required || authUserAtrErrors.firstName.required">
Vorname wird benötigt</div>
<div *ngIf="authUserAtrErrors.firstName.wrongData">Vorname ist ungültig</div>
</div>
</div>
<div class="input-field">
<input type="text" pInputText formControlName="lastName" placeholder="Nachname"
autocomplete="family-name">
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.lastName.errors && loginForm.controls.lastName.errors.required || authUserAtrErrors.lastName.required">
Nachname wird benötigt</div>
<div *ngIf="authUserAtrErrors.lastName.wrongData">Nachname ist ungültig</div>
</div>
</div>
<div class="input-field">
<input type="email" pInputText formControlName="email" placeholder="E-Mail"
[ngClass]="{ 'invalid-feedback-input': submitted && (authUserAtrErrors.email.wrongData || loginForm.controls.email.errors && loginForm.controls.email.errors.required || authUserAtrErrors.email.required)}"
autocomplete="username email">
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.email.errors && loginForm.controls.email.errors.required || authUserAtrErrors.email.required">
E-Mail wird benötigt</div>
<div *ngIf="authUserAtrErrors.email.wrongData">Benutzer existiert bereits</div>
</div>
</div>
<div class="input-field">
<input type="email" pInputText formControlName="emailRepeat" placeholder="E-Mail wiederholen"
[ngClass]="{ 'invalid-feedback-input': submitted && repeatErrors.email}">
<div *ngIf="submitted" class="invalid-feedback">
<div *ngIf="repeatErrors.email">Die E-Mails stimmen nicht überein</div>
</div>
</div>
<div class="input-field">
<p-password type="password" formControlName="password" placeholder="Passwort"
ngClass="{ 'invalid-feedback': submitted && loginForm.controls.password.errors && loginForm.controls.password.errors.required || authUserAtrErrors.password.required}"
autocomplete="new-password" [toggleMask]="true" [feedback]="false"
styleClass="p-password p-component p-inputwrapper p-input-icon-right"></p-password>
<div *ngIf="submitted" class="invalid-feedback">
<div
*ngIf="loginForm.controls.password.errors && loginForm.controls.password.errors.required || authUserAtrErrors.password.required">
Password wird benötigt</div>
</div>
</div>
<div class="input-field">
<p-password type="password" formControlName="passwordRepeat"
placeholder="Passwort wiederholen"
[ngClass]="{ 'invalid-feedback-input': submitted && repeatErrors.password}" [toggleMask]="true" [feedback]="false"
styleClass="p-password p-component p-inputwrapper p-input-icon-right"></p-password>
<div *ngIf="submitted" class="invalid-feedback">
<div *ngIf="repeatErrors.password">Die Passwörter stimmen nicht überein</div>
</div>
</div>
<div class="login-form-submit">
<button pButton label="Registrieren" class="btn login-form-submit-btn" (click)="register()"
[disabled]="loginForm.invalid"></button>
</div>
<div class="login-form-sub-button-wrapper">
<div class="login-form-sub-btn-wrapper">
<button pButton label="Einloggen" class="btn login-form-sub-btn" (click)="login()"></button>
</div>
</div>
</form>
</div>
</div>
</section>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrationComponent } from './registration.component';
describe('RegistrationComponent', () => {
let component: RegistrationComponent;
let fixture: ComponentFixture<RegistrationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RegistrationComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RegistrationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,137 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError } from 'rxjs/operators';
import { AuthErrorMessages } from 'src/app/models/auth/auth-error-messages.enum';
import { AuthUserDTO } from 'src/app/models/auth/auth-user.dto';
import { AuthUserAtrErrors } from 'src/app/models/auth/auth-user-atr-errors';
import { ErrorDTO } from 'src/app/models/error/error-dto';
import { ServiceErrorCode } from 'src/app/models/error/service-error-code.enum';
import { AuthService } from 'src/app/services/auth/auth.service';
import { SpinnerService } from 'src/app/services/spinner/spinner.service';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.scss']
})
export class RegistrationComponent implements OnInit {
loginForm: FormGroup;
submitted = false;
authUserAtrErrors: AuthUserAtrErrors;
repeatErrors = {
email: false,
password: false
};
showEMailConfirmation = false;
showEMailConfirmationError = false;
constructor(
private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router,
private spinnerService: SpinnerService,
private route: ActivatedRoute
) {
this.spinnerService.showSpinner();
this.authService.isUserLoggedInAsync().then(res => {
if (res) {
this.router.navigate(['/home']);
}
this.spinnerService.hideSpinner();
});
}
ngOnInit(): void {
this.initLoginForm();
this.resetStateFlags();
this.confirmEMail();
}
resetStateFlags(): void {
this.authUserAtrErrors = new AuthUserAtrErrors();
this.repeatErrors = {
email: false,
password: false
};
}
login(): void {
this.router.navigate(['/auth/login']);
}
initLoginForm(): void {
this.loginForm = this.formBuilder.group({
firstName: [null, Validators.required],
lastName: [null, Validators.required],
email: [null, [Validators.required, Validators.email]],
emailRepeat: [null, [Validators.required, Validators.email]],
password: [null, [Validators.required, Validators.minLength(8)]],
passwordRepeat: [null, [Validators.required, Validators.minLength(8)]]
});
}
register(): void {
this.submitted = true;
this.resetStateFlags();
// stop here if form is invalid
if (this.loginForm.invalid) {
return;
}
if (this.loginForm.value.email !== this.loginForm.value.emailRepeat) {
this.repeatErrors.email = true;
return;
}
if (this.loginForm.value.password !== this.loginForm.value.passwordRepeat) {
this.repeatErrors.password = true;
return;
}
this.spinnerService.showSpinner();
const user: AuthUserDTO = {
firstName: this.loginForm.value.firstName,
lastName: this.loginForm.value.lastName,
eMail: this.loginForm.value.email,
password: this.loginForm.value.password
};
this.authService.register(user)
.pipe(catchError(error => {
if (error.error !== null) {
const err: ErrorDTO = error.error;
if (err.errorCode === ServiceErrorCode.InvalidUser && err.message === AuthErrorMessages.UserAlreadyExists) {
this.authUserAtrErrors.email.wrongData = true;
}
}
this.spinnerService.hideSpinner();
throw error;
}))
.subscribe(resp => {
this.spinnerService.hideSpinner();
this.router.navigate(['/auth/login']);
});
}
confirmEMail(): void {
const id = this.route.snapshot.params.id;
if (id) {
this.spinnerService.showSpinner();
this.authService.confirmEMail(id)
.pipe(catchError(error => {
this.router.navigate(['/auth/login']);
this.spinnerService.hideSpinner();
throw error;
}))
.subscribe(resp => {
this.spinnerService.hideSpinner();
this.router.navigate(['/auth/login']);
});
}
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthGuard } from './auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(AuthGuard);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/services/auth/auth.service';
import { ThemeService } from 'src/app/services/theme/theme.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authService: AuthService,
private themeService: ThemeService
) {
}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
if (!this.authService.getToken().token) {
this.authService.logout();
return false;
}
if (!await this.authService.isUserLoggedInAsync()) {
this.authService.logout();
return false;
}
const role = route.data.role;
if (role) {
if (!await this.authService.hasUserPermission(role)) {
this.router.navigate(['/home']);
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AuthRoles } from 'src/app/models/auth/auth-roles.enum';
@Pipe({
name: 'authRole'
})
export class AuthRolePipe implements PipeTransform {
transform(value: AuthRoles): string {
return AuthRoles[value].toString();
}
}

View File

@ -0,0 +1,8 @@
import { BoolPipe } from './bool.pipe';
describe('BoolPipe', () => {
it('create an instance', () => {
const pipe = new BoolPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
name: 'bool'
})
export class BoolPipe implements PipeTransform {
constructor(
private translate: TranslateService
) {}
transform(value: boolean): string {
if (value === true) {
return this.translate.instant('common.bool_as_string.true');
}
return this.translate.instant('common.bool_as_string.false');
}
}

View File

@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'ipAddress'
})
export class IpAddressPipe implements PipeTransform {
transform(ipAsArray: number[]): string {
let ipAsString = "";
if (ipAsArray.length != 4){
throw new Error("Invalid IP")
}
for (let i = 0; i < ipAsArray.length; i++) {
const byte = ipAsArray[i];
if (i == ipAsArray.length - 1) {
ipAsString += `${byte}`;
} else {
ipAsString += `${byte}.`;
}
}
return ipAsString;
}
}

View File

@ -0,0 +1,71 @@
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DropdownModule } from 'primeng/dropdown';
import { DynamicDialogModule } from 'primeng/dynamicdialog';
import { InputTextModule } from 'primeng/inputtext';
import { MenuModule } from 'primeng/menu';
import { PasswordModule } from 'primeng/password';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { TableModule } from 'primeng/table';
import { ToastModule } from 'primeng/toast';
import { AuthRolePipe } from './pipes/auth-role.pipe';
import { IpAddressPipe } from './pipes/ip-address.pipe';
import { BoolPipe } from './pipes/bool.pipe';
@NgModule({
declarations: [
AuthRolePipe,
IpAddressPipe,
BoolPipe,
],
imports: [
CommonModule,
ButtonModule,
PasswordModule,
MenuModule,
DialogModule,
ProgressSpinnerModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
ToastModule,
ConfirmDialogModule,
TableModule,
InputTextModule,
CheckboxModule,
DropdownModule,
TranslateModule,
DynamicDialogModule
],
exports: [
ButtonModule,
PasswordModule,
MenuModule,
DialogModule,
ProgressSpinnerModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
ToastModule,
ConfirmDialogModule,
TableModule,
InputTextModule,
CheckboxModule,
DropdownModule,
TranslateModule,
DynamicDialogModule,
AuthRolePipe,
IpAddressPipe,
BoolPipe,
]
})
export class SharedModule { }

Some files were not shown because too many files have changed in this diff Show More