Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create/8080/create facebook login #454

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
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
29 changes: 29 additions & 0 deletions core/src/main/java/greencity/config/CorsFilterConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package greencity.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilterConfig extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
response.setHeader("Access-Control-Allow-Origin", "https://eb10-91-245-77-57.ngrok-free.app");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, ngrok-skip-browser-warning");
response.setHeader("Access-Control-Allow-Credentials", "true");
Comment on lines +19 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Externalize CORS configuration values

The hard-coded ngrok URL and CORS settings should be moved to configuration properties:

  1. ngrok URLs are temporary and will change
  2. CORS settings may differ between environments (dev/staging/prod)

Consider this approach:

+@Value("${cors.allowed-origin}")
+private String allowedOrigin;
+@Value("${cors.allowed-methods}")
+private String allowedMethods;
+@Value("${cors.allowed-headers}")
+private String allowedHeaders;

 @Override
 protected void doFilterInternal(...) {
-    response.setHeader("Access-Control-Allow-Origin", "https://eb10-91-245-77-57.ngrok-free.app");
-    response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
-    response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, ngrok-skip-browser-warning");
+    response.setHeader("Access-Control-Allow-Origin", allowedOrigin);
+    response.setHeader("Access-Control-Allow-Methods", allowedMethods);
+    response.setHeader("Access-Control-Allow-Headers", allowedHeaders);

Add to application.properties/yaml:

cors.allowed-origin=https://your-frontend-domain.com
cors.allowed-methods=GET,POST,OPTIONS
cors.allowed-headers=Content-Type,Authorization

if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
filterChain.doFilter(request, response);
}
}
3 changes: 3 additions & 0 deletions core/src/main/java/greencity/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/ownSecurity/unblockAccount",
"/api/testers/sign-in")
.permitAll()
.requestMatchers(HttpMethod.GET, "/facebookSecurity/login").permitAll()
.requestMatchers(HttpMethod.POST, "/facebookSecurity/login").permitAll()
.requestMatchers(HttpMethod.GET, "/check-auth").permitAll()
.requestMatchers(HttpMethod.GET,
"/user/to-do-list-items/habits/{habitId}/to-do-list",
"/user/{userId}/{habitId}/custom-to-do-list-items/available",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

/**
* Controller that provide google security logic.
Expand Down Expand Up @@ -54,4 +57,9 @@ public String generateFacebookAuthorizeURL() {
public SuccessSignInDto generateFacebookAccessToken(@RequestParam("code") String code) {
return facebookSecurityService.generateFacebookAccessToken(code);
}

@PostMapping(value = "/login", consumes = "application/json")
public SuccessSignInDto loginWithFacebook(@RequestBody Map<String, String> request) {
return facebookSecurityService.authenticate(request.get("token"), request.get("lang"));
}
}
2 changes: 2 additions & 0 deletions core/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ spring.messaging.stomp.websocket.allowed-origins=\

#Googleapis
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo?access_token=
#FaceBookAPI
facebook.resource.userInfoUri=https://graph.facebook.com/v22.0/me?fields=id,name,email,picture&access_token=
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package greencity.security.controller;

import greencity.security.dto.SuccessSignInDto;
import greencity.security.service.FacebookSecurityService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@ExtendWith(MockitoExtension.class)
Expand Down Expand Up @@ -46,4 +55,13 @@ void generateFacebookAccessTokenTest() throws Exception {
.andExpect(status().isOk());
verify(facebookSecurityService).generateFacebookAccessToken("almostSecretCode");
}

@Test
void authenticateTest() throws Exception {
mockMvc.perform(post("/facebookSecurity/login")
.contentType("application/json")
.content("{\"token\":\"almostSecretToken\", \"lang\":\"en\"}"))
.andExpect(status().isOk());
verify(facebookSecurityService).authenticate("almostSecretToken", "en");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class ErrorMessage {
public static final String BRUTEFORCE_PROTECTION_MESSAGE_WRONG_PASS =
"User account is blocked due to too many failed login attempts. Try again in %s minutes";
public static final String WRONG_SECRET_KEY = "Wrong secret key";
public static final String FB_TOKEN_OR_LANGUAGE_MISSING = "fbToken or language is missing";
public static final String WARNING_GIT_DIRECTORY_NOT_FOUND =
"WARNING: .git directory not found. Git commit info will be unavailable.";
public static final String GIT_REPOSITORY_NOT_INITIALIZED =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public interface FacebookSecurityService {
* @return {@link SuccessSignInDto} if token valid
*/
SuccessSignInDto generateFacebookAccessToken(String code);

SuccessSignInDto authenticate(String fbToken, String language);
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
package greencity.security.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import greencity.client.RestClient;
import greencity.constant.AppConstant;
import static greencity.constant.AppConstant.DEFAULT_RATING;
import static greencity.constant.AppConstant.REGISTRATION_EMAIL_FIELD_NAME;
import greencity.constant.ErrorMessage;
import greencity.dto.ubs.UbsProfileCreationDto;
import greencity.dto.user.UserVO;
import greencity.entity.Language;
import greencity.entity.User;
import greencity.entity.UserNotificationPreference;
import greencity.enums.EmailNotification;
import greencity.enums.EmailPreference;
import greencity.enums.EmailPreferencePeriodicity;
import greencity.enums.ProfilePrivacyPolicy;
import greencity.enums.Role;
import greencity.enums.UserStatus;
import greencity.exception.exceptions.UserDeactivatedException;
import greencity.repository.UserRepo;
import greencity.security.dto.SuccessSignInDto;
import greencity.security.jwt.JwtTool;
import greencity.service.UserService;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import greencity.dto.user.UserInfo;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.social.facebook.api.Facebook;
import org.springframework.social.facebook.api.impl.FacebookTemplate;
import org.springframework.social.facebook.connect.FacebookConnectionFactory;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.client.RestClientException;

/**
* {@inheritDoc}
Expand All @@ -33,13 +58,21 @@
public class FacebookSecurityServiceImpl implements FacebookSecurityService {
private final UserService userService;
private final JwtTool jwtTool;
private final HttpClient httpClient;
private final UserRepo userRepo;
private final PlatformTransactionManager transactionManager;
private final ModelMapper modelMapper;
private final RestClient restClient;
private final ObjectMapper objectMapper;

@Value("${address}")
private String address;
@Value("${spring.social.facebook.app-id}")
private String facebookAppId;
@Value("${spring.social.facebook.app-secret}")
private String facebookAppSecret;
@Value("${facebook.resource.userInfoUri}")
private String userInfoUrl;

/**
* {@inheritDoc}
Expand Down Expand Up @@ -97,7 +130,7 @@ public SuccessSignInDto generateFacebookAccessToken(String code) {
}
}

private User createNewUser(String email, String userName) {
User createNewUser(String email, String userName) {
return User.builder()
.email(email)
.name(userName)
Expand All @@ -111,9 +144,117 @@ private User createNewUser(String email, String userName) {
.build();
}

private SuccessSignInDto getSuccessSignInDto(UserVO user) {
User createNewUser(String email, String userName, String profilePicture, String language) {
User user = User.builder()
.email(email)
.name(userName)
.role(Role.ROLE_USER)
.dateOfRegistration(LocalDateTime.now())
.lastActivityTime(LocalDateTime.now())
.userStatus(UserStatus.ACTIVATED)
.emailNotification(EmailNotification.DISABLED)
.refreshTokenKey(jwtTool.generateTokenKey())
.profilePicturePath(profilePicture)
.showLocation(ProfilePrivacyPolicy.PUBLIC)
.showEcoPlace(ProfilePrivacyPolicy.PUBLIC)
.showToDoList(ProfilePrivacyPolicy.PUBLIC)
.rating(DEFAULT_RATING)
.language(Language.builder().id(modelMapper.map(language, Long.class)).build())
.build();
Set<UserNotificationPreference> userNotificationPreferences = Arrays.stream(EmailPreference.values())
.map(emailPreference -> UserNotificationPreference.builder()
.user(user)
.emailPreference(emailPreference)
.periodicity(EmailPreferencePeriodicity.TWICE_A_DAY)
.build())
.collect(Collectors.toSet());
user.setNotificationPreferences(userNotificationPreferences);
return user;
}
Comment on lines +147 to +173
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider more restrictive default privacy settings.

Setting all privacy settings to PUBLIC by default may not be the best practice for user privacy.

Consider more privacy-conscious defaults:

-            .showLocation(ProfilePrivacyPolicy.PUBLIC)
-            .showEcoPlace(ProfilePrivacyPolicy.PUBLIC)
-            .showToDoList(ProfilePrivacyPolicy.PUBLIC)
+            .showLocation(ProfilePrivacyPolicy.PRIVATE)
+            .showEcoPlace(ProfilePrivacyPolicy.FRIENDS_ONLY)
+            .showToDoList(ProfilePrivacyPolicy.FRIENDS_ONLY)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
User createNewUser(String email, String userName, String profilePicture, String language) {
User user = User.builder()
.email(email)
.name(userName)
.role(Role.ROLE_USER)
.dateOfRegistration(LocalDateTime.now())
.lastActivityTime(LocalDateTime.now())
.userStatus(UserStatus.ACTIVATED)
.emailNotification(EmailNotification.DISABLED)
.refreshTokenKey(jwtTool.generateTokenKey())
.profilePicturePath(profilePicture)
.showLocation(ProfilePrivacyPolicy.PUBLIC)
.showEcoPlace(ProfilePrivacyPolicy.PUBLIC)
.showToDoList(ProfilePrivacyPolicy.PUBLIC)
.rating(DEFAULT_RATING)
.language(Language.builder().id(modelMapper.map(language, Long.class)).build())
.build();
Set<UserNotificationPreference> userNotificationPreferences = Arrays.stream(EmailPreference.values())
.map(emailPreference -> UserNotificationPreference.builder()
.user(user)
.emailPreference(emailPreference)
.periodicity(EmailPreferencePeriodicity.TWICE_A_DAY)
.build())
.collect(Collectors.toSet());
user.setNotificationPreferences(userNotificationPreferences);
return user;
}
User createNewUser(String email, String userName, String profilePicture, String language) {
User user = User.builder()
.email(email)
.name(userName)
.role(Role.ROLE_USER)
.dateOfRegistration(LocalDateTime.now())
.lastActivityTime(LocalDateTime.now())
.userStatus(UserStatus.ACTIVATED)
.emailNotification(EmailNotification.DISABLED)
.refreshTokenKey(jwtTool.generateTokenKey())
.profilePicturePath(profilePicture)
.showLocation(ProfilePrivacyPolicy.PRIVATE)
.showEcoPlace(ProfilePrivacyPolicy.FRIENDS_ONLY)
.showToDoList(ProfilePrivacyPolicy.FRIENDS_ONLY)
.rating(DEFAULT_RATING)
.language(Language.builder().id(modelMapper.map(language, Long.class)).build())
.build();
Set<UserNotificationPreference> userNotificationPreferences = Arrays.stream(EmailPreference.values())
.map(emailPreference -> UserNotificationPreference.builder()
.user(user)
.emailPreference(emailPreference)
.periodicity(EmailPreferencePeriodicity.TWICE_A_DAY)
.build())
.collect(Collectors.toSet());
user.setNotificationPreferences(userNotificationPreferences);
return user;
}


public SuccessSignInDto authenticate(String fbToken, String language) {
if (fbToken == null || language == null) {
throw new IllegalArgumentException(ErrorMessage.FB_TOKEN_OR_LANGUAGE_MISSING);
}
try {
UserInfo userInfo = getUserInfoFromFacebook(fbToken);
if (userInfo.getEmail() == null) {
throw new IllegalArgumentException(ErrorMessage.BAD_FACEBOOK_TOKEN);
}
String profilePicture = null;
if (userInfo.getPicture() != null) {
profilePicture = userInfo.getPicture();
}
return processAuthentication(userInfo.getEmail(), userInfo.getName(), profilePicture, language);
} catch (IOException e) {
throw new IllegalArgumentException(ErrorMessage.BAD_FACEBOOK_TOKEN + e.getMessage());
}
}

SuccessSignInDto processAuthentication(String email, String userName, String profilePicture,
String language) {
UserVO userVO = userService.findByEmail(email);
if (userVO == null) {
log.info(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + "{}", email);
return handleNewUser(email, userName, profilePicture, language);
} else {
if (userVO.getUserStatus() == UserStatus.DEACTIVATED) {
throw new UserDeactivatedException(ErrorMessage.USER_DEACTIVATED);
}
return getSuccessSignInDto(userVO);
}
}

SuccessSignInDto handleNewUser(String email, String userName, String profilePicture, String language) {
User newUser = createNewUser(email, userName, profilePicture, language);
User savedUser = saveNewUser(newUser);
try {
restClient.createUbsProfile(modelMapper.map(savedUser, UbsProfileCreationDto.class));
} catch (RestClientException e) {
throw new RestClientException(ErrorMessage.TRANSACTION_FAILED, e);
}
UserVO userVO = modelMapper.map(savedUser, UserVO.class);
return getSuccessSignInDto(userVO);
}

User saveNewUser(User newUser) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
return transactionTemplate.execute(status -> {
newUser.setUuid(UUID.randomUUID().toString());
Long id = userRepo.save(newUser).getId();
newUser.setId(id);
return newUser;
});
}

SuccessSignInDto getSuccessSignInDto(UserVO user) {
String accessToken = jwtTool.createAccessToken(user.getEmail(), user.getRole());
String refreshToken = jwtTool.createRefreshToken(user);
return new SuccessSignInDto(user.getId(), accessToken, refreshToken, user.getName(), false);
}

UserInfo getUserInfoFromFacebook(String accessToken) throws IOException {
String requestUrl = userInfoUrl + "?fields=id,name,email,picture&access_token=" + accessToken;
HttpGet request = new HttpGet(requestUrl);
try (CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new IOException("Facebook API returned status: " + statusCode);
}
String jsonResponse = EntityUtils.toString(response.getEntity());
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
String name = jsonNode.has("name") ? jsonNode.get("name").asText() : "Unknown";
String email = jsonNode.has("email") ? jsonNode.get("email").asText() : null;
String pictureUrl = Optional.ofNullable(jsonNode.get("picture"))
.map(p -> p.get("data"))
.map(d -> d.get("url"))
.map(JsonNode::asText)
.orElse(null);
UserInfo userInfo = new UserInfo();
userInfo.setName(name);
userInfo.setEmail(email);
userInfo.setPicture(pictureUrl);
return userInfo;
}
}
}
Loading
Loading