localstorage service u. auslagerung

This commit is contained in:
Tizian.Breuch
2025-10-24 14:11:54 +02:00
parent dfb2968510
commit 1ec7ac6ccc
8 changed files with 182 additions and 101 deletions

1
public/icons/filter.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

1
public/icons/menu.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 193 B

1
public/icons/plus.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/></svg>

After

Width:  |  Height:  |  Size: 182 B

View File

@@ -1,53 +1,53 @@
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core'; // /src/app/core/components/cookie-consent/cookie-consent.component.ts
import { isPlatformBrowser, CommonModule } from '@angular/common'; // isPlatformBrowser importieren
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StorageService } from '../../services/storage.service'; // <-- NEUER IMPORT
// Ein Enum für die verschiedenen Zustände ist typsicherer als reine Strings
type ConsentStatus = 'accepted' | 'declined';
@Component({ @Component({
selector: 'app-cookie-consent', selector: 'app-cookie-consent',
standalone: true, // <-- standalone: true hinzufügen, falls es fehlt
imports: [CommonModule], imports: [CommonModule],
templateUrl: './cookie-consent.component.html', templateUrl: './cookie-consent.component.html',
styleUrl: './cookie-consent.component.css' styleUrl: './cookie-consent.component.css'
}) })
export class CookieConsentComponent implements OnInit { export class CookieConsentComponent implements OnInit {
// --- Abhängigkeiten mit moderner inject()-Syntax ---
private storageService = inject(StorageService);
isVisible = false; isVisible = false;
private readonly consentKey = 'cookie_consent_status'; private readonly consentKey = 'cookie_consent_status';
private isBrowser: boolean;
// Wir injizieren PLATFORM_ID, um die aktuelle Plattform (Browser/Server) zu ermitteln. // Der Konstruktor wird viel sauberer oder kann ganz entfallen
constructor(@Inject(PLATFORM_ID) private platformId: Object) { constructor() {}
// Speichere das Ergebnis in einer Eigenschaft, um es wiederverwenden zu können.
this.isBrowser = isPlatformBrowser(this.platformId);
}
ngOnInit(): void { ngOnInit(): void {
// Wir führen die Prüfung nur aus, wenn der Code im Browser läuft. // Der Service kümmert sich um die Browser-Prüfung.
if (this.isBrowser) { // Wir können ihn einfach immer aufrufen.
this.checkConsent(); this.checkConsent();
} }
}
private checkConsent(): void { private checkConsent(): void {
const consent = localStorage.getItem(this.consentKey); const consent = this.storageService.getItem<ConsentStatus>(this.consentKey);
// Das Banner wird nur angezeigt, wenn noch gar nichts gespeichert wurde (consent ist null)
if (!consent) { if (!consent) {
this.isVisible = true; this.isVisible = true;
} }
} }
accept(): void { accept(): void {
// Wir stellen sicher, dass wir localStorage nur im Browser schreiben. // Der Service kümmert sich um die Browser-Prüfung und Serialisierung.
if (this.isBrowser) { this.storageService.setItem(this.consentKey, 'accepted');
localStorage.setItem(this.consentKey, 'accepted');
this.isVisible = false; this.isVisible = false;
console.log('Cookies wurden akzeptiert.'); console.log('Cookies wurden akzeptiert.');
} }
}
decline(): void { decline(): void {
// Wir stellen sicher, dass wir localStorage nur im Browser schreiben. this.storageService.setItem(this.consentKey, 'declined');
if (this.isBrowser) {
localStorage.setItem(this.consentKey, 'declined');
this.isVisible = false; this.isVisible = false;
console.log('Cookies wurden abgelehnt.'); console.log('Cookies wurden abgelehnt.');
} }
}
} }

View File

@@ -1,17 +1,18 @@
// /src/app/core/services/auth.service.ts // /src/app/core/services/auth.service.ts
import { Injectable, PLATFORM_ID, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, BehaviorSubject, of } from 'rxjs'; import { Observable, BehaviorSubject, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators'; import { tap, catchError } from 'rxjs/operators';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
// Eigene Imports
import { LoginRequest, AuthResponse, RegisterRequest } from '../models/auth.models'; import { LoginRequest, AuthResponse, RegisterRequest } from '../models/auth.models';
import { API_URL } from '../tokens/api-url.token'; import { API_URL } from '../tokens/api-url.token';
import { StorageService } from './storage.service';
// Ein Hilfs-Interface, um die Struktur des dekodierten Tokens typsicher zu machen. // Hilfs-Interface für die dekodierten Token-Daten.
interface DecodedToken { interface DecodedToken {
exp: number; // Expiration time als UNIX-Timestamp in Sekunden exp: number; // Expiration time als UNIX-Timestamp in Sekunden
// Hier könnten weitere Claims wie 'email', 'sub', 'role' etc. stehen // Hier könnten weitere Claims wie 'email', 'sub', 'role' etc. stehen
@@ -21,24 +22,22 @@ interface DecodedToken {
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService { export class AuthService {
// --- Injizierte Abhängigkeiten mit moderner Syntax --- // --- Injizierte Abhängigkeiten ---
private http = inject(HttpClient); private http = inject(HttpClient);
private router = inject(Router); private router = inject(Router);
private platformId = inject(PLATFORM_ID); private storageService = inject(StorageService); // <-- NEU
private apiUrl = inject(API_URL); private apiUrl = inject(API_URL);
private readonly endpoint = '/Auth'; private readonly endpoint = '/Auth';
private readonly TOKEN_KEY = 'auth-token'; private readonly TOKEN_KEY = 'auth-token';
private readonly ROLES_KEY = 'auth-user-roles'; private readonly ROLES_KEY = 'auth-user-roles';
private loggedInStatus = new BehaviorSubject<boolean>(this.isBrowser() && !!this.getToken()); private loggedInStatus = new BehaviorSubject<boolean>(!!this.getToken());
public isLoggedIn$ = this.loggedInStatus.asObservable(); public isLoggedIn$ = this.loggedInStatus.asObservable();
private tokenExpirationTimer: any; private tokenExpirationTimer: any;
constructor() { constructor() {
// Beim Initialisieren des Services prüfen, ob bereits ein Token vorhanden ist
// und ggf. den Logout-Timer dafür starten (z.B. nach einem Neuladen der Seite).
this.initTokenCheck(); this.initTokenCheck();
} }
@@ -47,7 +46,7 @@ export class AuthService {
tap(response => { tap(response => {
if (response?.isAuthSuccessful) { if (response?.isAuthSuccessful) {
this.setSession(response); this.setSession(response);
this.startTokenExpirationTimer(); // Timer nach erfolgreichem Login starten this.startTokenExpirationTimer();
} }
}), }),
catchError(() => { catchError(() => {
@@ -62,7 +61,7 @@ export class AuthService {
tap(response => { tap(response => {
if (response?.isAuthSuccessful) { if (response?.isAuthSuccessful) {
this.setSession(response); this.setSession(response);
this.startTokenExpirationTimer(); // Timer nach erfolgreichem Login starten this.startTokenExpirationTimer();
} }
}), }),
catchError(() => { catchError(() => {
@@ -80,7 +79,6 @@ export class AuthService {
logout(): void { logout(): void {
this.clearSession(); this.clearSession();
// Den proaktiven Timer stoppen, da der Logout manuell erfolgt.
if (this.tokenExpirationTimer) { if (this.tokenExpirationTimer) {
clearTimeout(this.tokenExpirationTimer); clearTimeout(this.tokenExpirationTimer);
} }
@@ -88,13 +86,11 @@ export class AuthService {
} }
getToken(): string | null { getToken(): string | null {
return this.isBrowser() ? localStorage.getItem(this.TOKEN_KEY) : null; return this.storageService.getItem<string>(this.TOKEN_KEY);
} }
getUserRoles(): string[] { getUserRoles(): string[] {
if (!this.isBrowser()) return []; return this.storageService.getItem<string[]>(this.ROLES_KEY) || [];
const roles = localStorage.getItem(this.ROLES_KEY);
return roles ? JSON.parse(roles) : [];
} }
hasRole(requiredRole: string): boolean { hasRole(requiredRole: string): boolean {
@@ -102,25 +98,19 @@ export class AuthService {
} }
private setSession(authResponse: AuthResponse): void { private setSession(authResponse: AuthResponse): void {
if (this.isBrowser() && authResponse?.token && authResponse?.roles) { if (authResponse?.token && authResponse?.roles) {
localStorage.setItem(this.TOKEN_KEY, authResponse.token); this.storageService.setItem(this.TOKEN_KEY, authResponse.token);
localStorage.setItem(this.ROLES_KEY, JSON.stringify(authResponse.roles)); this.storageService.setItem(this.ROLES_KEY, authResponse.roles);
this.loggedInStatus.next(true); this.loggedInStatus.next(true);
} }
} }
private clearSession(): void { private clearSession(): void {
if (this.isBrowser()) { this.storageService.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.TOKEN_KEY); this.storageService.removeItem(this.ROLES_KEY);
localStorage.removeItem(this.ROLES_KEY);
}
this.loggedInStatus.next(false); this.loggedInStatus.next(false);
} }
private isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
private startTokenExpirationTimer(): void { private startTokenExpirationTimer(): void {
if (this.tokenExpirationTimer) { if (this.tokenExpirationTimer) {
clearTimeout(this.tokenExpirationTimer); clearTimeout(this.tokenExpirationTimer);
@@ -133,18 +123,21 @@ export class AuthService {
try { try {
const decodedToken = jwtDecode<DecodedToken>(token); const decodedToken = jwtDecode<DecodedToken>(token);
const expirationDate = new Date(decodedToken.exp * 1000); // JWT 'exp' ist in Sekunden, Date() braucht Millisekunden const expirationDate = new Date(decodedToken.exp * 1000);
const timeoutDuration = expirationDate.getTime() - new Date().getTime(); const timeoutDuration = expirationDate.getTime() - new Date().getTime();
if (timeoutDuration > 0) { // Puffer, um Clock-Skew-Probleme zu vermeiden
const clockSkewBuffer = 5000; // 5 Sekunden
if (timeoutDuration > clockSkewBuffer) {
this.tokenExpirationTimer = setTimeout(() => { this.tokenExpirationTimer = setTimeout(() => {
console.warn('Sitzung proaktiv beendet, da das Token abgelaufen ist.'); console.warn('Sitzung proaktiv beendet, da das Token abgelaufen ist.');
this.logout(); this.logout();
// Hier könnte man eine Snackbar-Nachricht anzeigen
}, timeoutDuration); }, timeoutDuration);
} else { } else {
// Das gespeicherte Token ist bereits abgelaufen if (this.getToken()) {
this.clearSession(); this.logout();
}
} }
} catch (error) { } catch (error) {
console.error('Fehler beim Dekodieren des Tokens. Session wird bereinigt.', error); console.error('Fehler beim Dekodieren des Tokens. Session wird bereinigt.', error);
@@ -153,8 +146,7 @@ export class AuthService {
} }
private initTokenCheck(): void { private initTokenCheck(): void {
if (this.isBrowser()) { // Der StorageService ist bereits SSR-sicher, wir brauchen hier keine extra Prüfung.
this.startTokenExpirationTimer(); this.startTokenExpirationTimer();
} }
}
} }

View File

@@ -0,0 +1,87 @@
// /src/app/core/services/storage.service.ts
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
// Definiert die verfügbaren Speichertypen
export type StorageType = 'localStorage' | 'sessionStorage';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private platformId = inject(PLATFORM_ID);
/**
* Speichert einen Wert im angegebenen Web Storage.
* Serialisiert Objekte automatisch zu JSON.
* @param key Der Schlüssel, unter dem der Wert gespeichert wird.
* @param value Der zu speichernde Wert.
* @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'.
*/
setItem<T>(key: string, value: T, storageType: StorageType = 'localStorage'): void {
if (isPlatformBrowser(this.platformId)) {
try {
const storage = this.getStorage(storageType);
const serializedValue = JSON.stringify(value);
storage.setItem(key, serializedValue);
} catch (e) {
console.error(`Error saving to ${storageType} with key "${key}"`, e);
}
}
}
/**
* Ruft einen Wert aus dem angegebenen Web Storage ab.
* Deserialisiert JSON-Strings automatisch in Objekte.
* @param key Der Schlüssel des abzurufenden Wertes.
* @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'.
* @returns Der abgerufene Wert (typsicher) oder null, wenn der Schlüssel nicht existiert oder ein Fehler auftritt.
*/
getItem<T>(key: string, storageType: StorageType = 'localStorage'): T | null {
if (isPlatformBrowser(this.platformId)) {
try {
const storage = this.getStorage(storageType);
const serializedValue = storage.getItem(key);
if (serializedValue === null) {
return null;
}
return JSON.parse(serializedValue) as T;
} catch (e) {
console.error(`Error reading from ${storageType} with key "${key}"`, e);
return null;
}
}
return null;
}
/**
* Entfernt einen Wert aus dem angegebenen Web Storage.
* @param key Der Schlüssel des zu entfernenden Wertes.
* @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'.
*/
removeItem(key: string, storageType: StorageType = 'localStorage'): void {
if (isPlatformBrowser(this.platformId)) {
const storage = this.getStorage(storageType);
storage.removeItem(key);
}
}
/**
* Löscht alle Einträge im angegebenen Web Storage.
* @param storageType Der zu leerende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'.
*/
clear(storageType: StorageType = 'localStorage'): void {
if (isPlatformBrowser(this.platformId)) {
const storage = this.getStorage(storageType);
storage.clear();
}
}
/**
* Private Hilfsfunktion, um das korrekte Storage-Objekt zurückzugeben.
*/
private getStorage(storageType: StorageType): Storage {
return storageType === 'localStorage' ? window.localStorage : window.sessionStorage;
}
}

View File

@@ -66,6 +66,30 @@ export class DashboardPageComponent {
amount: '€87.00', amount: '€87.00',
status: 'info', // NEU: Sprechender Status status: 'info', // NEU: Sprechender Status
}, },
{
id: 'a2d4b',
user: { name: 'Max Mustermann', email: 'max@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=max' },
amount: '€129.99',
status: 'completed', // NEU: Sprechender Status
},
{
id: 'f8e9c',
user: { name: 'Erika Musterfrau', email: 'erika@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=erika' },
amount: '€49.50',
status: 'processing', // NEU: Sprechender Status
},
{
id: 'h1g3j',
user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' },
amount: '€87.00',
status: 'cancelled', // NEU: Sprechender Status
},
{
id: 'h1g3j',
user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' },
amount: '€87.00',
status: 'info', // NEU: Sprechender Status
},
]; ];
handleDeleteOrder(orderId: string): void { handleDeleteOrder(orderId: string): void {

View File

@@ -1,76 +1,51 @@
import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; // /src/app/core/components/default-layout/sidebar/sidebar.component.ts
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router'; // RouterLink und RouterLinkActive importieren
import { IconComponent } from '../../ui/icon/icon.component'; import { IconComponent } from '../../ui/icon/icon.component';
import { StorageService } from '../../../../core/services/storage.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
standalone: true, standalone: true,
imports: [CommonModule, IconComponent], imports: [CommonModule, IconComponent], // RouterLink und RouterLinkActive hier hinzufügen
templateUrl: './sidebar.component.html', templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css', styleUrl: './sidebar.component.css',
}) })
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit {
// 1. OnInit implementieren // --- Abhängigkeiten mit moderner inject()-Syntax ---
// Key für localStorage, genau wie beim Dark Mode private storageService = inject(StorageService);
private readonly sidebarCollapsedKey = 'app-sidebar-collapsed-setting';
// Dummy-Eigenschaft für die aktive Route private readonly sidebarCollapsedKey = 'app-sidebar-collapsed';
activeRoute = 'dashboard';
// Der Standardwert ist 'false' (aufgeklappt), wird aber sofort überschrieben
public isCollapsed = false; public isCollapsed = false;
// 2. PLATFORM_ID injizieren, um localStorage sicher zu verwenden activeRoute = 'dashboard';
constructor( constructor(private router: Router) {}
@Inject(PLATFORM_ID) private platformId: Object,
private router: Router
) {}
// 3. Beim Start der Komponente den gespeicherten Zustand laden
ngOnInit(): void { ngOnInit(): void {
this.loadCollapsedState(); this.loadCollapsedState();
} }
// Dummy-Methode
setActive(route: string): void { setActive(route: string): void {
this.activeRoute = route; this.activeRoute = route;
this.router.navigateByUrl('/shop/' + route); this.router.navigateByUrl('/shop/' + route);
} }
// 4. Die Umschalt-Methode aktualisieren, damit sie den neuen Zustand speichert
toggleSidebar(): void { toggleSidebar(): void {
// Zuerst den Zustand ändern
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
// Dann den neuen Zustand speichern
this.saveCollapsedState(); this.saveCollapsedState();
} }
// 5. Methode zum Laden des Zustands (kopiert vom Dark-Mode-Muster)
private loadCollapsedState(): void { private loadCollapsedState(): void {
if (isPlatformBrowser(this.platformId)) { // Der Service kümmert sich um die Browser-Prüfung und gibt boolean oder null zurück
try { this.isCollapsed =
const storedValue = localStorage.getItem(this.sidebarCollapsedKey); this.storageService.getItem<boolean>(this.sidebarCollapsedKey) ?? false;
// Setze den Zustand der Komponente basierend auf dem gespeicherten Wert
this.isCollapsed = storedValue === 'true';
} catch (e) {
console.error('Could not access localStorage for sidebar state:', e);
}
}
} }
// 6. Methode zum Speichern des Zustands (kopiert vom Dark-Mode-Muster)
private saveCollapsedState(): void { private saveCollapsedState(): void {
if (isPlatformBrowser(this.platformId)) { // Der Service kümmert sich um die Serialisierung des booleans
try { this.storageService.setItem(this.sidebarCollapsedKey, this.isCollapsed);
localStorage.setItem(
this.sidebarCollapsedKey,
String(this.isCollapsed)
);
} catch (e) {
console.error('Could not write to localStorage for sidebar state:', e);
}
}
} }
} }