Initial commit: Angular PV Pulse App
This commit is contained in:
72
src/app/core/services/auth.service.ts
Normal file
72
src/app/core/services/auth.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
const TOKEN_KEY = 'pv_access_token';
|
||||
|
||||
interface TokenResponse {
|
||||
token?: string;
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly settingsService = inject(SettingsService);
|
||||
|
||||
private readonly _token = signal<string | null>(localStorage.getItem(TOKEN_KEY));
|
||||
|
||||
/** Aktuell gespeicherter Bearer-Token (reaktiv) */
|
||||
readonly token = this._token.asReadonly();
|
||||
|
||||
/** Gibt den aktuellen Token zurück */
|
||||
getToken(): string | null {
|
||||
return this._token();
|
||||
}
|
||||
|
||||
/** Löscht den Token aus Speicher und State */
|
||||
clearToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
this._token.set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet Anmeldedaten an den Auth-Endpunkt und speichert den erhaltenen Token.
|
||||
* Unterstützt sowohl `{ token }` als auch `{ access_token }` als Antwortformat.
|
||||
*/
|
||||
async login(username: string, password: string): Promise<string> {
|
||||
const { apiBaseUrl, authEndpoint } = this.settingsService.settings();
|
||||
const url = `${apiBaseUrl}${authEndpoint}`;
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.http.post<TokenResponse>(url, { username, password })
|
||||
);
|
||||
|
||||
const token = response.token ?? response.access_token;
|
||||
if (!token) {
|
||||
throw new Error('Kein Token in der Server-Antwort gefunden (erwartet: token oder access_token)');
|
||||
}
|
||||
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
this._token.set(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird beim App-Start aufgerufen.
|
||||
* Wenn Anmeldedaten gespeichert sind und kein Mock-Modus aktiv ist,
|
||||
* wird automatisch ein frischer Token abgerufen.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
const { username, password, useMocks } = this.settingsService.settings();
|
||||
if (useMocks || !username || !password) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.login(username, password);
|
||||
} catch (err) {
|
||||
console.warn('[AuthService] Auto-Login beim Start fehlgeschlagen:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/app/core/services/live-data.service.ts
Normal file
50
src/app/core/services/live-data.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { inject, Injectable, OnDestroy } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {
|
||||
Observable,
|
||||
timer,
|
||||
switchMap,
|
||||
distinctUntilChanged,
|
||||
shareReplay,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PvLiveData, PvHistoryData } from '../models/pv-data.model';
|
||||
import { ApiResponse } from '../models/api-response.model';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LiveDataService implements OnDestroy {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly settingsService = inject(SettingsService);
|
||||
|
||||
/** Liefert Livedaten per Polling alle `pollingIntervalMs` Millisekunden. */
|
||||
getLiveData(plantId: string): Observable<PvLiveData> {
|
||||
const intervalMs = this.settingsService.settings().pollingIntervalMs;
|
||||
return timer(0, intervalMs).pipe(
|
||||
switchMap(() => {
|
||||
const { apiBaseUrl, endpointLiveData } = this.settingsService.settings();
|
||||
const url = apiBaseUrl + this.settingsService.resolveEndpoint(endpointLiveData, plantId);
|
||||
return this.http
|
||||
.get<ApiResponse<PvLiveData>>(url)
|
||||
.pipe(map((res) => res.data));
|
||||
}),
|
||||
distinctUntilChanged(
|
||||
(a, b) => a.timestamp === b.timestamp
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
);
|
||||
}
|
||||
|
||||
getHistory(
|
||||
plantId: string,
|
||||
resolution: 'hour' | 'day' | 'month' | 'year' = 'day'
|
||||
): Observable<PvHistoryData> {
|
||||
const { apiBaseUrl, endpointHistory } = this.settingsService.settings();
|
||||
const url = apiBaseUrl + this.settingsService.resolveEndpoint(endpointHistory, plantId);
|
||||
return this.http
|
||||
.get<ApiResponse<PvHistoryData>>(url, { params: { resolution } })
|
||||
.pipe(map((res) => res.data));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
27
src/app/core/services/pv-plant.service.ts
Normal file
27
src/app/core/services/pv-plant.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PvPlant } from '../models/pv-plant.model';
|
||||
import { ApiResponse } from '../models/api-response.model';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PvPlantService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly settingsService = inject(SettingsService);
|
||||
|
||||
getAll(): Observable<PvPlant[]> {
|
||||
const { apiBaseUrl, endpointPlants } = this.settingsService.settings();
|
||||
return this.http
|
||||
.get<ApiResponse<PvPlant[]>>(apiBaseUrl + endpointPlants)
|
||||
.pipe(map((res) => res.data));
|
||||
}
|
||||
|
||||
getById(id: string): Observable<PvPlant> {
|
||||
const { apiBaseUrl, endpointPlants } = this.settingsService.settings();
|
||||
return this.http
|
||||
.get<ApiResponse<PvPlant>>(`${apiBaseUrl}${endpointPlants}/${encodeURIComponent(id)}`)
|
||||
.pipe(map((res) => res.data));
|
||||
}
|
||||
}
|
||||
78
src/app/core/services/settings.service.ts
Normal file
78
src/app/core/services/settings.service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AppSettings } from '../models/settings.model';
|
||||
|
||||
const STORAGE_KEY = 'pv_app_settings';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsService {
|
||||
private readonly _settings = signal<AppSettings>(this.loadFromStorage());
|
||||
private _pinVerified = false;
|
||||
|
||||
/** Aktuell aktive Einstellungen (reaktiv) */
|
||||
readonly settings = this._settings.asReadonly();
|
||||
|
||||
private defaultSettings(): AppSettings {
|
||||
return {
|
||||
apiBaseUrl: environment.apiBaseUrl,
|
||||
useMocks: environment.useMocks,
|
||||
pollingIntervalMs: environment.pollingIntervalMs,
|
||||
authEndpoint: '/auth/token',
|
||||
username: '',
|
||||
password: '',
|
||||
endpointPlants: '/plants',
|
||||
endpointLiveData: '/plants/{id}/live',
|
||||
endpointHistory: '/plants/{id}/history',
|
||||
};
|
||||
}
|
||||
|
||||
private loadFromStorage(): AppSettings {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
return { ...this.defaultSettings(), ...(JSON.parse(raw) as Partial<AppSettings>) };
|
||||
}
|
||||
} catch {
|
||||
// localStorage nicht verfügbar oder ungültiges JSON – Standardwerte verwenden
|
||||
}
|
||||
return this.defaultSettings();
|
||||
}
|
||||
|
||||
/** Speichert Einstellungen in localStorage und aktualisiert das reaktive Signal */
|
||||
save(settings: AppSettings): void {
|
||||
this._settings.set({ ...settings });
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
/** Löst den {id}-Platzhalter in einem Endpunkt-Template auf */
|
||||
resolveEndpoint(template: string, id: string): string {
|
||||
return template.replace('{id}', encodeURIComponent(id));
|
||||
}
|
||||
|
||||
/** Prüft ob der eingegebene PIN dem heutigen Datum entspricht (DDMM) */
|
||||
isValidPin(pin: string): boolean {
|
||||
const now = new Date();
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return pin === `${day}${month}`;
|
||||
}
|
||||
|
||||
/** Verifiziert den PIN und merkt sich das Ergebnis für den Route-Guard */
|
||||
verifyPin(pin: string): boolean {
|
||||
if (this.isValidPin(pin)) {
|
||||
this._pinVerified = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Gibt zurück ob der PIN in dieser Session bereits erfolgreich eingegeben wurde */
|
||||
get isPinVerified(): boolean {
|
||||
return this._pinVerified;
|
||||
}
|
||||
|
||||
/** Setzt die PIN-Verifizierung zurück (z.B. nach Logout) */
|
||||
resetPinVerification(): void {
|
||||
this._pinVerified = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user