diff --git a/pom.xml b/pom.xml
index 81ceb37..02c98d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,6 +104,12 @@
1.0
compile
+
+ nl.basjes.parse.useragent
+ yauaa
+ 7.28.1
+ compile
+
diff --git a/src/main/java/cc/pulseapp/api/controller/v1/UserController.java b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java
index f6c3f94..3de2c82 100644
--- a/src/main/java/cc/pulseapp/api/controller/v1/UserController.java
+++ b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java
@@ -3,6 +3,7 @@ package cc.pulseapp.api.controller.v1;
import cc.pulseapp.api.exception.impl.BadRequestException;
import cc.pulseapp.api.model.user.User;
import cc.pulseapp.api.model.user.UserDTO;
+import cc.pulseapp.api.model.user.device.Device;
import cc.pulseapp.api.model.user.input.CompleteOnboardingInput;
import cc.pulseapp.api.model.user.input.DisableTFAInput;
import cc.pulseapp.api.model.user.input.EnableTFAInput;
@@ -99,7 +100,7 @@ public final class UserController {
}
/**
- * A POST endpoint to disable TFA for a useer.
+ * A POST endpoint to disable TFA for a user.
*
* @param input the input to process
* @return the disabled response
@@ -111,6 +112,17 @@ public final class UserController {
return ResponseEntity.ok(Map.of("success", true));
}
+ /**
+ * A GET endpoint to get the
+ * devices logged into the user.
+ *
+ * @return the devices
+ */
+ @GetMapping("/devices") @ResponseBody @NonNull
+ public ResponseEntity> getDevices() {
+ return ResponseEntity.ok(userService.getDevices());
+ }
+
/**
* A POST endpoint to logout the user.
*
diff --git a/src/main/java/cc/pulseapp/api/model/user/device/BrowserType.java b/src/main/java/cc/pulseapp/api/model/user/device/BrowserType.java
new file mode 100644
index 0000000..d92fa1b
--- /dev/null
+++ b/src/main/java/cc/pulseapp/api/model/user/device/BrowserType.java
@@ -0,0 +1,11 @@
+package cc.pulseapp.api.model.user.device;
+
+/**
+ * The type of browser used
+ * by a {@link Device}.
+ *
+ * @author Braydon
+ */
+public enum BrowserType {
+ FIREFOX, EDGE, CHROME, SAFARI, SAMSUNGBROWSER, UNKNOWN
+}
\ No newline at end of file
diff --git a/src/main/java/cc/pulseapp/api/model/user/device/Device.java b/src/main/java/cc/pulseapp/api/model/user/device/Device.java
new file mode 100644
index 0000000..7a19aad
--- /dev/null
+++ b/src/main/java/cc/pulseapp/api/model/user/device/Device.java
@@ -0,0 +1,74 @@
+package cc.pulseapp.api.model.user.device;
+
+import cc.pulseapp.api.model.user.User;
+import cc.pulseapp.api.model.user.session.Session;
+import cc.pulseapp.api.model.user.session.SessionLocation;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.ToString;
+
+import java.util.Date;
+
+/**
+ * A device logged into a
+ * {@link User}'s account.
+ *
+ * @author Braydon
+ */
+@AllArgsConstructor @Getter @ToString
+public final class Device {
+ /**
+ * The type of this device.
+ */
+ @NonNull private final DeviceType type;
+
+ /**
+ * The browser type of this device.
+ */
+ @NonNull private final BrowserType browserType;
+
+ /**
+ * The IP address of this device.
+ */
+ @NonNull private final String ip;
+
+ /**
+ * The location of this device, if known.
+ */
+ private final String location;
+
+ /**
+ * The user agent of this device.
+ */
+ @NonNull private final String userAgent;
+
+ /**
+ * The session snowflake associated with this device.
+ */
+ private final long sessionSnowflake;
+
+ /**
+ * The date this device first logged into the account.
+ */
+ private final Date firstLogin;
+
+ /**
+ * Construct a device from a session.
+ *
+ * @param session the session
+ * @param deviceType the device type
+ * @param browserType the device browser type
+ * @param firstLogin the sessions first login time
+ * @return the constructed device
+ */
+ @NonNull
+ public static Device fromSession(@NonNull Session session, @NonNull DeviceType deviceType,
+ @NonNull BrowserType browserType, @NonNull Date firstLogin) {
+ SessionLocation rawLocation = session.getLocation();
+ String location = rawLocation.getCountry() == null ? null
+ : rawLocation.getCity() + ", " + rawLocation.getRegion() + ", " + rawLocation.getCountry();
+ return new Device(deviceType, browserType, rawLocation.getIp(), location,
+ rawLocation.getUserAgent(), session.getSnowflake(), firstLogin);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cc/pulseapp/api/model/user/device/DeviceType.java b/src/main/java/cc/pulseapp/api/model/user/device/DeviceType.java
new file mode 100644
index 0000000..2919cde
--- /dev/null
+++ b/src/main/java/cc/pulseapp/api/model/user/device/DeviceType.java
@@ -0,0 +1,10 @@
+package cc.pulseapp.api.model.user.device;
+
+/**
+ * The type of a {@link Device}.
+ *
+ * @author Braydon
+ */
+public enum DeviceType {
+ DESKTOP, TABLET, PHONE, UNKNOWN
+}
\ No newline at end of file
diff --git a/src/main/java/cc/pulseapp/api/model/user/session/Session.java b/src/main/java/cc/pulseapp/api/model/user/session/Session.java
index b8d1097..3ad8fc2 100644
--- a/src/main/java/cc/pulseapp/api/model/user/session/Session.java
+++ b/src/main/java/cc/pulseapp/api/model/user/session/Session.java
@@ -21,7 +21,7 @@ public final class Session {
/**
* The snowflake of this session.
*/
- @EqualsAndHashCode.Include @Id @JsonIgnore private final long snowflake;
+ @EqualsAndHashCode.Include @Id private final long snowflake;
/**
* The snowflake of the user this session is for.
diff --git a/src/main/java/cc/pulseapp/api/service/CaptchaService.java b/src/main/java/cc/pulseapp/api/service/CaptchaService.java
index 5b7e1f9..299dbef 100644
--- a/src/main/java/cc/pulseapp/api/service/CaptchaService.java
+++ b/src/main/java/cc/pulseapp/api/service/CaptchaService.java
@@ -1,5 +1,6 @@
package cc.pulseapp.api.service;
+import cc.pulseapp.api.common.EnvironmentUtils;
import cc.pulseapp.api.exception.impl.BadRequestException;
import cc.pulseapp.api.model.IGenericResponse;
import com.google.gson.JsonObject;
@@ -26,8 +27,6 @@ public final class CaptchaService {
* @throws BadRequestException if the response is invalid
*/
public void validateCaptcha(@NonNull String captchaResponse) throws BadRequestException {
- System.out.println("captchaResponse = " + captchaResponse);
-
JsonObject body = new JsonObject();
body.addProperty("secret", secretKey);
body.addProperty("response", captchaResponse);
@@ -35,8 +34,7 @@ public final class CaptchaService {
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.body(body)
.asJson();
- System.out.println("response = " + response.getBody().toPrettyString());
- if (/*EnvironmentUtils.isProduction() && */!response.getBody().getObject().getBoolean("success")) {
+ if (EnvironmentUtils.isProduction() && !response.getBody().getObject().getBoolean("success")) {
throw new BadRequestException(Error.CAPTCHA_INVALID);
}
}
diff --git a/src/main/java/cc/pulseapp/api/service/UserService.java b/src/main/java/cc/pulseapp/api/service/UserService.java
index e5899fc..cd1b385 100644
--- a/src/main/java/cc/pulseapp/api/service/UserService.java
+++ b/src/main/java/cc/pulseapp/api/service/UserService.java
@@ -10,6 +10,9 @@ import cc.pulseapp.api.model.user.TFAProfile;
import cc.pulseapp.api.model.user.User;
import cc.pulseapp.api.model.user.UserDTO;
import cc.pulseapp.api.model.user.UserFlag;
+import cc.pulseapp.api.model.user.device.BrowserType;
+import cc.pulseapp.api.model.user.device.Device;
+import cc.pulseapp.api.model.user.device.DeviceType;
import cc.pulseapp.api.model.user.input.CompleteOnboardingInput;
import cc.pulseapp.api.model.user.input.DisableTFAInput;
import cc.pulseapp.api.model.user.input.EnableTFAInput;
@@ -21,6 +24,9 @@ import cc.pulseapp.api.repository.UserRepository;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.NonNull;
+import nl.basjes.parse.useragent.UserAgent;
+import nl.basjes.parse.useragent.UserAgentAnalyzer;
+import org.apache.commons.lang3.EnumUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -35,6 +41,12 @@ import java.util.concurrent.TimeUnit;
*/
@Service
public final class UserService {
+ private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer
+ .newBuilder()
+ .useJava8CompatibleCaching()
+ .withCache(10000)
+ .build();
+
/**
* The auth service to use.
*/
@@ -60,11 +72,6 @@ public final class UserService {
*/
@NonNull private final TFAService tfaService;
- /**
- * The captcha service to use.
- */
- @NonNull private final CaptchaService captchaService;
-
/**
* The user repository to use.
*/
@@ -89,14 +96,13 @@ public final class UserService {
@Autowired
public UserService(@NonNull AuthService authService, @NonNull SnowflakeService snowflakeService,
@NonNull OrganizationService orgService, @NonNull StatusPageService statusPageService,
- @NonNull TFAService tfaService, @NonNull CaptchaService captchaService,
- @NonNull UserRepository userRepository, @NonNull SessionRepository sessionRepository) {
+ @NonNull TFAService tfaService, @NonNull UserRepository userRepository,
+ @NonNull SessionRepository sessionRepository) {
this.authService = authService;
this.snowflakeService = snowflakeService;
this.orgService = orgService;
this.statusPageService = statusPageService;
this.tfaService = tfaService;
- this.captchaService = captchaService;
this.userRepository = userRepository;
this.sessionRepository = sessionRepository;
}
@@ -245,6 +251,31 @@ public final class UserService {
userRepository.save(user);
}
+ /**
+ * Get the devices logged into
+ * the authenticated user.
+ *
+ * @return the devices
+ */
+ @NonNull
+ public List getDevices() {
+ List devices = new ArrayList<>();
+ User user = authService.getAuthenticatedUser();
+ for (Session session : sessionRepository.findAllByUserSnowflake(user.getSnowflake())) {
+ UserAgent.ImmutableUserAgent userAgent = userAgentAnalyzer.parse(session.getLocation().getUserAgent());
+ DeviceType deviceType = EnumUtils.getEnum(DeviceType.class, userAgent.get("DeviceClass").getValue().toUpperCase());
+ BrowserType browserType = EnumUtils.getEnum(BrowserType.class, userAgent.get("AgentName").getValue().toUpperCase());
+ if (deviceType == null) {
+ deviceType = DeviceType.UNKNOWN;
+ }
+ if (browserType == null) {
+ browserType = BrowserType.UNKNOWN;
+ }
+ devices.add(Device.fromSession(session, deviceType, browserType, new Date(snowflakeService.extractCreationTime(session.getSnowflake()))));
+ }
+ return devices;
+ }
+
/**
* Logout the user.
*/