diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java index b7d4365..66633e0 100644 --- a/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/OAuth2UserInfoFactory.java @@ -1,6 +1,8 @@ package de.szut.casino.security.oauth2; import de.szut.casino.exceptionHandling.exceptions.OAuth2AuthenticationProcessingException; +import de.szut.casino.security.oauth2.github.GitHubOAuth2UserInfo; +import de.szut.casino.security.oauth2.google.GoogleOAuth2UserInfo; import de.szut.casino.user.AuthProvider; import java.util.Map; @@ -10,6 +12,8 @@ public class OAuth2UserInfoFactory { public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.toString())) { return new GitHubOAuth2UserInfo(attributes); + } else if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.toString())) { + return new GoogleOAuth2UserInfo(attributes); } else { throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); } diff --git a/backend/src/main/java/de/szut/casino/security/GitHubController.java b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubController.java similarity index 94% rename from backend/src/main/java/de/szut/casino/security/GitHubController.java rename to backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubController.java index b45fb62..9abf96c 100644 --- a/backend/src/main/java/de/szut/casino/security/GitHubController.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubController.java @@ -1,7 +1,6 @@ -package de.szut.casino.security; +package de.szut.casino.security.oauth2.github; import de.szut.casino.security.dto.AuthResponseDto; -import de.szut.casino.security.dto.GithubCallbackDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubOAuth2UserInfo.java similarity index 82% rename from backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java rename to backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubOAuth2UserInfo.java index f98390e..c764fc7 100644 --- a/backend/src/main/java/de/szut/casino/security/oauth2/GitHubOAuth2UserInfo.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubOAuth2UserInfo.java @@ -1,4 +1,6 @@ -package de.szut.casino.security.oauth2; +package de.szut.casino.security.oauth2.github; + +import de.szut.casino.security.oauth2.OAuth2UserInfo; import java.util.Map; diff --git a/backend/src/main/java/de/szut/casino/security/GitHubService.java b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubService.java similarity index 98% rename from backend/src/main/java/de/szut/casino/security/GitHubService.java rename to backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubService.java index 1051498..4a8df24 100644 --- a/backend/src/main/java/de/szut/casino/security/GitHubService.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/github/GitHubService.java @@ -1,12 +1,10 @@ -package de.szut.casino.security; +package de.szut.casino.security.oauth2.github; import de.szut.casino.security.dto.AuthResponseDto; import de.szut.casino.security.jwt.JwtUtils; import de.szut.casino.user.AuthProvider; import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; diff --git a/backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java b/backend/src/main/java/de/szut/casino/security/oauth2/github/GithubCallbackDto.java similarity index 65% rename from backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java rename to backend/src/main/java/de/szut/casino/security/oauth2/github/GithubCallbackDto.java index 07619aa..620a708 100644 --- a/backend/src/main/java/de/szut/casino/security/dto/GithubCallbackDto.java +++ b/backend/src/main/java/de/szut/casino/security/oauth2/github/GithubCallbackDto.java @@ -1,4 +1,4 @@ -package de.szut.casino.security.dto; +package de.szut.casino.security.oauth2.github; import lombok.Data; diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleController.java b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleController.java new file mode 100644 index 0000000..30907f8 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleController.java @@ -0,0 +1,49 @@ +package de.szut.casino.security.oauth2.google; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.oauth2.github.GithubCallbackDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +@RestController +@RequestMapping("/oauth2/google") +public class GoogleController { + private static final Logger logger = LoggerFactory.getLogger(GoogleController.class); + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.provider.google.authorization-uri}") + private String authorizationUri; + + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + @Autowired + private GoogleService googleService; + + @GetMapping("/authorize") + public RedirectView authorizeGoogle() { + logger.info("Redirecting to Google for authorization"); + + String authUrl = authorizationUri + + "?client_id=" + clientId + + "&redirect_uri=" + redirectUri + + "&response_type=code" + + "&scope=email profile"; + + return new RedirectView(authUrl); + } + + @PostMapping("/callback") + public ResponseEntity googleCallback(@RequestBody GithubCallbackDto callbackDto) { + String code = callbackDto.getCode(); + AuthResponseDto response = googleService.processGoogleCode(code); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleOAuth2UserInfo.java b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleOAuth2UserInfo.java new file mode 100644 index 0000000..819a9b3 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleOAuth2UserInfo.java @@ -0,0 +1,27 @@ +package de.szut.casino.security.oauth2.google; + +import de.szut.casino.security.oauth2.OAuth2UserInfo; + +import java.util.Map; + +public class GoogleOAuth2UserInfo extends OAuth2UserInfo { + + public GoogleOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return (String) attributes.get("sub"); + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } +} diff --git a/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleService.java b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleService.java new file mode 100644 index 0000000..fd30303 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/security/oauth2/google/GoogleService.java @@ -0,0 +1,164 @@ +package de.szut.casino.security.oauth2.google; + +import de.szut.casino.security.dto.AuthResponseDto; +import de.szut.casino.security.jwt.JwtUtils; +import de.szut.casino.user.AuthProvider; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.*; + +@Service +public class GoogleService { + private static final Logger logger = LoggerFactory.getLogger(GoogleService.class); + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.google.client-secret}") + private String clientSecret; + + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + @Value("${spring.security.oauth2.client.provider.google.token-uri}") + private String tokenUri; + + @Value("${spring.security.oauth2.client.provider.google.user-info-uri}") + private String userInfoUri; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private PasswordEncoder oauth2PasswordEncoder; + + public AuthResponseDto processGoogleCode(String code) { + try { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders tokenHeaders = new HttpHeaders(); + tokenHeaders.set("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap tokenRequestBody = new LinkedMultiValueMap<>(); + tokenRequestBody.add("client_id", clientId); + tokenRequestBody.add("client_secret", clientSecret); + tokenRequestBody.add("code", code); + tokenRequestBody.add("redirect_uri", redirectUri); + tokenRequestBody.add("grant_type", "authorization_code"); + + HttpEntity> tokenRequestEntity = new HttpEntity<>(tokenRequestBody, tokenHeaders); + + ResponseEntity tokenResponse = restTemplate.exchange( + tokenUri, + HttpMethod.POST, + tokenRequestEntity, + Map.class + ); + + Map tokenResponseBody = tokenResponse.getBody(); + + if (tokenResponseBody == null || tokenResponseBody.containsKey("error")) { + String error = tokenResponseBody != null ? (String) tokenResponseBody.get("error") : "Unknown error"; + throw new RuntimeException("Google OAuth error: " + error); + } + + String accessToken = (String) tokenResponseBody.get("access_token"); + if (accessToken == null || accessToken.isEmpty()) { + throw new RuntimeException("Failed to receive access token from Google"); + } + + HttpHeaders userInfoHeaders = new HttpHeaders(); + userInfoHeaders.set("Authorization", "Bearer " + accessToken); + + HttpEntity userInfoRequestEntity = new HttpEntity<>(null, userInfoHeaders); + + ResponseEntity userResponse = restTemplate.exchange( + userInfoUri, + HttpMethod.GET, + userInfoRequestEntity, + Map.class + ); + + Map userAttributes = userResponse.getBody(); + if (userAttributes == null) { + throw new RuntimeException("Failed to fetch user data from Google"); + } + + String googleId = (String) userAttributes.get("sub"); + String email = (String) userAttributes.get("email"); + String name = (String) userAttributes.get("name"); + Boolean emailVerified = (Boolean) userAttributes.getOrDefault("email_verified", false); + + if (email == null) { + throw new RuntimeException("Google account does not have an email"); + } + + String username = name != null ? name.replaceAll("\\s+", "") : email.split("@")[0]; + + Optional userOptional = userRepository.findByProviderId(googleId); + UserEntity user; + + if (userOptional.isPresent()) { + user = userOptional.get(); + } else { + userOptional = userRepository.findByEmail(email); + + if (userOptional.isPresent()) { + user = userOptional.get(); + user.setProvider(AuthProvider.GOOGLE); + user.setProviderId(googleId); + } else { + user = new UserEntity(); + user.setEmail(email); + user.setUsername(username); + user.setProvider(AuthProvider.GOOGLE); + user.setProviderId(googleId); + user.setEmailVerified(emailVerified); + + user.setBalance(new BigDecimal("100.00")); + } + } + + String randomPassword = UUID.randomUUID().toString(); + user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); + + userRepository.save(user); + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword) + ); + + String token = jwtUtils.generateToken(authentication); + + return new AuthResponseDto(token); + + } catch (Exception e) { + logger.error("Failed to process Google authentication", e); + throw new RuntimeException("Failed to process Google authentication", e); + } + } +} diff --git a/backend/src/main/java/de/szut/casino/security/service/EmailService.java b/backend/src/main/java/de/szut/casino/security/service/EmailService.java index f276a3c..83d65e0 100644 --- a/backend/src/main/java/de/szut/casino/security/service/EmailService.java +++ b/backend/src/main/java/de/szut/casino/security/service/EmailService.java @@ -28,6 +28,7 @@ public class EmailService { this.mailConfig = mailConfig; this.mailSender.setHost(mailConfig.host); this.mailSender.setPort(mailConfig.port); + this.mailSender.setProtocol(mailConfig.protocol); if (mailConfig.authenticationEnabled) { this.mailSender.setUsername(mailConfig.username); this.mailSender.setPassword(mailConfig.password); diff --git a/backend/src/main/java/de/szut/casino/security/service/MailConfig.java b/backend/src/main/java/de/szut/casino/security/service/MailConfig.java index 56c7250..8a516fd 100644 --- a/backend/src/main/java/de/szut/casino/security/service/MailConfig.java +++ b/backend/src/main/java/de/szut/casino/security/service/MailConfig.java @@ -22,4 +22,7 @@ public class MailConfig { @Value("${app.mail.from-address}") public String fromAddress; + + @Value("${app.mail.protocol}") + public String protocol; } diff --git a/backend/src/main/java/de/szut/casino/user/AuthProvider.java b/backend/src/main/java/de/szut/casino/user/AuthProvider.java index 2216da7..c26b45c 100644 --- a/backend/src/main/java/de/szut/casino/user/AuthProvider.java +++ b/backend/src/main/java/de/szut/casino/user/AuthProvider.java @@ -2,5 +2,6 @@ package de.szut.casino.user; public enum AuthProvider { LOCAL, - GITHUB + GITHUB, + GOOGLE } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e583c50..7b761a7 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -14,6 +14,7 @@ app.mail.port=${MAIL_PORT:1025} app.mail.username=${MAIL_USER:null} app.mail.password=${MAIL_PASS:null} app.mail.from-address=${MAIL_FROM:casino@localhost} +app.mail.protocol=${MAIL_PROTOCOL:smtp} spring.application.name=casino @@ -41,3 +42,13 @@ spring.security.oauth2.client.provider.github.user-name-attribute=login # OAuth Success and Failure URLs app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback +# Google OAuth2 Configuration +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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} +spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google +spring.security.oauth2.client.registration.google.scope=email,profile +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index a9ce6d0..7499296 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,12 +1,12 @@ import { Component, HostListener, inject, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { NavbarComponent } from './shared/components/navbar/navbar.component'; -import { FooterComponent } from './shared/components/footer/footer.component'; +import { NavbarComponent } from '@shared/components/navbar/navbar.component'; +import { FooterComponent } from '@shared/components/footer/footer.component'; import { LoginComponent } from './feature/auth/login/login.component'; import { RegisterComponent } from './feature/auth/register/register.component'; -import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component'; -import { PlaySoundDirective } from './shared/directives/play-sound.directive'; -import { SoundInitializerService } from './shared/services/sound-initializer.service'; +import RecoverPasswordComponent from './feature/auth/recover-password/recover-password.component'; +import { PlaySoundDirective } from '@shared/directives/play-sound.directive'; +import { SoundInitializerService } from '@shared/services/sound-initializer.service'; @Component({ selector: 'app-root', diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 6e9f9b7..88f4fe1 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -14,61 +14,68 @@ export const routes: Routes = [ }, { path: 'verify', - loadComponent: () => - import('./feature/auth/verify-email/verify-email.component').then( - (m) => m.VerifyEmailComponent - ), + loadComponent: () => import('./feature/auth/verify-email/verify-email.component'), }, { path: 'recover-password', - loadComponent: () => - import('./feature/auth/recover-password/recover-password.component').then( - (m) => m.RecoverPasswordComponent - ), + loadComponent: () => import('./feature/auth/recover-password/recover-password.component'), }, { path: 'reset-password', - loadComponent: () => - import('./feature/auth/recover-password/recover-password.component').then( - (m) => m.RecoverPasswordComponent - ), + loadComponent: () => import('./feature/auth/recover-password/recover-password.component'), }, { - path: 'oauth2/callback/github', - loadComponent: () => - import('./feature/auth/oauth2/oauth2-callback.component').then( - (m) => m.OAuth2CallbackComponent - ), + path: 'oauth2/callback', + children: [ + { + path: 'github', + loadComponent: () => import('./feature/auth/oauth2/oauth2-callback.component'), + data: { provider: 'github' }, + }, + { + path: 'google', + loadComponent: () => import('./feature/auth/oauth2/oauth2-callback.component'), + data: { provider: 'google' }, + }, + ], }, { - path: 'game/blackjack', - loadComponent: () => import('./feature/game/blackjack/blackjack.component'), - canActivate: [authGuard], - }, - { - path: 'game/coinflip', - loadComponent: () => import('./feature/game/coinflip/coinflip.component'), - canActivate: [authGuard], - }, - { - path: 'game/slots', - loadComponent: () => import('./feature/game/slots/slots.component'), - canActivate: [authGuard], - }, - { - path: 'game/lootboxes', - loadComponent: () => - import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'), - canActivate: [authGuard], - }, - { - path: 'game/lootboxes/open/:id', - loadComponent: () => import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'), - canActivate: [authGuard], - }, - { - path: 'game/dice', - loadComponent: () => import('./feature/game/dice/dice.component').then((m) => m.DiceComponent), - canActivate: [authGuard], + path: 'game', + children: [ + { + path: 'blackjack', + loadComponent: () => import('./feature/game/blackjack/blackjack.component'), + canActivate: [authGuard], + }, + { + path: 'coinflip', + loadComponent: () => import('./feature/game/coinflip/coinflip.component'), + canActivate: [authGuard], + }, + { + path: 'slots', + loadComponent: () => import('./feature/game/slots/slots.component'), + canActivate: [authGuard], + }, + { + path: 'lootboxes', + loadComponent: () => + import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'), + canActivate: [authGuard], + children: [ + { + path: 'open/:id', + loadComponent: () => + import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'), + canActivate: [authGuard], + }, + ], + }, + { + path: 'dice', + loadComponent: () => import('./feature/game/dice/dice.component'), + canActivate: [authGuard], + }, + ], }, ]; diff --git a/frontend/src/app/feature/auth/login/login.component.html b/frontend/src/app/feature/auth/login/login.component.html index 14a7d1e..044742c 100644 --- a/frontend/src/app/feature/auth/login/login.component.html +++ b/frontend/src/app/feature/auth/login/login.component.html @@ -89,7 +89,7 @@
-
+
+ +
diff --git a/frontend/src/app/feature/auth/login/login.component.ts b/frontend/src/app/feature/auth/login/login.component.ts index 2b78f29..09c1cdf 100644 --- a/frontend/src/app/feature/auth/login/login.component.ts +++ b/frontend/src/app/feature/auth/login/login.component.ts @@ -71,6 +71,11 @@ export class LoginComponent { window.location.href = `${environment.apiUrl}/oauth2/github/authorize`; } + loginWithGoogle(): void { + this.isLoading.set(true); + window.location.href = `${environment.apiUrl}/oauth2/google/authorize`; + } + switchToForgotPassword() { this.forgotPassword.emit(); } diff --git a/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts b/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts index f905f75..9c4bcf3 100644 --- a/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts +++ b/frontend/src/app/feature/auth/oauth2/oauth2-callback.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, computed, inject, OnInit, Signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; -import { AuthService } from '@service/auth.service'; +import { Oauth2Service } from './oauth2.service'; @Component({ selector: 'app-oauth2-callback', @@ -10,51 +10,34 @@ import { AuthService } from '@service/auth.service'; template: `
-

Finishing authentication...

+

Authentifizierung...

-

{{ error }}

+

{{ error() }}

`, }) -export class OAuth2CallbackComponent implements OnInit { - error: string | null = null; +export default class OAuth2CallbackComponent implements OnInit { + error: Signal = computed(() => this.oauthService.error()); - constructor( - private route: ActivatedRoute, - private router: Router, - private authService: AuthService - ) {} + private route: ActivatedRoute = inject(ActivatedRoute); + private router: Router = inject(Router); + private oauthService: Oauth2Service = inject(Oauth2Service); ngOnInit(): void { - // Check for code in URL params this.route.queryParams.subscribe((params) => { const code = params['code']; + const provider = this.route.snapshot.data['provider'] || 'github'; 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); - }, - }); + this.oauthService.oauth(provider, code); } else { - this.error = 'Authentication failed. No authorization code received.'; + this.oauthService.error.set( + 'Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.' + ); - // Redirect back to landing page after showing error setTimeout(() => { this.router.navigate(['/']); }, 3000); diff --git a/frontend/src/app/feature/auth/oauth2/oauth2.service.ts b/frontend/src/app/feature/auth/oauth2/oauth2.service.ts new file mode 100644 index 0000000..79ad6d9 --- /dev/null +++ b/frontend/src/app/feature/auth/oauth2/oauth2.service.ts @@ -0,0 +1,36 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '@service/auth.service'; + +@Injectable({ + providedIn: 'root', +}) +export class Oauth2Service { + private router: Router = inject(Router); + private authService: AuthService = inject(AuthService); + private _error = signal(''); + + oauth(provider: string, code: string) { + const oauth$ = + provider === 'github' ? this.authService.githubAuth(code) : this.authService.googleAuth(code); + + oauth$.subscribe({ + next: () => { + this.router.navigate(['/home']); + }, + error: (err) => { + this._error.set( + err.error?.message || 'Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.' + ); + + setTimeout(() => { + this.router.navigate(['/']); + }, 3000); + }, + }); + } + + public get error() { + return this._error; + } +} diff --git a/frontend/src/app/feature/auth/recover-password/recover-password.component.ts b/frontend/src/app/feature/auth/recover-password/recover-password.component.ts index 905f200..89f5d96 100644 --- a/frontend/src/app/feature/auth/recover-password/recover-password.component.ts +++ b/frontend/src/app/feature/auth/recover-password/recover-password.component.ts @@ -10,7 +10,7 @@ import { AuthService } from '@service/auth.service'; imports: [CommonModule, ReactiveFormsModule, RouterModule], templateUrl: './recover-password.component.html', }) -export class RecoverPasswordComponent implements OnInit { +export default class RecoverPasswordComponent implements OnInit { emailForm: FormGroup; resetPasswordForm: FormGroup; errorMessage = signal(''); diff --git a/frontend/src/app/feature/auth/verify-email/verify-email.component.ts b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts index 6e04877..54f4c2f 100644 --- a/frontend/src/app/feature/auth/verify-email/verify-email.component.ts +++ b/frontend/src/app/feature/auth/verify-email/verify-email.component.ts @@ -7,7 +7,7 @@ import { AuthService } from '@service/auth.service'; imports: [], templateUrl: './verify-email.component.html', }) -export class VerifyEmailComponent implements OnInit { +export default class VerifyEmailComponent implements OnInit { route: ActivatedRoute = inject(ActivatedRoute); router: Router = inject(Router); authService: AuthService = inject(AuthService); diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.html b/frontend/src/app/feature/game/coinflip/coinflip.component.html index 671bc31..8dd6be8 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.html +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.html @@ -5,13 +5,11 @@ @if (gameResult()) {

- {{ gameResult()?.isWin ? 'You Won!' : 'You Lost' }} + {{ gameResult()?.isWin ? 'Du hast gewonnen!' : 'Du hast verloren' }}

- Coin landed on: - {{ - gameResult()?.coinSide === 'HEAD' ? 'HEAD' : 'TAILS' - }} + Münze zeigt: + {{ gameResult()?.coinSide === 'HEAD' ? 'KOPF' : 'ZAHL' }}

@if (gameResult()?.isWin) {

@@ -35,7 +33,7 @@

-
HEAD
+
KOPF
@@ -43,7 +41,7 @@ class="back coin-side bg-gray-700 flex items-center justify-center text-2xl font-bold text-white" > - TAILS + ZAHL
@@ -56,7 +54,7 @@ class="button-primary py-3 px-6 relative text-lg" [class.opacity-50]="gameInProgress()" > - Bet TAILS + Auf ZAHL setzen @@ -72,11 +70,11 @@
-

Game Information

+

Spielinformationen

- Current Bet: + Aktueller Einsatz: @@ -84,7 +82,7 @@
- Your Balance: + Dein Guthaben: {{ balance() | currency: 'EUR' }} @@ -103,9 +101,9 @@
- + Cannot exceed balanceDarf Guthaben nicht überschreiten
-

How to Play

+

Spielregeln

    -
  • • Choose your bet amount
  • -
  • • Select Heads or Tails
  • -
  • • Win double your bet if correct
  • +
  • • Wähle deinen Einsatzbetrag
  • +
  • • Wähle Kopf oder Zahl
  • +
  • • Gewinne das Doppelte deines Einsatzes bei richtiger Wahl
diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.ts b/frontend/src/app/feature/game/coinflip/coinflip.component.ts index 1766e4d..112a300 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.ts +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.ts @@ -44,14 +44,14 @@ export default class CoinflipComponent implements OnInit { private coinflipSound?: HTMLAudioElement; ngOnInit(): void { - // Subscribe to user updates for real-time balance changes + // Abonniere Benutzerupdates für Echtzeitaktualisierungen des Guthabens this.authService.userSubject.subscribe((user) => { if (user) { this.balance.set(user.balance); } }); - // Initialize coinflip sound + // Initialisiere Münzwurf-Sound this.coinflipSound = new Audio('/sounds/coinflip.mp3'); } @@ -65,26 +65,26 @@ export default class CoinflipComponent implements OnInit { const inputElement = event.target as HTMLInputElement; let value = Number(inputElement.value); - // Reset invalid bet state + // Setze ungültigen Einsatz-Status zurück this.isInvalidBet.set(false); - // Enforce minimum bet of 1 + // Erzwinge Mindesteinsatz von 1 if (value <= 0) { value = 1; } - // Cap bet at available balance and show feedback + // Begrenze Einsatz auf verfügbares Guthaben und zeige Feedback if (value > this.balance()) { value = this.balance(); - // Show visual feedback + // Visuelles Feedback anzeigen this.isInvalidBet.set(true); - // Indicate the error briefly + // Zeige den Fehler kurz an setTimeout(() => this.isInvalidBet.set(false), 800); - // Update the input field directly to show the user the max value + // Aktualisiere das Eingabefeld direkt, um dem Benutzer den maximalen Wert anzuzeigen inputElement.value = String(value); } - // Update signals + // Aktualisiere Signale this.betInputValue.set(value); this.currentBet.set(value); } @@ -100,34 +100,34 @@ export default class CoinflipComponent implements OnInit { private placeBet(side: 'HEAD' | 'TAILS') { if (this.gameInProgress() || this.isActionInProgress()) return; - // Reset previous result + // Setze vorheriges Ergebnis zurück this.gameResult.set(null); this.errorMessage.set(''); - // Set game state + // Setze Spielstatus this.gameInProgress.set(true); this.isActionInProgress.set(true); - // Play bet sound + // Spiele Einsatz-Sound this.audioService.playBetSound(); - // Create bet request + // Erstelle Einsatz-Anfrage const request: CoinflipRequest = { betAmount: this.currentBet(), coinSide: side, }; - // Call API + // API aufrufen this.http .post('/backend/coinflip', request) .pipe( catchError((error) => { - console.error('Error playing coinflip:', error); + console.error('Fehler beim Spielen von Coinflip:', error); if (error.status === 400 && error.error.message.includes('insufficient')) { - this.errorMessage.set('Insufficient funds'); + this.errorMessage.set('Unzureichendes Guthaben'); } else { - this.errorMessage.set('An error occurred. Please try again.'); + this.errorMessage.set('Ein Fehler ist aufgetreten. Bitte versuche es erneut.'); } this.gameInProgress.set(false); @@ -140,37 +140,37 @@ export default class CoinflipComponent implements OnInit { .subscribe((result) => { if (!result) return; - console.log('API response:', result); + console.log('API-Antwort:', result); - // Fix potential property naming inconsistency from the backend + // Behebe mögliche Inkonsistenzen bei der Eigenschaftenbenennung vom Backend const fixedResult: CoinflipGame = { isWin: result.isWin ?? result.win, payout: result.payout, coinSide: result.coinSide, }; - console.log('Fixed result:', fixedResult); + console.log('Korrigiertes Ergebnis:', fixedResult); - // Play coin flip animation and sound + // Spiele Münzwurf-Animation und -Sound this.playCoinFlipAnimation(fixedResult.coinSide); - // Set result after animation completes + // Setze Ergebnis nach Abschluss der Animation setTimeout(() => { this.gameResult.set(fixedResult); - // Update balance with new value from auth service + // Aktualisiere Guthaben mit neuem Wert vom Auth-Service this.authService.loadCurrentUser(); - // Play win sound if player won + // Spiele Gewinn-Sound, wenn der Spieler gewonnen hat if (fixedResult.isWin) { this.audioService.playWinSound(); } - // Reset game state after showing result + // Setze Spielstatus nach Anzeigen des Ergebnisses zurück setTimeout(() => { this.gameInProgress.set(false); }, 1500); - }, 1100); // Just after animation ends + }, 1100); // Kurz nach Ende der Animation }); } @@ -179,48 +179,50 @@ export default class CoinflipComponent implements OnInit { const coinEl = this.coinElement.nativeElement; - // Reset any existing animations + // Setze bestehende Animationen zurück coinEl.classList.remove('animate-to-heads', 'animate-to-tails'); - // Reset any inline styles from previous animations + // Setze alle Inline-Styles von vorherigen Animationen zurück coinEl.style.transform = ''; - // Force a reflow to restart animation + // Erzwinge Reflow, um Animation neu zu starten void coinEl.offsetWidth; - // Play flip sound + // Spiele Münzwurf-Sound if (this.coinflipSound) { this.coinflipSound.currentTime = 0; - this.coinflipSound.play().catch((err) => console.error('Error playing sound:', err)); + this.coinflipSound + .play() + .catch((err) => console.error('Fehler beim Abspielen des Sounds:', err)); } - // Add appropriate animation class based on result + // Füge passende Animationsklasse basierend auf dem Ergebnis hinzu if (result === 'HEAD') { coinEl.classList.add('animate-to-heads'); } else { coinEl.classList.add('animate-to-tails'); } - console.log(`Animation applied for result: ${result}`); + console.log(`Animation angewendet für Ergebnis: ${result}`); } /** - * Validates input as the user types to prevent invalid values + * Validiert Eingabe während der Benutzer tippt, um ungültige Werte zu verhindern */ validateBetInput(event: KeyboardEvent) { - // Allow navigation keys (arrows, delete, backspace, tab) + // Erlaube Navigationstasten (Pfeile, Entf, Rücktaste, Tab) const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab']; if (navigationKeys.includes(event.key)) { return; } - // Only allow numbers + // Erlaube nur Zahlen if (!/^\d$/.test(event.key)) { event.preventDefault(); return; } - // Get the value that would result after the keypress + // Ermittle den Wert, der nach dem Tastendruck entstehen würde const input = event.target as HTMLInputElement; const currentValue = input.value; const cursorPosition = input.selectionStart || 0; @@ -230,14 +232,14 @@ export default class CoinflipComponent implements OnInit { currentValue.substring(input.selectionEnd || cursorPosition); const numValue = Number(newValue); - // Prevent values greater than balance + // Verhindere Werte, die größer als das Guthaben sind if (numValue > this.balance()) { event.preventDefault(); } } - // We removed the paste handler for simplicity since the updateBet method - // will handle any value that gets into the input field + // Der Paste-Handler wurde der Einfachheit halber entfernt, da die updateBet-Methode + // jeden Wert behandelt, der in das Eingabefeld gelangt getResultClass() { if (!this.gameResult()) return ''; diff --git a/frontend/src/app/feature/game/dice/dice.component.ts b/frontend/src/app/feature/game/dice/dice.component.ts index 4967b78..aaca5c5 100644 --- a/frontend/src/app/feature/game/dice/dice.component.ts +++ b/frontend/src/app/feature/game/dice/dice.component.ts @@ -27,7 +27,7 @@ type DiceFormGroup = FormGroup<{ imports: [CommonModule, ReactiveFormsModule, PlaySoundDirective, DragSoundDirective], templateUrl: './dice.component.html', }) -export class DiceComponent implements OnInit { +export default class DiceComponent implements OnInit { private readonly formBuilder = inject(FormBuilder); private readonly diceService = inject(DiceService); private readonly userService = inject(UserService); diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index aa1867e..d354fa4 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -11,7 +11,8 @@ import { ActivatedRoute, RouterLink } from '@angular/router'; import { AuthService } from '@service/auth.service'; import { LoginComponent } from '../auth/login/login.component'; import { RegisterComponent } from '../auth/register/register.component'; -import { RecoverPasswordComponent } from '../auth/recover-password/recover-password.component'; +import '../auth/recover-password/recover-password.component'; +import RecoverPasswordComponent from '../auth/recover-password/recover-password.component'; @Component({ selector: 'app-landing-page', diff --git a/frontend/src/app/service/auth.service.ts b/frontend/src/app/service/auth.service.ts index 081ad72..61b3e7d 100644 --- a/frontend/src/app/service/auth.service.ts +++ b/frontend/src/app/service/auth.service.ts @@ -79,6 +79,15 @@ export class AuthService { ); } + googleAuth(code: string): Observable { + return this.http.post(`${this.oauthUrl}/google/callback`, { code }).pipe( + tap((response) => { + this.setToken(response.token); + this.loadCurrentUser(); + }) + ); + } + logout(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY);