feat: implement authentication with JWT and user management

This commit is contained in:
Constantin Simonis 2025-05-07 13:42:04 +02:00
commit 35d8fbaea0
No known key found for this signature in database
GPG key ID: 3878FF77C24AF4D2
42 changed files with 989 additions and 397 deletions

View file

@ -1,208 +1,96 @@
import { inject, Injectable } from '@angular/core';
import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc';
import { UserService } from './user.service';
import { User } from '../model/User';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router } from '@angular/router';
import { LoginRequest } from '../model/auth/LoginRequest';
import { RegisterRequest } from '../model/auth/RegisterRequest';
import { AuthResponse } from '../model/auth/AuthResponse';
import { User } from '../model/User';
import { environment } from '../../environments/environment';
import { catchError, from, of } from 'rxjs';
const TOKEN_KEY = 'auth-token';
const USER_KEY = 'auth-user';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly authConfig: AuthConfig = {
issuer: 'https://oauth.simonis.lol/application/o/casino-dev/',
clientId: environment.OAUTH_CLIENT_ID,
dummyClientSecret: environment.OAUTH_CLIENT_SECRET,
scope: `openid email profile ${environment.OAUTH_CLIENT_ID}`,
responseType: 'code',
redirectUri: window.location.origin + '/auth/callback',
postLogoutRedirectUri: '',
redirectUriAsPostLogoutRedirectUriFallback: false,
oidc: true,
requestAccessToken: true,
tokenEndpoint: 'https://oauth.simonis.lol/application/o/token/',
userinfoEndpoint: 'https://oauth.simonis.lol/application/o/userinfo/',
strictDiscoveryDocumentValidation: false,
skipIssuerCheck: true,
disableAtHashCheck: true,
requireHttps: false,
showDebugInformation: false,
sessionChecksEnabled: false,
};
private userService: UserService = inject(UserService);
private oauthService: OAuthService = inject(OAuthService);
private router: Router = inject(Router);
private user: User | null = null;
constructor() {
this.oauthService.configure(this.authConfig);
this.setupEventHandling();
const hasAuthParams =
window.location.search.includes('code=') ||
window.location.search.includes('token=') ||
window.location.search.includes('id_token=');
if (hasAuthParams) {
this.processCodeFlow();
} else {
this.checkExistingSession();
private authUrl = `${environment.apiUrl}/api/auth`;
private userUrl = `${environment.apiUrl}/api/users`;
private currentUserSubject: BehaviorSubject<User | null>;
public currentUser: Observable<User | null>;
constructor(private http: HttpClient, private router: Router) {
this.currentUserSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
this.currentUser = this.currentUserSubject.asObservable();
// Check if token exists and load user data
if (this.getToken()) {
this.loadCurrentUser();
}
}
private processCodeFlow() {
this.oauthService
.tryLogin({
onTokenReceived: () => {
this.handleSuccessfulLogin();
},
})
.catch((err) => {
console.error('Error processing code flow:', err);
});
public get currentUserValue(): User | null {
return this.currentUserSubject.value;
}
private checkExistingSession() {
this.oauthService
.loadDiscoveryDocumentAndTryLogin()
.then((isLoggedIn) => {
if (isLoggedIn && !this.user) {
this.handleSuccessfulLogin();
}
})
.catch((err) => {
console.error('Error during initial login attempt:', err);
});
}
private setupEventHandling() {
this.oauthService.events.subscribe((event: OAuthEvent) => {
if (event.type === 'token_received') {
this.handleSuccessfulLogin();
}
});
}
private handleSuccessfulLogin() {
const claims = this.oauthService.getIdentityClaims();
if (claims && (claims['sub'] || claims['email'])) {
this.processUserProfile(claims);
return;
}
try {
from(this.oauthService.loadUserProfile())
.pipe(
catchError((error) => {
console.error('Error loading user profile:', error);
if (this.oauthService.hasValidAccessToken()) {
this.oauthService.getAccessToken();
const minimalProfile = {
sub: 'user-' + Math.random().toString(36).substring(2, 10),
preferred_username: 'user' + Date.now(),
};
return of({ info: minimalProfile });
}
return of(null);
})
)
.subscribe((profile) => {
if (profile) {
this.processUserProfile(profile);
} else {
this.router.navigate(['/']);
}
});
} catch (err) {
console.error('Exception in handleSuccessfulLogin:', err);
if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
}
}
private processUserProfile(profile: unknown) {
this.fromUserProfile(profile as Record<string, unknown>).subscribe({
next: (user) => {
this.user = user;
this.router.navigate(['home']);
},
error: (err) => {
console.error('Error creating/retrieving user:', err);
if (this.oauthService.hasValidAccessToken()) {
this.router.navigate(['/home']);
} else {
this.router.navigate(['/']);
}
},
});
}
login() {
try {
this.oauthService
.loadDiscoveryDocument()
.then(() => {
this.oauthService.initLoginFlow();
login(loginRequest: LoginRequest): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.authUrl}/login`, loginRequest)
.pipe(
tap(response => {
this.setToken(response.token);
this.loadCurrentUser();
})
.catch((err) => {
console.error('Error loading discovery document:', err);
this.oauthService.initLoginFlow();
});
} catch (err) {
console.error('Exception in login:', err);
const redirectUri = this.authConfig.redirectUri || window.location.origin + '/auth/callback';
const scope = this.authConfig.scope || 'openid email profile';
const authUrl = `${this.authConfig.issuer}authorize?client_id=${this.authConfig.clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
window.location.href = authUrl;
}
);
}
logout() {
try {
this.user = null;
this.oauthService.logOut(true);
if (window.location.href.includes('id_token') || window.location.href.includes('logout')) {
window.location.href = window.location.origin;
}
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('id_token');
sessionStorage.removeItem('refresh_token');
this.router.navigate(['/']);
} catch (err) {
console.error('Exception in logout:', err);
localStorage.clear();
sessionStorage.clear();
this.router.navigate(['/']);
}
register(registerRequest: RegisterRequest): Observable<User> {
return this.http.post<User>(`${this.authUrl}/register`, registerRequest);
}
isLoggedIn() {
return this.oauthService.hasValidAccessToken();
logout(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
this.currentUserSubject.next(null);
this.router.navigate(['/']);
}
private fromUserProfile(profile: Record<string, unknown>) {
return this.userService.getOrCreateUser(profile);
isLoggedIn(): boolean {
return !!this.getToken();
}
getAccessToken() {
return this.oauthService.getAccessToken();
getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
getUser() {
return this.user;
private setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
private setUser(user: User): void {
localStorage.setItem(USER_KEY, JSON.stringify(user));
this.currentUserSubject.next(user);
}
private getUserFromStorage(): User | null {
const user = localStorage.getItem(USER_KEY);
return user ? JSON.parse(user) : null;
}
private loadCurrentUser(): void {
this.http.get<User>(`${this.userUrl}/me`)
.subscribe({
next: (user) => {
this.setUser(user);
},
error: () => {
this.logout();
}
});
}
getUser(): User | null {
return this.currentUserValue;
}
}