Files
listify/listify-client/src/app/auth/auth.service.ts
Bastian Wagner f45583f3ea SSO implementiert
2026-07-14 17:32:48 +02:00

142 lines
4.3 KiB
TypeScript

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, computed, inject, signal } from '@angular/core';
import { Observable, finalize, shareReplay, tap, throwError } from 'rxjs';
import {
AuthTokenResponse,
LoginRequest,
PublicUser,
PublicUserSearchResult,
RegisterRequest,
RegisterResponse,
TaskDigestPreference,
} from './auth.models';
const ACCESS_TOKEN_KEY = 'listify.accessToken';
const REFRESH_TOKEN_KEY = 'listify.refreshToken';
const USER_KEY = 'listify.user';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly apiUrl = '/api/auth';
private readonly userSignal = signal<PublicUser | null>(this.readStoredUser());
private refreshRequest$: Observable<AuthTokenResponse> | null = null;
readonly user = this.userSignal.asReadonly();
readonly isAuthenticated = computed(() => Boolean(this.userSignal()));
login(credentials: LoginRequest): Observable<AuthTokenResponse> {
return this.http
.post<AuthTokenResponse>(`${this.apiUrl}/login`, credentials)
.pipe(tap((response) => this.storeSession(response)));
}
startSsoLogin(): void {
if (typeof window !== 'undefined') {
window.location.href = `${this.apiUrl}/sso/login`;
}
}
completeSsoLogin(response: AuthTokenResponse): void {
this.storeSession(response);
}
exchangeSsoCode(code: string, state: string): Observable<AuthTokenResponse> {
return this.http
.post<AuthTokenResponse>(`${this.apiUrl}/sso/exchange`, { code, state })
.pipe(tap((response) => this.storeSession(response)));
}
register(data: RegisterRequest): Observable<RegisterResponse> {
return this.http.post<RegisterResponse>(`${this.apiUrl}/register`, data);
}
loadCurrentUser(): Observable<PublicUser> {
return this.http.get<PublicUser>(`${this.apiUrl}/me`).pipe(tap((user) => this.storeUser(user)));
}
searchUsers(query: string): Observable<PublicUserSearchResult[]> {
const params = new HttpParams().set('q', query);
return this.http.get<PublicUserSearchResult[]>(`${this.apiUrl}/users/search`, {
params,
});
}
updateOnboardingCompleted(completed: boolean): Observable<PublicUser> {
return this.http
.patch<PublicUser>(`${this.apiUrl}/me/onboarding`, { completed })
.pipe(tap((user) => this.storeUser(user)));
}
updateTaskDigestPreference(preference: TaskDigestPreference): Observable<PublicUser> {
return this.http
.patch<PublicUser>(`${this.apiUrl}/me/task-digest`, { preference })
.pipe(tap((user) => this.storeUser(user)));
}
accessToken(): string | null {
return this.storage?.getItem(ACCESS_TOKEN_KEY) ?? null;
}
refreshToken(): string | null {
return this.storage?.getItem(REFRESH_TOKEN_KEY) ?? null;
}
refreshSession(): Observable<AuthTokenResponse> {
const refreshToken = this.refreshToken();
if (!refreshToken) {
return throwError(() => new Error('Refresh token is missing.'));
}
this.refreshRequest$ ??= this.http
.post<AuthTokenResponse>(`${this.apiUrl}/refresh`, { refreshToken })
.pipe(
tap((response) => this.storeSession(response)),
finalize(() => {
this.refreshRequest$ = null;
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
return this.refreshRequest$;
}
logout(): void {
this.storage?.removeItem(ACCESS_TOKEN_KEY);
this.storage?.removeItem(REFRESH_TOKEN_KEY);
this.storage?.removeItem(USER_KEY);
this.userSignal.set(null);
}
private storeSession(response: AuthTokenResponse): void {
this.storage?.setItem(ACCESS_TOKEN_KEY, response.accessToken);
this.storage?.setItem(REFRESH_TOKEN_KEY, response.refreshToken);
this.storeUser(response.user);
}
private storeUser(user: PublicUser): void {
this.storage?.setItem(USER_KEY, JSON.stringify(user));
this.userSignal.set(user);
}
private readStoredUser(): PublicUser | null {
const rawUser = this.storage?.getItem(USER_KEY);
if (!rawUser) {
return null;
}
try {
return JSON.parse(rawUser) as PublicUser;
} catch {
this.storage?.removeItem(USER_KEY);
return null;
}
}
private get storage(): Storage | null {
return typeof window === 'undefined' ? null : window.localStorage;
}
}