feat: add GitHub OAuth2 authentication support

This commit is contained in:
Constantin Simonis 2025-05-21 10:33:30 +02:00
commit cc1979a068
No known key found for this signature in database
GPG key ID: 3878FF77C24AF4D2
24 changed files with 845 additions and 8 deletions

View file

@ -33,6 +33,13 @@ export const routes: Routes = [
(m) => m.RecoverPasswordComponent
),
},
{
path: 'oauth2/callback/github',
loadComponent: () =>
import('./feature/auth/oauth2/oauth2-callback.component').then(
(m) => m.OAuth2CallbackComponent
),
},
{
path: 'game/blackjack',
loadComponent: () => import('./feature/game/blackjack/blackjack.component'),

View file

@ -82,8 +82,26 @@
</button>
</div>
</form>
<div class="my-4 flex items-center">
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
<span class="px-3 text-xs text-text-secondary">ODER</span>
<div class="flex-grow h-px bg-deep-blue-light/30"></div>
</div>
<div class="mb-4">
<button
(click)="loginWithGithub()"
class="w-full py-2.5 px-4 rounded flex items-center justify-center bg-gray-800 hover:bg-gray-700 text-white transition-colors"
>
<svg class="h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
Mit GitHub anmelden
</button>
</div>
<div class="mt-6 text-center">
<div class="text-center">
<p class="text-sm text-text-secondary">
Passwort vergessen?
<button

View file

@ -4,6 +4,7 @@ import { Router } from '@angular/router';
import { LoginRequest } from '../../../model/auth/LoginRequest';
import { AuthService } from '@service/auth.service';
import { CommonModule } from '@angular/common';
import { environment } from '@environments/environment';
@Component({
selector: 'app-login',
@ -65,6 +66,11 @@ export class LoginComponent {
});
}
loginWithGithub(): void {
this.isLoading.set(true);
window.location.href = `${environment.apiUrl}/oauth2/github/authorize`;
}
switchToForgotPassword() {
this.forgotPassword.emit();
}

View file

@ -0,0 +1,62 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '@service/auth.service';
@Component({
selector: 'app-oauth2-callback',
standalone: true,
imports: [CommonModule],
template: `
<div class="min-h-screen bg-deep-blue flex items-center justify-center">
<div class="text-center">
<h2 class="text-2xl font-bold text-white mb-4">Finishing authentication...</h2>
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald mx-auto"></div>
<p *ngIf="error" class="mt-4 text-accent-red">{{ error }}</p>
</div>
</div>
`,
})
export class OAuth2CallbackComponent implements OnInit {
error: string | null = null;
constructor(
private route: ActivatedRoute,
private router: Router,
private authService: AuthService
) {}
ngOnInit(): void {
// Check for code in URL params
this.route.queryParams.subscribe(params => {
const code = params['code'];
if (code) {
// Exchange GitHub code for a JWT token
this.authService.githubAuth(code).subscribe({
next: () => {
// Redirect to home after successful authentication
this.router.navigate(['/home']);
},
error: (err) => {
console.error('GitHub authentication error:', err);
this.error = err.error?.message || "Authentication failed. Please try again.";
console.log('Error details:', err);
// Redirect back to landing page after showing error
setTimeout(() => {
this.router.navigate(['/']);
}, 3000);
}
});
} else {
this.error = "Authentication failed. No authorization code received.";
// Redirect back to landing page after showing error
setTimeout(() => {
this.router.navigate(['/']);
}, 3000);
}
});
}
}

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router } from '@angular/router';
import { Router, ActivatedRoute } from '@angular/router';
import { LoginRequest } from '../model/auth/LoginRequest';
import { RegisterRequest } from '../model/auth/RegisterRequest';
import { AuthResponse } from '../model/auth/AuthResponse';
@ -17,20 +17,41 @@ const USER_KEY = 'user';
export class AuthService {
private authUrl = `${environment.apiUrl}/auth`;
private userUrl = `${environment.apiUrl}/users`;
private oauthUrl = `${environment.apiUrl}/oauth2`;
userSubject: BehaviorSubject<User | null>;
constructor(
private http: HttpClient,
private router: Router
private router: Router,
private route: ActivatedRoute
) {
this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
// Check for token in URL (OAuth callback) on initialization
this.route.queryParams.subscribe(params => {
const token = params['token'];
if (token) {
this.handleOAuthCallback(token);
}
});
if (this.getToken()) {
this.loadCurrentUser();
}
}
private handleOAuthCallback(token: string): void {
this.setToken(token);
this.loadCurrentUser();
// Clean up the URL by removing the token
this.router.navigate([], {
relativeTo: this.route,
queryParams: {},
replaceUrl: true
});
}
public get currentUserValue(): User | null {
return this.userSubject.value;
}
@ -48,6 +69,16 @@ export class AuthService {
return this.http.post<User>(`${this.authUrl}/register`, registerRequest);
}
githubAuth(code: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.oauthUrl}/github/callback`, { code }).pipe(
tap((response) => {
console.log(response.token);
this.setToken(response.token);
this.loadCurrentUser();
})
);
}
logout(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);