Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
</properties>
<dependencies>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
<version>4.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
Expand All @@ -52,6 +58,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.example.backend.controller;

import lombok.RequiredArgsConstructor;
import org.example.backend.model.Discount;
import org.example.backend.service.DiscountsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class DiscountsController {

private final DiscountsService service;

@GetMapping("/data" )
public List<Discount> getAllDiscounts(){
return service.getAllDiscounts();
}
}
6 changes: 6 additions & 0 deletions backend/src/main/java/org/example/backend/model/Discount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.backend.model;

import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "Discounters")
public record Discount(String id, String name, String image, String price,String provider) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.example.backend.repository;

import org.example.backend.model.Discount;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface DiscountRepository extends MongoRepository<Discount,String> {
List<Discount> findDiscountByProvider(String provider);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.backend.security;

import lombok.Builder;

@Builder
public record AppUser(String id,String providerId,String provider,String userName, String avatarUrl) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.example.backend.security;

import lombok.Builder;
import lombok.With;

@With
@Builder
public record AppUserDto(String id,String userName, String avatarUrl) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.backend.security;

// An interface to unify our custom principal types
public interface AppUserPrincipal {
AppUser getAppUser();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.example.backend.security;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppUserRepository extends MongoRepository <AppUser,String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.example.backend.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
@GetMapping("/me")
public AppUserDto getMe(@AuthenticationPrincipal AppUserPrincipal user){
if (user == null || user.getAppUser() == null) {
return null;
}
AppUser appUser=user.getAppUser();
return new AppUserDto(appUser.id(),appUser.userName(),appUser.avatarUrl());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.example.backend.security;

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Map;

public class CustomOAuth2User implements OAuth2User, AppUserPrincipal {

private final OAuth2User oauth2User;
@Getter
private final AppUser appUser;

public CustomOAuth2User(OAuth2User oauth2User, AppUser appUser) {
this.oauth2User = oauth2User;
this.appUser = appUser;
}

@Override
public Map<String, Object> getAttributes() {
return oauth2User.getAttributes();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return oauth2User.getAuthorities();
}

@Override
public String getName() {
// Return the unique ID from our AppUser, which is more reliable and consistent.
return appUser.id();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.example.backend.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final AppUserRepository userRepo;

// Package-private for easy testing
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getName();
String uniqueId = provider + ":" + providerId;

AppUser appUser = userRepo.findById(uniqueId)
.orElseGet(() -> createAppUser(oAuth2User, uniqueId, provider, providerId));

System.out.println("Loaded user: " + appUser.userName() + " with ID: " + appUser.id());
return new CustomOAuth2User(oAuth2User, appUser);
}

private AppUser createAppUser(OAuth2User oAuth2User, String uniqueId, String provider, String providerId) {
String name = null;
String avatarUrl = null;

switch (provider) {
case "github":
name = oAuth2User.getAttribute("login");
avatarUrl = oAuth2User.getAttribute("avatar_url");
break;
case "google":
name = oAuth2User.getAttribute("name");
avatarUrl = oAuth2User.getAttribute("picture");
break;
case "facebook":
name = oAuth2User.getAttribute("name");
Map<String, Object> picture = oAuth2User.getAttribute("picture");
if (picture != null) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) picture.get("data");
if (data != null) {
avatarUrl = (String) data.get("url");
}
}
break;
}

AppUser newUser = AppUser.builder()
.id(uniqueId)
.provider(provider)
.providerId(providerId)
.userName(name)
.avatarUrl(avatarUrl)
.build();

return userRepo.save(newUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.example.backend.security;

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

import java.util.Collection;
import java.util.Map;

public class CustomOidcUser implements OidcUser, AppUserPrincipal {

private final OidcUser oidcUser;
@Getter
private final AppUser appUser;

public CustomOidcUser(OidcUser oidcUser, AppUser appUser) {
this.oidcUser = oidcUser;
this.appUser = appUser;
}

@Override
public Map<String, Object> getClaims() {
return oidcUser.getClaims();
}

@Override
public OidcUserInfo getUserInfo() {
return oidcUser.getUserInfo();
}

@Override
public OidcIdToken getIdToken() {
return oidcUser.getIdToken();
}

@Override
public Map<String, Object> getAttributes() {
return oidcUser.getAttributes();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return oidcUser.getAuthorities();
}

@Override
public String getName() {
// Use the consistent ID from our AppUser entity
return appUser.id();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.example.backend.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@RequiredArgsConstructor
public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {

private final AppUserRepository userRepo;

// Package-private for easy testing
OidcUserService delegate = new OidcUserService();

@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = delegate.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();

// 'sub' is the standard OIDC claim for user's unique ID
String providerId = oidcUser.getSubject();
String uniqueId = provider + ":" + providerId;

AppUser appUser = userRepo.findById(uniqueId)
.orElseGet(() -> createAppUser(oidcUser, uniqueId, provider, providerId));
System.out.println("Loaded user: " + appUser.userName() + " with ID: " + appUser.id());
return new CustomOidcUser(oidcUser, appUser);
}

private AppUser createAppUser(OidcUser oidcUser, String uniqueId, String provider, String providerId) {
Map<String, Object> attributes = oidcUser.getAttributes();
String name = attributes.get("name").toString();
String avatarUrl = attributes.get("picture").toString();

AppUser newUser = AppUser.builder()
.id(uniqueId)
.provider(provider)
.providerId(providerId)
.userName(name)
.avatarUrl(avatarUrl)
.build();

System.out.println("Saved user:" + newUser);
return userRepo.save(newUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.example.backend.security;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Value("${app.url}")
private String appUrl;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomOidcUserService customOidcUserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(a-> a
// .requestMatchers("/api/auth/params").permitAll()
.requestMatchers("/api/auth/me").authenticated()
.requestMatchers("/api/secured").authenticated()
// .requestMatchers("/api/discounts").authenticated()
.anyRequest().permitAll())
.logout(l->l.logoutSuccessUrl(appUrl))
.oauth2Login(o->o
.defaultSuccessUrl(appUrl)
.userInfoEndpoint(u->u
.userService(customOAuth2UserService)
.oidcUserService(customOidcUserService)))
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
return http.build();
}
}
Loading