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 Feb 27, 2018
1 parent 06def6d commit 2090d61
Show file tree
Hide file tree
Showing 16 changed files with 376 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: test
client-secret: test
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.faforever.api.mautic;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.io.InputStream;
import java.util.Map;

@Component
public class MauticApiMessageConverter extends AbstractHttpMessageConverter<Object> {

private final ObjectMapper objectMapper;

@Inject
public MauticApiMessageConverter(ObjectMapper objectMapper) {
super(MediaType.APPLICATION_JSON);
this.objectMapper = objectMapper;
}

@Override
protected boolean supports(Class<?> clazz) {
return true;
}

@Override
@SneakyThrows
@SuppressWarnings("unchecked")
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) {
try(InputStream inputStream = inputMessage.getBody()) {
return objectMapper.readValue(inputStream, Map.class);
}
}

@Override
@SneakyThrows
protected void writeInternal(Object o, HttpOutputMessage outputMessage) {
outputMessage.getBody().write(objectMapper.writeValueAsBytes(o));
}
}
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;
}
}
74 changes: 74 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,74 @@
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.web.client.RestTemplateBuilder;
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
public class MauticService {

private final RestTemplate restOperations;

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

@VisibleForTesting
MauticService(MauticApiMessageConverter 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
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.
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
42 changes: 35 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,6 +23,7 @@

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
Expand All @@ -45,9 +47,12 @@ public class UserService {
private final AnopeUserRepository anopeUserRepository;
private final FafTokenService fafTokenService;
private final SteamService steamService;
private final 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, MauticService mauticService) {
this.emailService = emailService;
this.playerRepository = playerRepository;
this.userRepository = userRepository;
Expand All @@ -56,6 +61,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 +116,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 +128,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 +142,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 +166,26 @@ void changeLogin(String newLogin, User user) {
nameRecordRepository.save(nameRecord);

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

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

private void createOrUpdateMauticContact(User user, String ipAddress) {
mauticService.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) {
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 +194,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
Loading

0 comments on commit 2090d61

Please sign in to comment.