diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..432b482 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Vegasco-Web/angular.json b/src/Vegasco-Web/angular.json index 54594d0..e053d58 100644 --- a/src/Vegasco-Web/angular.json +++ b/src/Vegasco-Web/angular.json @@ -47,12 +47,24 @@ "maximumError": "8kB" } ], - "outputHashing": "all" + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.production.ts" + } + ] }, "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/src/Vegasco-Web/package.json b/src/Vegasco-Web/package.json index 1dcc17f..c83237b 100644 --- a/src/Vegasco-Web/package.json +++ b/src/Vegasco-Web/package.json @@ -18,6 +18,7 @@ "@angular/forms": "^20.0.3", "@angular/platform-browser": "^20.0.3", "@angular/router": "^20.0.3", + "keycloak-angular": "^19.0.2", "rxjs": "~7.8.2", "tslib": "^2.8.1", "zone.js": "~0.15.1" diff --git a/src/Vegasco-Web/pnpm-lock.yaml b/src/Vegasco-Web/pnpm-lock.yaml index c281cbf..bea5b4e 100644 --- a/src/Vegasco-Web/pnpm-lock.yaml +++ b/src/Vegasco-Web/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@angular/router': specifier: ^20.0.3 version: 20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + keycloak-angular: + specifier: ^19.0.2 + version: 19.0.2(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(keycloak-js@26.2.0) rxjs: specifier: ~7.8.2 version: 7.8.2 @@ -1769,6 +1772,17 @@ packages: engines: {node: '>= 10'} hasBin: true + keycloak-angular@19.0.2: + resolution: {integrity: sha512-GzQKC/jFJLZRmUxWOEXkla+6shDAZFAOe6Z3qsw916Ckb/UhZnO704HMZrd8xyVB3RH6xOcNCp45oHmIiqJ7dA==} + peerDependencies: + '@angular/common': ^19 + '@angular/core': ^19 + '@angular/router': ^19 + keycloak-js: ^18 || ^19 || ^20 || ^21 || ^22 || ^23 || ^24 || ^25 || ^26 + + keycloak-js@26.2.0: + resolution: {integrity: sha512-CrFcXTN+d6J0V/1v3Zpioys6qHNWE6yUzVVIsCUAmFn9H14GZ0vuYod+lt+SSpMgWGPuneDZBSGBAeLBFuqjsw==} + listr2@8.3.3: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} @@ -4244,6 +4258,16 @@ snapshots: - supports-color - utf-8-validate + keycloak-angular@19.0.2(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(keycloak-js@26.2.0): + dependencies: + '@angular/common': 20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/router': 20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.3(@angular/common@20.0.3(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.3(@angular/compiler@20.0.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + keycloak-js: 26.2.0 + tslib: 2.8.1 + + keycloak-js@26.2.0: {} + listr2@8.3.3: dependencies: cli-truncate: 4.0.0 diff --git a/src/Vegasco-Web/src/app/api/models/consumption-entry.ts b/src/Vegasco-Web/src/app/api/models/consumption-entry.ts new file mode 100644 index 0000000..66cd3fc --- /dev/null +++ b/src/Vegasco-Web/src/app/api/models/consumption-entry.ts @@ -0,0 +1,8 @@ +interface ConsumptionEntry { + id: string; + dateTime: string; + distance: number; + amount: number; + ignoreInCalculation: boolean; + carId: string; +} diff --git a/src/Vegasco-Web/src/app/app.config.ts b/src/Vegasco-Web/src/app/app.config.ts index 72e9f2c..3df29db 100644 --- a/src/Vegasco-Web/src/app/app.config.ts +++ b/src/Vegasco-Web/src/app/app.config.ts @@ -1,14 +1,17 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; -import {provideHttpClient} from '@angular/common/http'; +import {provideHttpClient, withInterceptors} from '@angular/common/http'; +import { provideKeycloakAngular } from './auth/auth.config'; +import { includeBearerTokenInterceptor } from 'keycloak-angular'; export const appConfig: ApplicationConfig = { providers: [ + provideKeycloakAngular(), provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), - provideHttpClient(), + provideRouter(routes, withComponentInputBinding()), + provideHttpClient(withInterceptors([includeBearerTokenInterceptor])), ] }; diff --git a/src/Vegasco-Web/src/app/app.html b/src/Vegasco-Web/src/app/app.html index 7cb32c6..530a5f8 100644 --- a/src/Vegasco-Web/src/app/app.html +++ b/src/Vegasco-Web/src/app/app.html @@ -1,366 +1,5 @@ - - - - - - - - - - -
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- @if (serverInfo$ | async; as serverInfo) { - - - - - - - - - - - - - - - - - -
VersionCommit IDCommit DateEnvironment
{{ serverInfo.fullVersion }}{{ serverInfo.commitId }}{{ serverInfo.commitDate | date:"dd.MM.yyyy HH:mm:ss" }}{{ serverInfo.environment }}
- } -
-
- -
-
- @for (item of [ - {title: 'Explore the Docs', link: 'https://angular.dev'}, - {title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials'}, - {title: 'CLI Docs', link: 'https://angular.dev/tools/cli'}, - {title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service'}, - {title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools'}, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
+
- - - - - - - - - - - diff --git a/src/Vegasco-Web/src/app/app.routes.ts b/src/Vegasco-Web/src/app/app.routes.ts index dc39edb..d6a5093 100644 --- a/src/Vegasco-Web/src/app/app.routes.ts +++ b/src/Vegasco-Web/src/app/app.routes.ts @@ -1,3 +1,13 @@ import { Routes } from '@angular/router'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + redirectTo: 'entries', + pathMatch: 'full' + }, + { + path: 'entries', + loadChildren: () => import('./modules/entries/entries.routes').then(m => m.routes) + } +]; diff --git a/src/Vegasco-Web/src/app/app.ts b/src/Vegasco-Web/src/app/app.ts index b9a2c0a..98798e3 100644 --- a/src/Vegasco-Web/src/app/app.ts +++ b/src/Vegasco-Web/src/app/app.ts @@ -1,33 +1,12 @@ -import {Component, inject} from '@angular/core'; -import {RouterOutlet} from '@angular/router'; -import {HttpClient} from '@angular/common/http'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {AsyncPipe, DatePipe} from '@angular/common'; -import {tap} from 'rxjs'; +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', - imports: [AsyncPipe, DatePipe, RouterOutlet], + imports: [RouterOutlet], templateUrl: './app.html', styleUrl: './app.scss' }) export class App { protected title = 'Vegasco-Web'; - - private readonly http = inject(HttpClient); - - protected readonly serverInfo$; - - constructor() { - this.serverInfo$ = this.http.get('/api/v1/info/server') - .pipe(takeUntilDestroyed()); - } } - -interface ServerInfo { - fullVersion: string; - commitId: string; - commitDate: string; - environment: string; -} - diff --git a/src/Vegasco-Web/src/app/auth/auth.config.ts b/src/Vegasco-Web/src/app/auth/auth.config.ts new file mode 100644 index 0000000..ad6227b --- /dev/null +++ b/src/Vegasco-Web/src/app/auth/auth.config.ts @@ -0,0 +1,45 @@ +import { environment } from '../../environments/environment'; +import { + provideKeycloak, + createInterceptorCondition, + IncludeBearerTokenCondition, + INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG, + withAutoRefreshToken, + AutoRefreshTokenService, + UserActivityService +} from 'keycloak-angular'; + +const serverHostBearerInterceptorCondition = createInterceptorCondition({ + // The API is consumed through a proxy running on the same origin as the application. + // This means that the interceptor should include the bearer token for requests to the same origin + // which includes requests starting to / which implicitly sends the request to the same origin. + urlPattern: new RegExp(`^(${window.origin}|/)`) +}); + +export const provideKeycloakAngular = () => + provideKeycloak({ + config: { + url: environment.keycloak.host, + realm: environment.keycloak.realm, + clientId: environment.keycloak.clientId, + }, + initOptions: { + onLoad: 'login-required', + silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + redirectUri: window.location.origin + '/', + checkLoginIframe: false, + }, + features: [ + withAutoRefreshToken({ + onInactivityTimeout: 'login', + }) + ], + providers: [ + AutoRefreshTokenService, + UserActivityService, + { + provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG, + useValue: [serverHostBearerInterceptorCondition] + } + ] + }); diff --git a/src/Vegasco-Web/src/app/modules/entries/entries.routes.ts b/src/Vegasco-Web/src/app/modules/entries/entries.routes.ts new file mode 100644 index 0000000..598ef33 --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries.routes.ts @@ -0,0 +1,9 @@ +import { Routes } from "@angular/router"; +import { EntriesComponent } from "./entries/entries.component"; + +export const routes: Routes = [ + { + path: '', + component: EntriesComponent + } +]; \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html new file mode 100644 index 0000000..dce0f49 --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html @@ -0,0 +1,22 @@ +@if (consumptionEntries$ | async; as consumptionEntries) { +
+ + + + + + + + + + @for (entry of consumptionEntries; track entry.id) { + + + + + + } + +
DatumDistanzMenge
{{ entry.dateTime | date }}{{ entry.distance }} km{{ entry.amount }} l
+
+} \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.scss b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.scss new file mode 100644 index 0000000..e71c33b --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.scss @@ -0,0 +1,3 @@ +th, td { + padding: 0.5rem; +} \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts new file mode 100644 index 0000000..c9e5b8a --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts @@ -0,0 +1,27 @@ +import { AsyncPipe, DatePipe } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable, tap } from 'rxjs'; + +@Component({ + selector: 'app-entries', + imports: [AsyncPipe, DatePipe], + templateUrl: './entries.component.html', + styleUrl: './entries.component.scss' +}) +export class EntriesComponent { + private readonly http = inject(HttpClient); + + protected readonly consumptionEntries$: Observable; + + constructor() { + this.consumptionEntries$ = this.http.get('/api/v1/consumptions') + .pipe( + takeUntilDestroyed(), + tap((response) => { + console.log('Entries response:', response); + }), + ); + } +} diff --git a/src/Vegasco-Web/src/environments/environment.development.ts b/src/Vegasco-Web/src/environments/environment.development.ts new file mode 100644 index 0000000..3135d63 --- /dev/null +++ b/src/Vegasco-Web/src/environments/environment.development.ts @@ -0,0 +1,11 @@ +import { Environment } from "./environment.interface"; + +export const environment: Environment = { + name: "Dev", + isProduction: false, + keycloak: { + host: "https://login.nuyken.dev", + realm: "development", + clientId: "vegasco" + } +}; diff --git a/src/Vegasco-Web/src/environments/environment.interface.ts b/src/Vegasco-Web/src/environments/environment.interface.ts new file mode 100644 index 0000000..ed18e6d --- /dev/null +++ b/src/Vegasco-Web/src/environments/environment.interface.ts @@ -0,0 +1,17 @@ + +/** The app's configuration based on the target environment */ +export interface Environment { + /** A name for this configuration, e.g. 'Prod' */ + name: string; + /** Whether this configuration is for production or not */ + isProduction: boolean; + /** Keycloak login configuration */ + keycloak: { + /** The host under which the keycloak is reachable */ + host: string; + /** The keycloak realm in which the client lives */ + realm: string; + /** The app's client id */ + clientId: string; + } +} diff --git a/src/Vegasco-Web/src/environments/environment.production.ts b/src/Vegasco-Web/src/environments/environment.production.ts new file mode 100644 index 0000000..5c0d939 --- /dev/null +++ b/src/Vegasco-Web/src/environments/environment.production.ts @@ -0,0 +1,11 @@ +import { Environment } from "./environment.interface"; + +export const environment: Environment = { + name: "Prod", + isProduction: true, + keycloak: { + host: "https://login.nuyken.dev", + realm: "apps", + clientId: "vegasco" + } +}; diff --git a/src/Vegasco-Web/src/environments/environment.ts b/src/Vegasco-Web/src/environments/environment.ts new file mode 100644 index 0000000..4f4cb10 --- /dev/null +++ b/src/Vegasco-Web/src/environments/environment.ts @@ -0,0 +1,11 @@ +import { Environment } from "./environment.interface"; + +export const environment: Environment = { + name: "", + isProduction: false, + keycloak: { + host: "", + realm: "", + clientId: "" + } +};