Skip to content

Commit

Permalink
Create/update Mautic contacts
Browse files Browse the repository at this point in the history
Fixes #202
  • Loading branch information
micheljung committed Apr 1, 2018
1 parent 06def6d commit aeaba70
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/inttest/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ faf-api:
api-key: "banana"
replay:
download-url-format: "http://localhost/faf/vault/replay_vault/replay.php?id=%s"
mautic:
client-id: banana
client-secret: banana
9 changes: 9 additions & 0 deletions src/main/java/com/faforever/api/config/FafApiProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class FafApiProperties {
private Challonge challonge = new Challonge();
private User user = new User();
private Database database = new Database();
private Mautic mautic = new Mautic();

@Data
public static class OAuth2 {
Expand Down Expand Up @@ -215,4 +216,12 @@ public static class Database {
*/
private String schemaVersion;
}

@Data
public static class Mautic {
private String baseUrl;
private String clientId;
private String clientSecret;
private String accessTokenUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.io.IOException;


public class JsonApiOauthMessageConverter extends MappingJackson2HttpMessageConverter {
public class JsonApiOAuthMessageConverter extends MappingJackson2HttpMessageConverter {

@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public JsonApiOauthExceptionRenderer() {

private List<HttpMessageConverter<?>> createMessageConverters() {
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>();
result.add(new JsonApiOauthMessageConverter());
result.add(new JsonApiOAuthMessageConverter());
result.addAll(new RestTemplate().getMessageConverters());
result.add(new JaxbOAuth2ExceptionMessageConverter());
return result;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticApiErrorHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.faforever.api.mautic;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;

import java.io.IOException;
import java.io.InputStream;

@Component
public class MauticApiErrorHandler extends DefaultResponseErrorHandler {

private final ObjectMapper objectMapper;

public MauticApiErrorHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
try (InputStream inputStream = response.getBody()) {
MauticErrorResponse errorResponse = objectMapper.readValue(inputStream, MauticErrorResponse.class);
throw new MauticApiException(errorResponse.getErrors());
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticApiException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.faforever.api.mautic;

import com.faforever.api.mautic.MauticErrorResponse.Error;
import lombok.Getter;

import java.util.List;

@Getter
public class MauticApiException extends RuntimeException {
private final List<Error> errors;

public MauticApiException(List<Error> errors) {
super(errors.toString());
this.errors = errors;
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.faforever.api.mautic;

import lombok.Data;
import lombok.ToString;

import java.util.List;
import java.util.Map;

@Data
public class MauticErrorResponse {
private List<Error> errors;

@Data
@ToString
public static class Error {
private int code;
private String message;
private Map<String, Object> details;
}
}
77 changes: 77 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.faforever.api.mautic;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.config.FafApiProperties.Mautic;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import javax.inject.Inject;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/**
* Provides access to a Mautic (Open Source Marketing Automation) instance via REST API.
*/
@Service
@ConditionalOnProperty(value = "faf-server.mautic.client-id")
public class MauticService {

private final RestTemplate restOperations;

@Inject
public MauticService(MappingJackson2HttpMessageConverter mauticApiMessageConverter,
ResponseErrorHandler mauticApiErrorHandler, FafApiProperties properties) {
this(mauticApiMessageConverter, mauticApiErrorHandler, properties, new RestTemplateBuilder());
}

@VisibleForTesting
MauticService(MappingJackson2HttpMessageConverter mauticApiMessageConverter,
ResponseErrorHandler mauticApiErrorHandler, FafApiProperties properties,
RestTemplateBuilder restTemplateBuilder) {

Mautic mauticProperties = properties.getMautic();

restTemplateBuilder
.additionalMessageConverters(mauticApiMessageConverter)
.errorHandler(mauticApiErrorHandler)
.rootUri(mauticProperties.getBaseUrl());

// TODO use as soon as this is solved: https://github.com/mautic/mautic/issues/5743
// ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
// details.setClientId(mauticProperties.getClientId());
// details.setClientSecret(mauticProperties.getClientSecret());
// details.setClientAuthenticationScheme(AuthenticationScheme.header);
// details.setAccessTokenUri(mauticProperties.getAccessTokenUrl());
// restOperations = restTemplateBuilder.configure(new OAuth2RestTemplate(details));

// TODO for new, client ID needs to be a username and client secret the user's password.
restTemplateBuilder = restTemplateBuilder.basicAuthorization(mauticProperties.getClientId(), mauticProperties.getClientSecret());

restOperations = restTemplateBuilder.build();
}

@Async
public CompletableFuture<Object> createOrUpdateContact(String email, String fafUserId, String fafUserName, String ipAddress, OffsetDateTime lastActive) {
Map<String, Object> body = new HashMap<>();

// These are Mautic default fields. For some reason, these are camel case.
Optional.ofNullable(email).ifPresent(s -> body.put("email", email));
Optional.ofNullable(ipAddress).ifPresent(s -> body.put("ipAddress", ipAddress));
Optional.ofNullable(lastActive).ifPresent(s -> body.put("lastActive", lastActive));

// These are Mautic "custom fields" that need to be created explicitly. For some reason, these are underscore case by default.
Optional.ofNullable(fafUserId).ifPresent(s -> body.put("faf_user_id", fafUserId));
Optional.ofNullable(fafUserName).ifPresent(s -> body.put("faf_username", fafUserName));

return CompletableFuture.completedFuture(restOperations.postForObject("/contacts/new", body, Object.class));
}
}
13 changes: 7 additions & 6 deletions src/main/java/com/faforever/api/user/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.faforever.api.utils.RemoteAddressUtil;
import com.google.common.collect.ImmutableMap;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.MediaType;
Expand Down Expand Up @@ -51,9 +52,9 @@ public void register(HttpServletRequest request,

@ApiOperation("Activates a previously registered account.")
@RequestMapping(path = "/activate", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public void activate(HttpServletResponse response,
public void activate(HttpServletRequest request, HttpServletResponse response,
@RequestParam("token") String token) throws IOException {
userService.activate(token);
userService.activate(token, RemoteAddressUtil.getRemoteAddress(request));
response.sendRedirect(fafApiProperties.getRegistration().getSuccessRedirectUrl());
}

Expand All @@ -67,16 +68,16 @@ public void changePassword(@RequestParam("currentPassword") String currentPasswo
@PreAuthorize("#oauth2.hasScope('write_account_data') and hasRole('ROLE_USER')")
@ApiOperation("Changes the login of a previously registered account.")
@RequestMapping(path = "/changeUsername", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public void changeLogin(@RequestParam("newUsername") String newUsername, Authentication authentication) {
userService.changeLogin(newUsername, userService.getUser(authentication));
public void changeLogin(HttpServletRequest request, @RequestParam("newUsername") String newUsername, Authentication authentication) {
userService.changeLogin(newUsername, userService.getUser(authentication), RemoteAddressUtil.getRemoteAddress(request));
}


@PreAuthorize("#oauth2.hasScope('write_account_data') and hasRole('ROLE_USER')")
@ApiOperation("Changes the email of a previously registered account.")
@RequestMapping(path = "/changeEmail", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public void changeEmail(@RequestParam("currentPassword") String currentPassword, @RequestParam("newEmail") String newEmail, Authentication authentication) {
userService.changeEmail(currentPassword, newEmail, userService.getUser(authentication));
public void changeEmail(HttpServletRequest request, @RequestParam("currentPassword") String currentPassword, @RequestParam("newEmail") String newEmail, Authentication authentication) {
userService.changeEmail(currentPassword, newEmail, userService.getUser(authentication), RemoteAddressUtil.getRemoteAddress(request));
}


Expand Down
43 changes: 36 additions & 7 deletions src/main/java/com/faforever/api/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.faforever.api.mautic.MauticService;
import com.faforever.api.player.PlayerRepository;
import com.faforever.api.security.FafPasswordEncoder;
import com.faforever.api.security.FafTokenService;
Expand All @@ -22,8 +23,10 @@

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

import static com.faforever.api.error.ErrorCode.TOKEN_INVALID;
Expand All @@ -45,9 +48,12 @@ public class UserService {
private final AnopeUserRepository anopeUserRepository;
private final FafTokenService fafTokenService;
private final SteamService steamService;
private final Optional<MauticService> mauticService;

public UserService(EmailService emailService, PlayerRepository playerRepository, UserRepository userRepository,
NameRecordRepository nameRecordRepository, FafApiProperties properties, AnopeUserRepository anopeUserRepository, FafTokenService fafTokenService, SteamService steamService) {
NameRecordRepository nameRecordRepository, FafApiProperties properties,
AnopeUserRepository anopeUserRepository, FafTokenService fafTokenService,
SteamService steamService, Optional<MauticService> mauticService) {
this.emailService = emailService;
this.playerRepository = playerRepository;
this.userRepository = userRepository;
Expand All @@ -56,6 +62,7 @@ public UserService(EmailService emailService, PlayerRepository playerRepository,
this.anopeUserRepository = anopeUserRepository;
this.fafTokenService = fafTokenService;
this.steamService = steamService;
this.mauticService = mauticService;
this.passwordEncoder = new FafPasswordEncoder();
}

Expand Down Expand Up @@ -110,7 +117,7 @@ private void validateUsername(String username) {
@SneakyThrows
@SuppressWarnings("unchecked")
@Transactional
void activate(String token) {
void activate(String token, String ipAddress) {
Map<String, String> claims = fafTokenService.resolveToken(FafTokenType.REGISTRATION, token);

String username = claims.get(KEY_USERNAME);
Expand All @@ -122,8 +129,10 @@ void activate(String token) {
user.setEmail(email);
user.setLogin(username);

user = userRepository.save(user);
log.debug("User has been activated: {}", user);
userRepository.save(user);

createOrUpdateMauticContact(user, ipAddress);
}

void changePassword(String currentPassword, String newPassword, User user) {
Expand All @@ -134,7 +143,7 @@ void changePassword(String currentPassword, String newPassword, User user) {
setPassword(user, newPassword);
}

void changeLogin(String newLogin, User user) {
void changeLogin(String newLogin, User user, String ipAddress) {
validateUsername(newLogin);

int minDaysBetweenChange = properties.getUser().getMinimumDaysBetweenUsernameChange();
Expand All @@ -158,10 +167,26 @@ void changeLogin(String newLogin, User user) {
nameRecordRepository.save(nameRecord);

user.setLogin(newLogin);
userRepository.save(user);

createOrUpdateMauticContact(userRepository.save(user), ipAddress);
}

public void changeEmail(String currentPassword, String newEmail, User user) {
private void createOrUpdateMauticContact(User user, String ipAddress) {
mauticService.ifPresent(service -> service.createOrUpdateContact(
user.getEmail(),
String.valueOf(user.getId()),
user.getLogin(),
ipAddress,
OffsetDateTime.now()
)
.thenAccept(result -> log.debug("Updated contact in Mautic: {}", user.getEmail()))
.exceptionally(throwable -> {
log.warn("Could not update contact in Mautic: {}", user, throwable);
return null;
}));
}

public void changeEmail(String currentPassword, String newEmail, User user, String ipAddress) {
if (!Objects.equals(user.getPassword(), passwordEncoder.encode(currentPassword))) {
throw new ApiException(new Error(ErrorCode.EMAIL_CHANGE_FAILED_WRONG_PASSWORD));
}
Expand All @@ -170,7 +195,11 @@ public void changeEmail(String currentPassword, String newEmail, User user) {

log.debug("Changing email for user ''{}'' to ''{}''", user, newEmail);
user.setEmail(newEmail);
userRepository.save(user);
createOrUpdateUser(user, ipAddress);
}

private void createOrUpdateUser(User user, String ipAddress) {
createOrUpdateMauticContact(userRepository.save(user), ipAddress);
}

void resetPassword(String identifier, String newPassword) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/faforever/api/utils/RemoteAddressUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.faforever.api.utils;

import lombok.experimental.UtilityClass;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;

@UtilityClass
public class RemoteAddressUtil {
/** Returns the remote address of the request, considering the {@code X-Forwarded-For} header. */
public String getRemoteAddress(HttpServletRequest request) {
Assert.notNull(request, "The parameter 'request' must not be null");

String remoteAddr = request.getHeader("X-FORWARDED-FOR");
if (remoteAddr == null || remoteAddr.isEmpty()) {
remoteAddr = request.getRemoteAddr();
}

return remoteAddr;
}
}
1 change: 1 addition & 0 deletions src/main/resources/config/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ faf-api:
steam-redirect-url-format: ${STEAM_LINK_REDIRECT_URL:http://localhost:8010/users/linkToSteam?token=%s}
success-redirect-url: ${STEAM_LINK_SUCCESS_URL:http://localhost:8020/linked_to_steam}
error-redirect-url-format: ${STEAM_LINK_ERROR_URL:http://localhost:8020/error?code=%s}

spring:
datasource:
url: jdbc:mysql://${DATABASE_ADDRESS:127.0.0.1}/${DATABASE_NAME:faf}?useSSL=false
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/config/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ faf-api:
steam-redirect-url-format: ${STEAM_LINK_REDIRECT_URL}
success-redirect-url: ${STEAM_LINK_SUCCESS_URL}
error-redirect-url-format: ${STEAM_LINK_ERROR_URL}
mautic:
base-url: ${MAUTIC_BASE_URL}
client-id: ${MAUTIC_CLIENT_ID}
client-secret: ${MAUTIC_CLIENT_SECRET}
access-token-url: ${MAUTIC_ACCESS_TOKEN_URL}

spring:
datasource:
Expand Down

0 comments on commit aeaba70

Please sign in to comment.