/user/devices route
All checks were successful
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Successful in 47s

This commit is contained in:
Braydon 2024-09-20 02:42:11 -04:00
parent 562fd568e2
commit c5841402f3
8 changed files with 156 additions and 14 deletions

@ -104,6 +104,12 @@
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>7.28.1</version>
<scope>compile</scope>
</dependency>
<!-- Unirest -->
<dependency>

@ -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<List<Device>> getDevices() {
return ResponseEntity.ok(userService.getDevices());
}
/**
* A POST endpoint to logout the user.
*

@ -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
}

@ -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);
}
}

@ -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
}

@ -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.

@ -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);
}
}

@ -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<Device> getDevices() {
List<Device> 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.
*/