diff --git a/backend/src/main/java/de/szut/casino/user/UserController.java b/backend/src/main/java/de/szut/casino/user/UserController.java new file mode 100644 index 0000000..4d232ac --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -0,0 +1,62 @@ +package de.szut.casino.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/user/{id}") + public ResponseEntity getUser(@PathVariable String id) { + if (id == null || !userService.exists(id)) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(userService.getUser(id)); + } + + @PostMapping("/user") + public ResponseEntity createUser(@RequestBody @Valid CreateUserDto userData) { + if (userService.exists(userData.getKeycloakId())) { + + return this.redirect("/user/" + userData.getKeycloakId()); + } + + return ResponseEntity.ok(userService.createUser(userData)); + } + + @GetMapping("/user") + public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { + GetUserDto userData = userService.getCurrentUser(token); + + if (userData == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(userData); + } + + private ResponseEntity redirect(String route) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Location", route); + + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java new file mode 100644 index 0000000..f6c1a5b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -0,0 +1,30 @@ +package de.szut.casino.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@Entity +@NoArgsConstructor +public class UserEntity { + @Id + @GeneratedValue + private Long id; + @Column(unique = true) + private String keycloakId; + private String username; + private float balance; + + public UserEntity(String keycloakId, String username, float balance) { + this.keycloakId = keycloakId; + this.username = username; + this.balance = balance; + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserMappingService.java b/backend/src/main/java/de/szut/casino/user/UserMappingService.java new file mode 100644 index 0000000..e0183ec --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -0,0 +1,17 @@ +package de.szut.casino.user; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import org.springframework.stereotype.Service; + +@Service +public class UserMappingService { + public GetUserDto mapToGetUserDto(UserEntity user) { + return new GetUserDto(user.getKeycloakId(), user.getUsername(), user.getBalance()); + } + + public UserEntity mapToUserEntity(CreateUserDto createUserDto) { + return new UserEntity(createUserDto.getKeycloakId(), createUserDto.getUsername(), 0); + } +} + diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java new file mode 100644 index 0000000..aaa5752 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -0,0 +1,15 @@ +package de.szut.casino.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public interface UserRepository extends JpaRepository { + @Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1") + Optional findOneByKeycloakId(String keycloakId); + + boolean existsByKeycloakId(String keycloakId); +} diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java new file mode 100644 index 0000000..724962e --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -0,0 +1,64 @@ +package de.szut.casino.user; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import de.szut.casino.user.dto.KeycloakUserDto; + +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + @Autowired + private RestTemplate http; + + @Autowired + private UserMappingService mappingService; + + public UserEntity createUser(CreateUserDto createUserDto) { + UserEntity user = mappingService.mapToUserEntity(createUserDto); + userRepository.save(user); + + return user; + } + + public GetUserDto getUser(String keycloakId) { + Optional user = this.userRepository.findOneByKeycloakId(keycloakId); + + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + } + + public GetUserDto getCurrentUser(String token) { + KeycloakUserDto userData = getKeycloakUserInfo(token); + + if (userData == null) { + return null; + } + Optional user = this.userRepository.findOneByKeycloakId(userData.getSub()); + + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + + } + + private KeycloakUserDto getKeycloakUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity response = this.http.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + + return response.getBody(); + } + + public boolean exists(String keycloakId) { + return userRepository.existsByKeycloakId(keycloakId); + } +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java new file mode 100644 index 0000000..ff28427 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java @@ -0,0 +1,15 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CreateUserDto { + private String keycloakId; + private String username; +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java new file mode 100644 index 0000000..fb690af --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java @@ -0,0 +1,16 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GetUserDto { + private String keycloakId; + private String username; + private float balance; +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java new file mode 100644 index 0000000..4238e13 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java @@ -0,0 +1,15 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class KeycloakUserDto { + private String sub; + private String preferred_username; +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 8401ece..217efd4 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -27,7 +27,7 @@ export const initializeKeycloak = (keycloak: KeycloakService) => async () => onLoad: 'check-sso', silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', checkLoginIframe: false, - redirectUri: 'http://localhost:4200', + redirectUri: window.location.origin + '/', }, }); diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index dc68406..6fbef95 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -7,6 +7,10 @@ export const routes: Routes = [ path: '', component: LandingComponent, }, + { + path: 'login/success', + loadComponent: () => import('./feature/login-success/login-success.component'), + }, { path: 'home', loadComponent: () => import('./feature/home/home.component'), diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts index 0743ff6..035ccc8 100644 --- a/frontend/src/app/auth.guard.ts +++ b/frontend/src/app/auth.guard.ts @@ -1,23 +1,16 @@ -import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; +import { CanActivateFn, Router } from '@angular/router'; import { inject } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; -export const authGuard: CanActivateFn = async ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot -) => { +export const authGuard: CanActivateFn = async () => { const keycloakService = inject(KeycloakService); - const isLoggedIn = keycloakService.isLoggedIn(); + const router = inject(Router); - if (isLoggedIn) { + if (keycloakService.isLoggedIn()) { return true; } - const baseurl = window.location.origin; - - keycloakService.login({ - redirectUri: `${baseurl}${state.url}`, - }); + router.navigate(['']); return false; }; diff --git a/frontend/src/app/feature/login-success/login-success.component.css b/frontend/src/app/feature/login-success/login-success.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/feature/login-success/login-success.component.html b/frontend/src/app/feature/login-success/login-success.component.html new file mode 100644 index 0000000..ba9d449 --- /dev/null +++ b/frontend/src/app/feature/login-success/login-success.component.html @@ -0,0 +1 @@ +

Logging in...

diff --git a/frontend/src/app/feature/login-success/login-success.component.ts b/frontend/src/app/feature/login-success/login-success.component.ts new file mode 100644 index 0000000..bf81ac6 --- /dev/null +++ b/frontend/src/app/feature/login-success/login-success.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { UserService } from '../../service/user.service'; +import { KeycloakService } from 'keycloak-angular'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-login-success', + standalone: true, + imports: [], + templateUrl: './login-success.component.html', + styleUrl: './login-success.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class LoginSuccessComponent implements OnInit { + private userService: UserService = inject(UserService); + private keycloakService: KeycloakService = inject(KeycloakService); + private router: Router = inject(Router); + + async ngOnInit() { + const userProfile = await this.keycloakService.loadUserProfile(); + const user = await this.userService.getOrCreateUser(userProfile); + sessionStorage.setItem('user', JSON.stringify(user)); + + this.router.navigate(['']); + } +} diff --git a/frontend/src/app/model/User.ts b/frontend/src/app/model/User.ts new file mode 100644 index 0000000..a579b7a --- /dev/null +++ b/frontend/src/app/model/User.ts @@ -0,0 +1,5 @@ +export interface User { + keycloakId: string; + username: string; + balance: number; +} diff --git a/frontend/src/app/service/user.service.ts b/frontend/src/app/service/user.service.ts new file mode 100644 index 0000000..ba6bead --- /dev/null +++ b/frontend/src/app/service/user.service.ts @@ -0,0 +1,38 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { KeycloakProfile } from 'keycloak-js'; +import { catchError, EMPTY, Observable } from 'rxjs'; +import { User } from '../model/User'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + private http: HttpClient = inject(HttpClient); + + public getUser(id: string): Observable { + return this.http.get(`/backend/user/${id}`).pipe(catchError(() => EMPTY)); + } + + public createUser(id: string, username: string): Observable { + return this.http.post('/backend/user', { + keycloakId: id, + username: username, + }); + } + + public async getOrCreateUser(userProfile: KeycloakProfile) { + if (userProfile.id == null) { + return; + } + return await this.getUser(userProfile.id) + .toPromise() + .then(async (user) => { + if (user) { + return user; + } + + return await this.createUser(userProfile.id ?? '', userProfile.username ?? '').toPromise(); + }); + } +} diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index ba86e73..53d1dee 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -18,7 +18,7 @@ export class NavbarComponent { login() { try { const baseUrl = window.location.origin; - this.keycloakService.login({ redirectUri: `${baseUrl}/home` }); + this.keycloakService.login({ redirectUri: `${baseUrl}/login/success` }); } catch (error) { console.error('Login failed:', error); }