diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/angular.json b/angular.json index b8acc35..1edc6ea 100644 --- a/angular.json +++ b/angular.json @@ -47,18 +47,19 @@ "src/favicon.ico" ], "styles": [ - "@angular/material/prebuilt-themes/deeppurple-amber.css", "src/styles.scss" ], - "scripts": [] + "scripts": [ + "node_modules/flowbite/dist/flowbite.min.js" + ] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "800kb", - "maximumError": "2mb" + "maximumWarning": "5mb", + "maximumError": "10mb" }, { "type": "anyComponentStyle", @@ -69,9 +70,7 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true @@ -83,10 +82,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "template:build:production" + "buildTarget": "template:build:production" }, "development": { - "browserTarget": "template:build:development" + "buildTarget": "template:build:development" } }, "defaultConfiguration": "development" @@ -94,7 +93,7 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "template:build" + "buildTarget": "template:build" } }, "test": { @@ -110,7 +109,6 @@ "src/favicon.ico" ], "styles": [ - "@angular/material/prebuilt-themes/deeppurple-amber.scss", "src/styles.scss" ], "scripts": [] diff --git a/bin/script/firstRun b/bin/script/firstRun new file mode 100755 index 0000000..608a1a8 --- /dev/null +++ b/bin/script/firstRun @@ -0,0 +1,19 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +PROJECT_DIR=$(realpath $SCRIPT_DIR/../../) +ENV_DIR=$(realpath $PROJECT_DIR/../../../) + +source $ENV_DIR/bin/denv_msg +source $ENV_DIR/bin/drun + +# Install node Packages +denv_echo_msg "[frontend]: NPM install" +drun frontend npm install +drun frontend npm install -g @angular/cli +denv_success_msg "[frontend]: NPM install done" + +# Initial build of website +denv_echo_msg "[frontend]: NPM run build" +drun frontend npm run build +denv_success_msg "[frontend]: NPM run build done" \ No newline at end of file diff --git a/bin/script/init b/bin/script/init index 8f28c30..80a3818 100755 --- a/bin/script/init +++ b/bin/script/init @@ -4,40 +4,10 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) PROJECT_DIR=$(realpath $SCRIPT_DIR/../../) ENV_DIR=$(realpath $PROJECT_DIR/../../../) -EXIT=0 +source $ENV_DIR/bin/denv_msg # Check docker-compose.yml file -if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] -then +if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml" - EXIT=1 -fi - -# Check docker-compose-mac.yml file -if [ ! -f "$PROJECT_DIR/docker/docker-compose-mac.yml" ] -then - cp "$PROJECT_DIR/docker/docker-compose-mac.yml.dist" "$PROJECT_DIR/docker/docker-compose-mac.yml" - EXIT=1 -fi - - -if [ $EXIT -eq 1 ] -then - echo "docker-compose or env files created, please change variables and call init again" - exit 1 -fi - -# Source key-scripts -source $ENV_DIR/bin/drun -source $ENV_DIR/bin/dexec - -# Build and start docker containers -dexec template-frontend build -dexec template-frontend up -d - -# Install node Packages -drun template-frontend npm install -drun template-frontend npm install -g @angular/cli - -# Initial build of website -drun template-frontend npm run build \ No newline at end of file + denv_info_msg "[frontend] Created docker-compose.yml" +fi \ No newline at end of file diff --git a/bin/script/update b/bin/script/update new file mode 100755 index 0000000..931daaf --- /dev/null +++ b/bin/script/update @@ -0,0 +1,29 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +PROJECT_DIR=$(realpath $SCRIPT_DIR/../../) +ENV_DIR=$(realpath $PROJECT_DIR/../../../) +CWD=$(pwd) + +source $ENV_DIR/bin/denv_msg +source $ENV_DIR/bin/drun + + +# Pull branch in project directory +denv_echo_msg "[frontend]: Git pull" +cd "$PROJECT_DIR" +git pull +denv_success_msg "[frontend]: Git pull done" + +# Install node Packages +denv_echo_msg "[frontend]: NPM install" +drun frontend npm install +denv_success_msg "[frontend]: NPM install done" + +# Initial build of website +denv_echo_msg "[frontend]: NPM run build" +drun frontend npm run build +denv_success_msg "[frontend]: NPM run build done" + +# Switch back to current working directory +cd "$CWD" \ No newline at end of file diff --git a/docker/docker-compose-mac.yml.dist b/docker/docker-compose-mac.yml.dist deleted file mode 100644 index 07d9979..0000000 --- a/docker/docker-compose-mac.yml.dist +++ /dev/null @@ -1,31 +0,0 @@ -networks: - template: - external: true - -services: - template-frontend-app: - image: template-frontend-app - networks: - - template - volumes: - - /Users/flo/dev/frontend/template/:/var/www/ - build: - context: ./../ - dockerfile: ./docker/npm/dockerfile - tty: true - - template-frontend-nginx: - image: template-frontend-nginx - networks: - - template - volumes: - - /Users/flo/dev/frontend/template/:/var/www/html:z - build: - context: ./../ - dockerfile: ./docker/nginx/dockerfile - labels: - - "traefik.http.routers.frontend.rule=Host(`template.local`)" - - "traefik.http.routers.frontend.entrypoints=websecure" - - "traefik.http.routers.frontend.tls.certresolver=le" - depends_on: - - template-frontend-app \ No newline at end of file diff --git a/docker/docker-compose.yml.dist b/docker/docker-compose.yml.dist index 5dd392e..5b80811 100644 --- a/docker/docker-compose.yml.dist +++ b/docker/docker-compose.yml.dist @@ -28,4 +28,4 @@ services: - "traefik.http.routers.frontend.entrypoints=websecure" - "traefik.http.routers.frontend.tls.certresolver=le" depends_on: - - template-frontend-app \ No newline at end of file + - template-frontend-app diff --git a/package.json b/package.json index 24e8935..233aa94 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,28 @@ }, "private": true, "dependencies": { - "@angular/animations": "^15.2.10", - "@angular/cdk": "^15.2.9", - "@angular/common": "^15.2.0", - "@angular/compiler": "^15.2.0", - "@angular/core": "^15.2.0", - "@angular/forms": "^15.2.0", - "@angular/material": "^15.2.9", - "@angular/platform-browser": "^15.2.0", - "@angular/platform-browser-dynamic": "^15.2.0", - "@angular/router": "^15.2.0", + "@angular/animations": "^19.0.4", + "@angular/common": "^19.0.4", + "@angular/compiler": "^19.0.4", + "@angular/core": "^19.0.4", + "@angular/forms": "^19.0.4", + "@angular/material": "^18.2.4", + "@angular/platform-browser": "^19.0.4", + "@angular/platform-browser-dynamic": "^19.0.4", + "@angular/router": "^19.0.4", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "angular-svg-icon": "^18.0.2", + "apexcharts": "^4.0.0", + "flowbite": "^2.5.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", - "zone.js": "~0.12.0" + "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^15.2.6", - "@angular/cli": "~15.2.6", - "@angular/compiler-cli": "^15.2.0", + "@angular-devkit/build-angular": "^19.0.5", + "@angular/cli": "~19.0.5", + "@angular/compiler-cli": "^19.0.4", "@types/jasmine": "~4.3.0", "jasmine-core": "~4.5.0", "karma": "~6.4.0", @@ -34,7 +38,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "tailwindcss": "^3.4.3", - "typescript": "~4.9.4" + "tailwindcss": "^3.4.10", + "typescript": "~5.6.3" } } diff --git a/src/app/app.component.html b/src/app/app.component.html index 0680b43..1363652 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,5 @@ + +
+ +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fe8da85..6e2d87d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,59 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit } from "@angular/core"; +import { AuthService } from "./core/services/auth.service"; +import { TranslateService } from "@ngx-translate/core"; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.scss"], + standalone: false }) -export class AppComponent { +export class AppComponent implements OnInit { + constructor( + private authService: AuthService, + private translate: TranslateService + ) { + var language = navigator.language; + + switch(language) { + case "en": + case "en_EN": + case "en-EN": + language = 'en'; + break; + case "de": + case "de_DE": + case "de-DE": + language = 'de'; + break; + case "es": + case "es_ES": + case "es-ES": + language = 'es'; + break; + case "fr": + case "fr_FR": + case "fr-FR": + language = 'fr'; + break; + case "tr": + case "tr_TR": + case "tr-TR": + language = 'tr'; + break; + default: + language = 'en'; + } + + this.translate.setDefaultLang(language); + this.translate.use(language); + + this.translate.onLangChange.subscribe(() => { + document.title = this.translate.instant("title"); + }); + } + + ngOnInit(): void { + this.authService.readUserState(); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ad51b68..2e9badd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,30 +1,56 @@ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { AppComponent } from './app.component'; -import { RouterModule, Routes } from '@angular/router'; -import { SharedModule } from './shared/shared.module'; -import { HomeComponent } from './core/components/home/home.component'; -import { CoreModule } from './core/core.module'; +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { HttpClient, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateModule, TranslateLoader, TranslatePipe } from "@ngx-translate/core"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { AppComponent } from "./app.component"; +import { RouterModule, Routes } from "@angular/router"; +import { SharedModule } from "./shared/shared.module"; +import { CoreModule } from "./core/core.module"; +import { AngularSvgIconModule } from 'angular-svg-icon'; +import { DatePipe } from "@angular/common"; const routes: Routes = [ - { path: 'home', component: HomeComponent }, - { path: '', redirectTo: 'home', pathMatch: 'full' }, + { + path: "auth", + loadChildren: () => + import("./core/auth/auth.module").then((m) => m.AuthModule), + }, + { + path: "", + loadChildren: () => + import("./core/home/home.module").then((m) => m.HomeModule), + } ]; +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, "./assets/i18n/", ".json"); +} + @NgModule({ declarations: [AppComponent], + bootstrap: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, - HttpClientModule, + RouterModule, RouterModule.forRoot(routes), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient], + }, + }), + AngularSvgIconModule.forRoot(), SharedModule, - CoreModule, + CoreModule + ], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + TranslatePipe, + DatePipe ], - providers: [], - bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/app.service.ts b/src/app/app.service.ts deleted file mode 100644 index e65248d..0000000 --- a/src/app/app.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class AppService { - - private username: string|null; - - constructor() { - this.username = null; - } - - public getUsername(): string|null { - return this.username; - } - - public setUsername(name: string|null): void { - this.username = name; - } -} diff --git a/src/app/core/auth/auth.module.ts b/src/app/core/auth/auth.module.ts new file mode 100644 index 0000000..e126ed9 --- /dev/null +++ b/src/app/core/auth/auth.module.ts @@ -0,0 +1,57 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { LoginComponent } from "./components/login/login.component"; +import { RegistrationComponent } from "./components/registration/registration.component"; +import { ConfirmRegistrationComponent } from "./components/confirm-registration/confirm-registration.component"; +import { RouterModule, Routes } from "@angular/router"; +import { ReactiveFormsModule } from "@angular/forms"; +import { ForgotPasswordComponent } from "./components/forgot-password/forgot-password.component"; +import { ResetPasswordComponent } from "./components/reset-password/reset-password.component"; +import { TranslateModule } from "@ngx-translate/core"; +import { SharedModule } from "src/app/shared/shared.module"; +import { CoreModule } from "../core.module"; +import { AuthComponent } from "./components/auth/auth.component"; +import { AngularSvgIconModule } from "angular-svg-icon"; + +const routes: Routes = [ + { + path: "", + component: AuthComponent, + children: [ + { path: "login", component: LoginComponent }, + { path: "registration", component: RegistrationComponent }, + { path: "forgot-password", component: ForgotPasswordComponent }, + { path: "registration/:registrationId", component: ConfirmRegistrationComponent }, + { path: "reset-password/:passwordToken", component: ResetPasswordComponent }, + { path: "**", redirectTo: "login" }, + ] + } +]; + +@NgModule({ + declarations: [ + LoginComponent, + RegistrationComponent, + ConfirmRegistrationComponent, + ForgotPasswordComponent, + ResetPasswordComponent, + AuthComponent, + ], + exports: [ + LoginComponent, + RegistrationComponent, + ConfirmRegistrationComponent, + ForgotPasswordComponent, + ResetPasswordComponent, + ], + imports: [ + RouterModule.forChild(routes), + AngularSvgIconModule, + CommonModule, + ReactiveFormsModule, + TranslateModule, + SharedModule, + CoreModule + ], +}) +export class AuthModule {} diff --git a/src/app/core/auth/components/auth/auth.component.html b/src/app/core/auth/components/auth/auth.component.html new file mode 100644 index 0000000..1e59a05 --- /dev/null +++ b/src/app/core/auth/components/auth/auth.component.html @@ -0,0 +1,23 @@ + +
+ + +
+ +
+
+ +
+
+ +

{{ "title" | translate }}

+

{{ label | translate }}

+
+ +
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/app/core/components/home/home.component.scss b/src/app/core/auth/components/auth/auth.component.scss similarity index 100% rename from src/app/core/components/home/home.component.scss rename to src/app/core/auth/components/auth/auth.component.scss diff --git a/src/app/core/auth/components/auth/auth.component.ts b/src/app/core/auth/components/auth/auth.component.ts new file mode 100644 index 0000000..0418628 --- /dev/null +++ b/src/app/core/auth/components/auth/auth.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; +import { initFlowbite } from "flowbite"; +import { filter, map } from "rxjs"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-auth", + templateUrl: "./auth.component.html", + styleUrls: ["./auth.component.scss"], + standalone: false +}) +export class AuthComponent implements OnInit { + label: string = ''; + + constructor( + private authService: AuthService, + private route: ActivatedRoute, + private router: Router + ) { + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.route) + ).subscribe(newRoute => { + if ((newRoute.firstChild?.routeConfig?.path ?? undefined) === undefined) { + this.label = ''; + } else { + let routePart = newRoute.firstChild!.routeConfig!.path!; + let routeParts = routePart.split('/'); + this.label = 'auth.' + routeParts[0]; + } + }); + + this.authService.readUserState().subscribe(response => { + this.authService.currentState$.next(response); + this.router.navigateByUrl("/dashboard"); + }); + } + + ngOnInit(): void { + initFlowbite(); + } +} diff --git a/src/app/core/auth/components/confirm-registration/confirm-registration.component.html b/src/app/core/auth/components/confirm-registration/confirm-registration.component.html new file mode 100644 index 0000000..49d78cc --- /dev/null +++ b/src/app/core/auth/components/confirm-registration/confirm-registration.component.html @@ -0,0 +1,11 @@ +
+
+ + +
+
+ + +
+ + \ No newline at end of file diff --git a/src/app/core/components/navigation/navigation.component.scss b/src/app/core/auth/components/confirm-registration/confirm-registration.component.scss similarity index 100% rename from src/app/core/components/navigation/navigation.component.scss rename to src/app/core/auth/components/confirm-registration/confirm-registration.component.scss diff --git a/src/app/core/auth/components/confirm-registration/confirm-registration.component.ts b/src/app/core/auth/components/confirm-registration/confirm-registration.component.ts new file mode 100644 index 0000000..1753f09 --- /dev/null +++ b/src/app/core/auth/components/confirm-registration/confirm-registration.component.ts @@ -0,0 +1,42 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-confirm-registration", + templateUrl: "./confirm-registration.component.html", + styleUrls: ["./confirm-registration.component.scss"], + standalone: false +}) +export class ConfirmRegistrationComponent { + confirmRegistrationForm = new FormGroup({ + password: new FormControl("", [Validators.required]), + passwordConfirmation: new FormControl("", [Validators.required]), + }); + + registrationId: string | undefined; + + constructor( + private authService: AuthService, + private activatedRoute: ActivatedRoute, + private router: Router + ) { + this.activatedRoute.params.subscribe((params) => { + this.registrationId = params["registrationId"]; + }); + } + + confirm(): void { + if (this.registrationId === undefined) return; + + this.authService.confirmRegistration({ + id: this.registrationId!, + password: this.confirmRegistrationForm.value.password!, + passwordConfirmation: + this.confirmRegistrationForm.value.passwordConfirmation!, + }).subscribe(response => { + this.router.navigateByUrl("/auth"); + }); + } +} diff --git a/src/app/core/auth/components/forgot-password/forgot-password.component.html b/src/app/core/auth/components/forgot-password/forgot-password.component.html new file mode 100644 index 0000000..fd81a55 --- /dev/null +++ b/src/app/core/auth/components/forgot-password/forgot-password.component.html @@ -0,0 +1,7 @@ +
+
+ + +
+ + \ No newline at end of file diff --git a/src/app/shared/components/form/form.component.scss b/src/app/core/auth/components/forgot-password/forgot-password.component.scss similarity index 100% rename from src/app/shared/components/form/form.component.scss rename to src/app/core/auth/components/forgot-password/forgot-password.component.scss diff --git a/src/app/core/auth/components/forgot-password/forgot-password.component.ts b/src/app/core/auth/components/forgot-password/forgot-password.component.ts new file mode 100644 index 0000000..cf19bcc --- /dev/null +++ b/src/app/core/auth/components/forgot-password/forgot-password.component.ts @@ -0,0 +1,27 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-forgot-password", + templateUrl: "./forgot-password.component.html", + styleUrls: ["./forgot-password.component.scss"], + standalone: false +}) +export class ForgotPasswordComponent { + forgotPasswordForm = new FormGroup({ + mail: new FormControl("", [Validators.required, Validators.email]), + }); + + constructor(private authService: AuthService, private router: Router) { + } + + confirm(): void { + this.authService.forgotPassword({ + mail: this.forgotPasswordForm.value.mail!, + }).subscribe(response => { + this.router.navigateByUrl("/auth"); + }); + } +} diff --git a/src/app/core/auth/components/login/login.component.html b/src/app/core/auth/components/login/login.component.html new file mode 100644 index 0000000..85bb028 --- /dev/null +++ b/src/app/core/auth/components/login/login.component.html @@ -0,0 +1,23 @@ +
+
+ + +

+ {{ "auth.not-yet-registered" | translate }} + + {{ "auth.register-now" | translate }} + +

+
+ + + + \ No newline at end of file diff --git a/src/app/shared/components/tab-control/tab-control.component.scss b/src/app/core/auth/components/login/login.component.scss similarity index 100% rename from src/app/shared/components/tab-control/tab-control.component.scss rename to src/app/core/auth/components/login/login.component.scss diff --git a/src/app/core/auth/components/login/login.component.ts b/src/app/core/auth/components/login/login.component.ts new file mode 100644 index 0000000..0a18f17 --- /dev/null +++ b/src/app/core/auth/components/login/login.component.ts @@ -0,0 +1,29 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-login", + templateUrl: "./login.component.html", + styleUrls: ["./login.component.scss"], + standalone: false +}) +export class LoginComponent { + loginForm = new FormGroup({ + identifier: new FormControl("", [Validators.required]), + password: new FormControl("", [Validators.required]), + }); + + constructor(private authService: AuthService, private router: Router) { + } + + login(): void { + this.authService.login({ + identifier: this.loginForm.value.identifier!, + password: this.loginForm.value.password!, + }).subscribe(response => { + this.router.navigateByUrl("/dashboard"); + }); + } +} diff --git a/src/app/core/auth/components/registration/registration.component.html b/src/app/core/auth/components/registration/registration.component.html new file mode 100644 index 0000000..6b70d5c --- /dev/null +++ b/src/app/core/auth/components/registration/registration.component.html @@ -0,0 +1,17 @@ +
+
+ + +

+ {{ "auth.already-registered" | translate }} + + {{ "auth.login-now" | translate }} + +

+
+
+ + +
+ + diff --git a/src/app/core/auth/components/registration/registration.component.scss b/src/app/core/auth/components/registration/registration.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/auth/components/registration/registration.component.ts b/src/app/core/auth/components/registration/registration.component.ts new file mode 100644 index 0000000..1c4d447 --- /dev/null +++ b/src/app/core/auth/components/registration/registration.component.ts @@ -0,0 +1,29 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-registration", + templateUrl: "./registration.component.html", + styleUrls: ["./registration.component.scss"], + standalone: false +}) +export class RegistrationComponent { + registrationForm = new FormGroup({ + mail: new FormControl("", [Validators.required]), + username: new FormControl("", [Validators.required]), + }); + + constructor(private authService: AuthService, private router: Router) { + } + + register(): void { + this.authService.register({ + mail: this.registrationForm.value.mail!, + username: this.registrationForm.value.username!, + }).subscribe(response => { + this.router.navigateByUrl("/auth"); + }); + } +} diff --git a/src/app/core/auth/components/reset-password/reset-password.component.html b/src/app/core/auth/components/reset-password/reset-password.component.html new file mode 100644 index 0000000..24c565d --- /dev/null +++ b/src/app/core/auth/components/reset-password/reset-password.component.html @@ -0,0 +1,11 @@ +
+
+ + +
+
+ + +
+ + \ No newline at end of file diff --git a/src/app/core/auth/components/reset-password/reset-password.component.scss b/src/app/core/auth/components/reset-password/reset-password.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/auth/components/reset-password/reset-password.component.ts b/src/app/core/auth/components/reset-password/reset-password.component.ts new file mode 100644 index 0000000..09988d7 --- /dev/null +++ b/src/app/core/auth/components/reset-password/reset-password.component.ts @@ -0,0 +1,43 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { initFlowbite } from "flowbite"; +import { filter } from "rxjs"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-reset-password", + templateUrl: "./reset-password.component.html", + styleUrls: ["./reset-password.component.scss"], + standalone: false +}) +export class ResetPasswordComponent { + resetPasswordForm = new FormGroup({ + newPassword: new FormControl("", [Validators.required]), + passwordConfirmation: new FormControl("", [Validators.required]), + }); + + passwordToken: string | undefined; + + constructor( + private authService: AuthService, + private activatedRoute: ActivatedRoute, + private router: Router + ) { + this.activatedRoute.params.subscribe((params) => { + this.passwordToken = params["passwordToken"]; + }); + } + + confirm(): void { + if (this.passwordToken === undefined) return; + + this.authService.resetPassword({ + passwordToken: this.passwordToken!, + newPassword: this.resetPasswordForm.value.newPassword!, + passwordConfirmation: this.resetPasswordForm.value.passwordConfirmation!, + }).subscribe(response => { + this.router.navigateByUrl("/auth"); + }); + } +} diff --git a/src/app/core/components/home/home.component.html b/src/app/core/components/home/home.component.html deleted file mode 100644 index bd16462..0000000 --- a/src/app/core/components/home/home.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
- Home -
\ No newline at end of file diff --git a/src/app/core/components/home/home.component.ts b/src/app/core/components/home/home.component.ts deleted file mode 100644 index b1f2660..0000000 --- a/src/app/core/components/home/home.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; -import { RequestService } from 'src/app/core/services/request.service'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'], -}) -export class HomeComponent { - constructor(public requestService: RequestService) { - } -} diff --git a/src/app/core/components/language-picker/language-picker.component.html b/src/app/core/components/language-picker/language-picker.component.html new file mode 100644 index 0000000..272ccb2 --- /dev/null +++ b/src/app/core/components/language-picker/language-picker.component.html @@ -0,0 +1,23 @@ + +
+
+ + +
+
diff --git a/src/app/core/components/language-picker/language-picker.component.scss b/src/app/core/components/language-picker/language-picker.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/components/language-picker/language-picker.component.ts b/src/app/core/components/language-picker/language-picker.component.ts new file mode 100644 index 0000000..a11af0c --- /dev/null +++ b/src/app/core/components/language-picker/language-picker.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit } from "@angular/core"; +import { initFlowbite } from "flowbite"; +import { AppService } from "../../services/app.service"; +import { TranslateService } from "@ngx-translate/core"; + +interface Language { + imageSrc: string; + label: string; +} + +@Component({ + selector: "app-language-picker", + templateUrl: "./language-picker.component.html", + styleUrls: ["./language-picker.component.scss"], + standalone: false +}) +export class LanguagePickerComponent implements OnInit { + languages: Language[] = [ + { + label: "de", + imageSrc: "assets/de.svg" + }, + { + label: "en", + imageSrc: "assets/en.svg" + }, + { + label: "tr", + imageSrc: "assets/tr.svg" + }, + { + label: "es", + imageSrc: "assets/es.svg" + }, + { + label: "fr", + imageSrc: "assets/fr.svg" + }, + { + label: "it", + imageSrc: "assets/it.svg" + }, + { + label: "jp", + imageSrc: "assets/jp.svg" + }, + ]; + selectedLanguage: Language; + + constructor( + private appService: AppService, + private translate: TranslateService, + ) { + this.selectedLanguage = this.languages.find((f) => { + f.label === translate.currentLang; + }) ?? this.languages[0]; + } + + ngOnInit(): void { + initFlowbite(); + } + + selectLanguage(language: Language): void { + this.translate.use(language.label); + this.selectedLanguage = language; + } +} diff --git a/src/app/core/components/navigation/navigation.component.html b/src/app/core/components/navigation/navigation.component.html deleted file mode 100644 index a658057..0000000 --- a/src/app/core/components/navigation/navigation.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -

- Navigation -

-
\ No newline at end of file diff --git a/src/app/core/components/navigation/navigation.component.ts b/src/app/core/components/navigation/navigation.component.ts deleted file mode 100644 index 6c2fd97..0000000 --- a/src/app/core/components/navigation/navigation.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-navigation', - templateUrl: './navigation.component.html', - styleUrls: ['./navigation.component.scss'], -}) -export class NavigationComponent { -} diff --git a/src/app/core/components/notification-bar/notification-bar.component.html b/src/app/core/components/notification-bar/notification-bar.component.html new file mode 100644 index 0000000..0278b0d --- /dev/null +++ b/src/app/core/components/notification-bar/notification-bar.component.html @@ -0,0 +1,42 @@ +
+ + {{ item.element.type }} +
+ {{ item.element.message }} +
+ +
diff --git a/src/app/core/components/notification-bar/notification-bar.component.scss b/src/app/core/components/notification-bar/notification-bar.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/components/notification-bar/notification-bar.component.ts b/src/app/core/components/notification-bar/notification-bar.component.ts new file mode 100644 index 0000000..7695460 --- /dev/null +++ b/src/app/core/components/notification-bar/notification-bar.component.ts @@ -0,0 +1,70 @@ +import { Component } from "@angular/core"; +import { NotificationService } from "../../services/notification.service"; + +export interface NotificationElement { + message: string; + type: "info" | "danger" | "warn" | "success"; + timeout: number | undefined; +} + +interface NotificationItem { + element: NotificationElement; + shownAt: Date; +} + +@Component({ + selector: "app-notification-bar", + templateUrl: "./notification-bar.component.html", + styleUrls: ["./notification-bar.component.scss"], + standalone: false +}) +export class NotificationBarComponent { + items: NotificationItem[] = []; + isRunning: boolean; + + constructor(private notificationService: NotificationService) { + this.isRunning = false; + + this.notificationService.notification$.subscribe((notification) => { + if (notification !== undefined) { + this.items.push({ + element: notification, + shownAt: new Date(), + }); + + this.startTimeoutIfNecessary(); + } + }); + } + + startTimeoutIfNecessary(): void { + if (this.isRunning === true) { + return; + } + + let intervalId = setInterval(() => { + this.isRunning = true; + let now = new Date(); + this.items.forEach((item) => { + if (item.element.timeout !== undefined) { + let timeout = new Date( + item.shownAt.getTime() + item.element.timeout * 1000 + ); + + if (timeout < now) { + this.dismiss(item); + } + } + }); + + if (this.items.length === 0) { + clearInterval(intervalId); + this.isRunning = false; + } + }, 1000); + } + + dismiss(item: NotificationItem): void { + this.items = this.items.filter((i) => i !== item); + } +} diff --git a/src/app/core/components/theme-picker/theme-picker.component.html b/src/app/core/components/theme-picker/theme-picker.component.html new file mode 100644 index 0000000..6d56f1e --- /dev/null +++ b/src/app/core/components/theme-picker/theme-picker.component.html @@ -0,0 +1,23 @@ + +
+
+ + +
+
diff --git a/src/app/core/components/theme-picker/theme-picker.component.scss b/src/app/core/components/theme-picker/theme-picker.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/components/theme-picker/theme-picker.component.ts b/src/app/core/components/theme-picker/theme-picker.component.ts new file mode 100644 index 0000000..7585ccd --- /dev/null +++ b/src/app/core/components/theme-picker/theme-picker.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from "@angular/core"; +import { initFlowbite } from "flowbite"; +import { AppService } from "../../services/app.service"; +import { TranslateService } from "@ngx-translate/core"; + +interface Theme { + imageSrc: string; + label: string; +} + +@Component({ + selector: "app-theme-picker", + templateUrl: "./theme-picker.component.html", + styleUrls: ["./theme-picker.component.scss"], + standalone: false +}) +export class ThemePickerComponent implements OnInit { + themes: Theme[] = [ + { + label: "dark", + imageSrc: "assets/dark.svg" + }, + { + label: "light", + imageSrc: "assets/light.svg" + } + ]; + selectedTheme: Theme; + + constructor( + private appService: AppService, + ) { + this.selectedTheme = appService.darkMode ? this.themes[0] : this.themes[1]; + } + + ngOnInit(): void { + initFlowbite(); + } + + selectTheme(theme: Theme): void { + if (this.selectedTheme != theme) { + this.appService.toggleDarkMode(); + this.selectedTheme = theme; + } + } +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 2d58380..3e68725 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,15 +1,44 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { HomeComponent } from './components/home/home.component'; -import { NavigationComponent } from './components/navigation/navigation.component'; - - +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { AuthGuard } from "./guards/auth.guard"; +import { AuthService } from "./services/auth.service"; +import { RequestService } from "./services/request.service"; +import { AppService } from "./services/app.service"; +import { SharedModule } from "../shared/shared.module"; +import { RouterModule } from "@angular/router"; +import { ReactiveFormsModule } from "@angular/forms"; +import { NotificationBarComponent } from "./components/notification-bar/notification-bar.component"; +import { NotificationService } from "./services/notification.service"; +import { TranslateModule } from "@ngx-translate/core"; +import { AngularSvgIconModule } from 'angular-svg-icon'; +import { LanguagePickerComponent } from "./components/language-picker/language-picker.component"; +import { ThemePickerComponent } from "./components/theme-picker/theme-picker.component"; @NgModule({ - declarations: [HomeComponent, NavigationComponent], - exports: [HomeComponent, NavigationComponent], + declarations: [ + NotificationBarComponent, + LanguagePickerComponent, + ThemePickerComponent, + ], + exports: [ + NotificationBarComponent, + LanguagePickerComponent, + ThemePickerComponent, + ], imports: [ - CommonModule - ] + AngularSvgIconModule, + CommonModule, + SharedModule, + RouterModule, + ReactiveFormsModule, + TranslateModule, + ], + providers: [ + AuthGuard, + AuthService, + RequestService, + AppService, + NotificationService, + ], }) -export class CoreModule { } +export class CoreModule {} diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000..593591e --- /dev/null +++ b/src/app/core/guards/auth.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from "@angular/router"; +import { filter, map, Observable } from "rxjs"; +import { AuthService } from "../services/auth.service"; + +@Injectable() +export class AuthGuard { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(): Observable | boolean { + return this.authService.currentState$ + .pipe( + map( + (currentState) => { + if (currentState === null) { + this.router.navigateByUrl("/auth"); + return false; + } + return true; + } + ) + ); + } +} diff --git a/src/app/core/home/components/home/home.component.html b/src/app/core/home/components/home/home.component.html new file mode 100644 index 0000000..2b68548 --- /dev/null +++ b/src/app/core/home/components/home/home.component.html @@ -0,0 +1,9 @@ + + +
+
+
+ +
+
+
diff --git a/src/app/core/home/components/home/home.component.scss b/src/app/core/home/components/home/home.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/home/components/home/home.component.ts b/src/app/core/home/components/home/home.component.ts new file mode 100644 index 0000000..13eb562 --- /dev/null +++ b/src/app/core/home/components/home/home.component.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-home", + templateUrl: "./home.component.html", + styleUrls: ["./home.component.scss"], + standalone: false +}) +export class HomeComponent {} diff --git a/src/app/core/home/components/navigation/navigation.component.html b/src/app/core/home/components/navigation/navigation.component.html new file mode 100644 index 0000000..4ae9b51 --- /dev/null +++ b/src/app/core/home/components/navigation/navigation.component.html @@ -0,0 +1,79 @@ + +
+
+ + + + {{ "title" | translate }} + + + + + + + + + +
+
+ + + diff --git a/src/app/core/home/components/navigation/navigation.component.scss b/src/app/core/home/components/navigation/navigation.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/home/components/navigation/navigation.component.ts b/src/app/core/home/components/navigation/navigation.component.ts new file mode 100644 index 0000000..d41f4d4 --- /dev/null +++ b/src/app/core/home/components/navigation/navigation.component.ts @@ -0,0 +1,121 @@ +import { Component, OnInit } from "@angular/core"; +import { initFlowbite } from "flowbite"; +import { Router } from "@angular/router"; +import { StateResponse } from "src/app/core/models/user-state-request.model"; +import { AuthService } from "src/app/core/services/auth.service"; + +interface NavigationLink { + imageSrc?: string; + label: string; + routerLink: string; + isVisible?: Function | undefined; +} + +interface UserMenuButton { + label: Function | string; + routerLink?: string | undefined; + clickCallback?: Function | undefined; + isVisible?: Function | undefined; +} + +@Component({ + selector: "app-navigation", + templateUrl: "./navigation.component.html", + styleUrls: ["./navigation.component.scss"], + standalone: false +}) +export class NavigationComponent implements OnInit { + navigationLinks: NavigationLink[] = [ + { + routerLink: "/dashboard", + imageSrc: "assets/template.svg", + label: "navigation.home", + }, + { + routerLink: "/location", + imageSrc: "assets/lightbulb.svg", + label: "navigation.locations", + isVisible: () => this.state?.roleIdentifier === 'admin' + }, + { + routerLink: "/plant", + imageSrc: "assets/template.svg", + label: "navigation.plants", + isVisible: () => this.state?.roleIdentifier === 'admin' + }, + { + routerLink: "/device", + imageSrc: "assets/plug.svg", + label: "navigation.devices", + isVisible: () => this.state?.roleIdentifier === 'admin' + }, + { + routerLink: "/admin", + imageSrc: "assets/admin.svg", + label: "navigation.admin", + isVisible: () => this.state?.roleIdentifier === 'admin' + }, + ]; + + usermenuButtons: UserMenuButton[] = [ + { + label: () => "menu.settings", + routerLink: "/settings", + }, + { + label: () => "menu.login", + clickCallback: () => { + this.router.navigateByUrl('/auth'); + }, + isVisible: () => this.state === undefined + }, + { + label: () => "menu.logout", + clickCallback: () => { + this.logout(); + }, + isVisible: () => this.state !== undefined + }, + ]; + + state: StateResponse | undefined; + + constructor( + private authService: AuthService, + private router: Router, + ) { + this.state = undefined; + + this.authService.readUserState().subscribe(response => { + this.authService.currentState$.next(response); + this.state = response; + }); + } + + ngOnInit(): void { + initFlowbite(); + } + + logout(): void { + this.authService + .logout() + .subscribe( response => { + this.state = undefined; + this.router.navigateByUrl('/dashboard') + }); + } + + clickUserMenuButtonLabel(button: UserMenuButton) { + if (button.clickCallback !== undefined) { + button.clickCallback(); + } + } + + getUserMenuButtonLabel(button: UserMenuButton) { + if (typeof button.label === "function") { + return button.label(); + } else { + return button.label; + } + } +} diff --git a/src/app/core/home/components/settings/settings.component.html b/src/app/core/home/components/settings/settings.component.html new file mode 100644 index 0000000..9f020c3 --- /dev/null +++ b/src/app/core/home/components/settings/settings.component.html @@ -0,0 +1,22 @@ +
+
+
+ {{ state.username }} +
+
+ {{ state.roleIdentifier }} +
+
+ + + + + + + + + +
diff --git a/src/app/core/home/components/settings/settings.component.scss b/src/app/core/home/components/settings/settings.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/home/components/settings/settings.component.ts b/src/app/core/home/components/settings/settings.component.ts new file mode 100644 index 0000000..50028d7 --- /dev/null +++ b/src/app/core/home/components/settings/settings.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from "@angular/core"; +import { initFlowbite } from "flowbite"; +import { StateResponse } from "src/app/core/models/user-state-request.model"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-settings", + templateUrl: "./settings.component.html", + styleUrls: ["./settings.component.scss"], + standalone: false +}) +export class SettingsComponent implements OnInit { + state?: StateResponse; + + constructor(private authService: AuthService) { + this.authService.currentState$.subscribe(state => {this.state = state}); + } + + ngOnInit(): void { + initFlowbite(); + } +} diff --git a/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.html b/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.html new file mode 100644 index 0000000..07c4e45 --- /dev/null +++ b/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.scss b/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.ts b/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.ts new file mode 100644 index 0000000..fd4c60d --- /dev/null +++ b/src/app/core/home/components/settings/tabs/tab-profile/tab-profile.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; + +interface Product { + description: string; + id: string; + price: number; + stock: number; +} + +@Component({ + selector: "app-tab-profile", + templateUrl: "./tab-profile.component.html", + styleUrls: ["./tab-profile.component.scss"], + standalone: false +}) +export class TabProfileComponent { + constructor() {} +} diff --git a/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.html b/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.html new file mode 100644 index 0000000..4ae615c --- /dev/null +++ b/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.html @@ -0,0 +1,33 @@ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+ + +
+
diff --git a/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.scss b/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.ts b/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.ts new file mode 100644 index 0000000..d5f3277 --- /dev/null +++ b/src/app/core/home/components/settings/tabs/tab-security/tab-security.component.ts @@ -0,0 +1,41 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { AuthService } from "src/app/core/services/auth.service"; + +@Component({ + selector: "app-tab-security", + templateUrl: "./tab-security.component.html", + styleUrls: ["./tab-security.component.scss"], + standalone: false +}) +export class TabSecurityComponent { + changePasswordForm = new FormGroup({ + password: new FormControl("", [Validators.required]), + newPassword: new FormControl("", [Validators.required]), + newPasswordConfirmation: new FormControl("", [Validators.required]), + }); + + changeUsernameForm = new FormGroup({ + password: new FormControl("", [Validators.required]), + newUsername: new FormControl("", [Validators.required]), + }); + + constructor(private authService: AuthService) {} + + changePassword(): void { + if (this.changePasswordForm.value.newPassword !== this.changePasswordForm.value.newPasswordConfirmation) + return; + + this.authService.changePassword({ + password: this.changePasswordForm.value.password!, + newPassword: this.changePasswordForm.value.newPassword!, + }).subscribe(response => {}); + } + + changeUsername(): void { + this.authService.changeUsername({ + password: this.changeUsernameForm.value.password!, + newUsername: this.changeUsernameForm.value.newUsername!, + }).subscribe(response => {}); + } +} diff --git a/src/app/core/home/home.module.ts b/src/app/core/home/home.module.ts new file mode 100644 index 0000000..2e2d895 --- /dev/null +++ b/src/app/core/home/home.module.ts @@ -0,0 +1,82 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule, Routes } from "@angular/router"; +import { ReactiveFormsModule } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; +import { SharedModule } from "src/app/shared/shared.module"; +import { AngularSvgIconModule } from "angular-svg-icon"; +import { HomeComponent } from "./components/home/home.component"; +import { AuthGuard } from "../guards/auth.guard"; +import { SettingsComponent } from "./components/settings/settings.component"; +import { TabProfileComponent } from "./components/settings/tabs/tab-profile/tab-profile.component"; +import { TabSecurityComponent } from "./components/settings/tabs/tab-security/tab-security.component"; +import { NavigationComponent } from "./components/navigation/navigation.component"; +import { CoreModule } from "../core.module"; + +const routes: Routes = [ + { + path: "", + component: HomeComponent, + children: [ + { + path: "settings", + canActivate: [AuthGuard], + component: SettingsComponent + }, + { + path: "dashboard", + loadChildren: () => + import('../../feature/dashboard/dashboard.module').then((m) => m.DashboardModule) + }, + { + path: "plant", + canActivate: [AuthGuard], + loadChildren: () => + import('../../feature/plant/plant.module').then((m) => m.PlantModule) + }, + { + path: "device", + canActivate: [AuthGuard], + loadChildren: () => + import('../../feature/device/device.module').then((m) => m.DeviceModule) + }, + { + path: "location", + canActivate: [AuthGuard], + loadChildren: () => + import('../../feature/location/location.module').then((m) => m.LocationModule) + }, + { + path: "admin", + canActivate: [AuthGuard], + loadChildren: () => + import('../../feature/admin/admin.module').then((m) => m.AdminModule) + }, + { + path: '**', + redirectTo: 'dashboard', + pathMatch: 'full' + } + ], + }, +]; + +@NgModule({ + declarations: [ + SettingsComponent, + NavigationComponent, + TabProfileComponent, + TabSecurityComponent, + HomeComponent + ], + imports: [ + RouterModule.forChild(routes), + AngularSvgIconModule, + CommonModule, + ReactiveFormsModule, + TranslateModule, + SharedModule, + CoreModule + ] +}) +export class HomeModule {} diff --git a/src/app/core/models/change-password-request.model.ts b/src/app/core/models/change-password-request.model.ts new file mode 100644 index 0000000..d612faf --- /dev/null +++ b/src/app/core/models/change-password-request.model.ts @@ -0,0 +1,7 @@ +export interface ChangePasswordRequest { + password: string; + newPassword: string; +} + +export interface ChangePasswordResponse { +} diff --git a/src/app/core/models/change-username-request.model.ts b/src/app/core/models/change-username-request.model.ts new file mode 100644 index 0000000..1137d5b --- /dev/null +++ b/src/app/core/models/change-username-request.model.ts @@ -0,0 +1,7 @@ +export interface ChangeUsernameRequest { + password: string; + newUsername: string; +} + +export interface ChangeUsernameResponse { +} diff --git a/src/app/core/models/confirm-registration-request.model.ts b/src/app/core/models/confirm-registration-request.model.ts new file mode 100644 index 0000000..cd5a493 --- /dev/null +++ b/src/app/core/models/confirm-registration-request.model.ts @@ -0,0 +1,12 @@ +export interface ConfirmRegistrationRequest { + id: string; + password: string; + passwordConfirmation: string; +} + +export interface ConfirmRegistrationResponse { + id: string; + username: string; + roleIdentifier: "admin"|"user"; + permissions: string[]; +} diff --git a/src/app/core/models/forgot-password-request.model.ts b/src/app/core/models/forgot-password-request.model.ts new file mode 100644 index 0000000..95d83c3 --- /dev/null +++ b/src/app/core/models/forgot-password-request.model.ts @@ -0,0 +1,6 @@ +export interface ForgotPasswordRequest { + mail: string; +} + +export interface ForgotPasswordResponse { +} diff --git a/src/app/core/models/login-request.model.ts b/src/app/core/models/login-request.model.ts new file mode 100644 index 0000000..7d4d58e --- /dev/null +++ b/src/app/core/models/login-request.model.ts @@ -0,0 +1,8 @@ +export interface LoginUserRequest { + identifier: string; + password: string; +} + +export interface LoginUserResponse { + sessionId: string; +} diff --git a/src/app/core/models/logout-request.model.ts b/src/app/core/models/logout-request.model.ts new file mode 100644 index 0000000..0bbb074 --- /dev/null +++ b/src/app/core/models/logout-request.model.ts @@ -0,0 +1,6 @@ +export interface LogoutUserRequest { +} + +export interface LogoutUserResponse { + response?: string|undefined; +} diff --git a/src/app/core/models/register-user-request.model.ts b/src/app/core/models/register-user-request.model.ts new file mode 100644 index 0000000..4a1df17 --- /dev/null +++ b/src/app/core/models/register-user-request.model.ts @@ -0,0 +1,8 @@ +export interface RegisterUserRequest { + username: string; + mail: string; +} + +export interface RegisterUserResponse { + response?: string|undefined; +} diff --git a/src/app/core/models/reset-password-request.model.ts b/src/app/core/models/reset-password-request.model.ts new file mode 100644 index 0000000..d45ebf6 --- /dev/null +++ b/src/app/core/models/reset-password-request.model.ts @@ -0,0 +1,8 @@ +export interface ResetPasswordRequest { + passwordToken: string; + newPassword: string; + passwordConfirmation: string; +} + +export interface ResetPasswordResponse { +} diff --git a/src/app/core/models/user-state-request.model.ts b/src/app/core/models/user-state-request.model.ts new file mode 100644 index 0000000..634a869 --- /dev/null +++ b/src/app/core/models/user-state-request.model.ts @@ -0,0 +1,13 @@ +export interface StateRequest { +} + +export interface StateResponse { + id: string; + username: string; + roleIdentifier: any|null; + permissions: string[]; + createdAt: string; + updatedAt: string; + sessionId: string; + role?: "admin"|"user"|undefined; +} diff --git a/src/app/core/services/app.service.ts b/src/app/core/services/app.service.ts new file mode 100644 index 0000000..8026d53 --- /dev/null +++ b/src/app/core/services/app.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from "@angular/core"; + +@Injectable() +export class AppService { + darkMode: boolean; + + constructor() { + this.darkMode = + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches; + this.applyDarkMode(); + } + + private applyDarkMode(): void { + if (this.darkMode) { + document.documentElement.classList.add("theme-dark"); + } else { + document.documentElement.classList.remove("theme-dark"); + } + } + + toggleDarkMode() { + this.darkMode = !this.darkMode; + this.applyDarkMode(); + } + + getDarkMode(): boolean { + return this.darkMode; + } +} diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..a4a296d --- /dev/null +++ b/src/app/core/services/auth.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from "@angular/core"; +import { RequestService } from "./request.service"; +import { StateResponse } from "../models/user-state-request.model"; +import { BehaviorSubject, catchError, Observable } from "rxjs"; +import { LoginUserRequest, LoginUserResponse } from "../models/login-request.model"; +import { Router } from "@angular/router"; +import { + RegisterUserRequest, + RegisterUserResponse, +} from "../models/register-user-request.model"; +import { + ConfirmRegistrationRequest, + ConfirmRegistrationResponse, +} from "../models/confirm-registration-request.model"; +import { + ResetPasswordRequest, + ResetPasswordResponse, +} from "../models/reset-password-request.model"; +import { + ForgotPasswordRequest, + ForgotPasswordResponse, +} from "../models/forgot-password-request.model"; +import { + ChangePasswordRequest, + ChangePasswordResponse, +} from "../models/change-password-request.model"; +import { + ChangeUsernameRequest, + ChangeUsernameResponse, +} from "../models/change-username-request.model"; +import { LogoutUserResponse } from "../models/logout-request.model"; + +@Injectable() +export class AuthService { + currentState$ = new BehaviorSubject( + undefined + ); + + constructor(private requestService: RequestService, private router: Router) {} + + readUserState() + { + return this.requestService.request( + 'get', + this.requestService.obtainUrl("user/state"), + {} + ) + .pipe( + catchError( + error => { + this.currentState$.next(undefined); + throw 'User State not readable'; + } + ) + ); + } + + logout(): Observable { + this.currentState$.next(undefined); + return this.requestService.call( + "get", + "auth/logout-user", + {} + ); + } + + login(body: LoginUserRequest): Observable { + return this.requestService.call( + 'post', + "auth/login-user", + body + ); + } + + register(body: RegisterUserRequest): Observable { + return this.requestService.call( + "post", + "auth/register-user", + body, + ); + } + + confirmRegistration(body: ConfirmRegistrationRequest): Observable { + return this.requestService.call( + 'post', + 'auth/confirm-registration', + body, + ); + } + + resetPassword(body: ResetPasswordRequest): Observable { + return this.requestService.call( + "post", + "auth/reset-password", + body, + ); + } + + forgotPassword(body: ForgotPasswordRequest): Observable { + return this.requestService.call( + 'post', + "auth/forgot-password", + body, + ); + } + + changePassword(body: ChangePasswordRequest): Observable { + return this.requestService.call( + 'post', + "user/change-password", + body, + ); + } + + changeUsername(body: ChangeUsernameRequest): Observable { + return this.requestService.call( + 'post', + "user/change-username", + body, + ); + } +} diff --git a/src/app/core/services/notification.service.ts b/src/app/core/services/notification.service.ts new file mode 100644 index 0000000..a53ca9c --- /dev/null +++ b/src/app/core/services/notification.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject, Observable, Subject } from "rxjs"; +import { NotificationElement } from "../components/notification-bar/notification-bar.component"; + +@Injectable() +export class NotificationService { + notification$ = new Subject(); + + push(message: string, type: "info" | "danger" | "warn" | "success"): void { + let timeout = undefined; + switch (type) { + case "info": + timeout = 5; + break; + case "danger": + timeout = 10; + break; + case "warn": + timeout = 7; + break; + case "success": + timeout = 3; + break; + } + + this.notification$.next({ + message: message, + type: type, + timeout: timeout, + }); + } +} diff --git a/src/app/core/services/request.service.ts b/src/app/core/services/request.service.ts index 38ec3fa..9ed625d 100644 --- a/src/app/core/services/request.service.ts +++ b/src/app/core/services/request.service.ts @@ -1,105 +1,83 @@ -import { HttpClient } from '@angular/common/http'; -import { Router } from '@angular/router'; -import { Injectable } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { HttpClient } from "@angular/common/http"; +import { Router } from "@angular/router"; +import { Injectable } from "@angular/core"; +import { NotificationService } from "./notification.service"; +import { TranslateService } from "@ngx-translate/core"; +import { Observable, catchError, first, map, take } from "rxjs"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class RequestService { - constructor( - private http: HttpClient, + private http: HttpClient, private router: Router, - private snackBar: MatSnackBar - ) { - } + private notificationService: NotificationService, + private translate: TranslateService + ) {} - post(apiPath: string, body: any, fct: Function) - { - let url = this.obtainUrl(apiPath); - let observable = this.http.post(url, body).subscribe( - (answer:any) => { - fct(answer); - }, - (error:any) => { - this.handleError(error); - } + public call(method: 'post' | 'get', path: string, body: any|undefined): Observable { + return this.request( + method, + this.obtainUrl(path), + body + ).pipe( + catchError( + error => { + this.handleError(error) + throw 'Error occurred, during API call' + } + ) ); } - postFiles(apiPath: string, files: File[], fct: Function) { - if (!files || files.length === 0) { - throw 'Need to select at least one file'; + public request(method:'get'|'post', url: string, body: any|undefined): Observable { + if(method === 'get') { + return this.http.get(url); + } else { + return this.http.post(url, body); } - - const formData = new FormData(); - - for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { - formData.append('file' + fileIndex, files[fileIndex]); - } - - let url = this.obtainUrl(apiPath); - let observable = this.http.post(url, formData).subscribe( - (answer: any) => { - fct(answer); - }, - (error: any) => { - this.handleError(error); - } - ); - } - - get(apiPath: string, body: any, fct: Function) - { - let url = this.obtainUrl(apiPath); - let observable = this.http.get(url, body).subscribe( - (answer:any) => { - fct(answer); - }, - (error:any) => { - this.handleError(error); - } - ); } - private obtainUrl(apiPath: string): string { + public obtainUrl(apiPath: string): string { let hostString = window.location.host; let protocol = window.location.protocol; - return protocol + '//' + hostString + '/api/' + apiPath; + return protocol + "//" + hostString + "/api/" + apiPath; } - private handleError(answer: any): void { - + public handleError(answer: any): void { if (answer.status == 401) { - console.log('Deine Sitzung konnte nicht gefunden werden'); - this.router.navigate(['/auth']); + this.notificationService.push( + this.translate.instant("error.unauthorized"), + 'info' + ); + this.router.navigate(["/auth"]); return; } - + if (answer.status == 403) { - this.showSnackBar('Du bist nicht für diese Aktion autorisiert', 'Ok'); + this.notificationService.push( + this.translate.instant("error.forbidden"), + 'info' + ); return; } - + try { let errorObject = answer.error.error; - if (errorObject.hasOwnProperty('message')) { + if (errorObject.hasOwnProperty("code")) { + throw ( + this.translate.instant("error." + errorObject.code.toString()) ?? + errorObject.code.toString() + ); + } + + if (errorObject.hasOwnProperty("message")) { throw errorObject.message.toString(); } - - if (errorObject.hasOwnProperty('code')) { - throw errorObject.code.toString(); - } - } catch(error:any) { - this.showSnackBar(error.toString(), 'Ok'); + } catch (error: any) { + this.notificationService.push(error.toString(), 'danger'); } } - - private showSnackBar(message: string, action?: string) { - this.snackBar.open(message.toString(), action, { - duration: 3000, - }); - } } diff --git a/src/app/shared/components/button/button.component.html b/src/app/shared/components/button/button.component.html new file mode 100644 index 0000000..3e5dc07 --- /dev/null +++ b/src/app/shared/components/button/button.component.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/app/shared/components/button/button.component.scss b/src/app/shared/components/button/button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/button/button.component.ts b/src/app/shared/components/button/button.component.ts new file mode 100644 index 0000000..a5b7a53 --- /dev/null +++ b/src/app/shared/components/button/button.component.ts @@ -0,0 +1,13 @@ +import { Component, EventEmitter, inject, Input, Output } from "@angular/core"; +import { ControlContainer, FormGroup } from "@angular/forms"; + +@Component({ + selector: "shared-button", + templateUrl: "./button.component.html", + styleUrl: "./button.component.scss", + standalone: false +}) +export class ButtonComponent { + @Output() onClick = new EventEmitter(); + @Input({ required: true }) label!: string; +} diff --git a/src/app/shared/components/card/card.component.html b/src/app/shared/components/card/card.component.html index 02ae685..1925a7f 100644 --- a/src/app/shared/components/card/card.component.html +++ b/src/app/shared/components/card/card.component.html @@ -1,31 +1,11 @@ -
-