/@me route

This commit is contained in:
Braydon 2024-09-16 21:26:17 -04:00
parent d326767c7c
commit ba473d1dae
9 changed files with 155 additions and 6 deletions

@ -68,6 +68,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Libraries --> <!-- Libraries -->
<dependency> <dependency>

@ -5,6 +5,7 @@ import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import java.io.File; import java.io.File;
import java.nio.file.Files; import java.nio.file.Files;
@ -14,7 +15,7 @@ import java.util.Objects;
/** /**
* @author Braydon * @author Braydon
*/ */
@SpringBootApplication @SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
@Log4j2(topic = "PulseApp") @Log4j2(topic = "PulseApp")
public class PulseAPI { public class PulseAPI {
@SneakyThrows @SneakyThrows

@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(value = "/v1/auth", produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/v1/auth", produces = MediaType.APPLICATION_JSON_VALUE)
public final class AuthController { public final class AuthController {
/** /**
* The user service to use. * The auth service to use.
*/ */
@NonNull private final AuthService authService; @NonNull private final AuthService authService;

@ -0,0 +1,43 @@
package cc.pulseapp.api.controller.v1;
import cc.pulseapp.api.model.user.UserDTO;
import cc.pulseapp.api.service.UserService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* This controller is responsible for
* handling user authentication requests.
*
* @author Braydon
*/
@RestController
@RequestMapping(value = "/v1/user", produces = MediaType.APPLICATION_JSON_VALUE)
public final class UserController {
/**
* The user service to use.
*/
@NonNull private final UserService userService;
@Autowired
public UserController(@NonNull UserService userService) {
this.userService = userService;
}
/**
* A GET endpoint to get the
* currently authenticated user.
*
* @return the currently authenticated user
*/
@PostMapping("/@me") @ResponseBody @NonNull
public ResponseEntity<UserDTO> getUser() {
return ResponseEntity.ok(userService.getUser());
}
}

@ -0,0 +1,19 @@
package cc.pulseapp.api.exception.impl;
import cc.pulseapp.api.model.IGenericResponse;
import lombok.NonNull;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* This exception is raised
* when a resource is not found.
*
* @author Braydon
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
public final class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(@NonNull IGenericResponse error) {
super(error.name());
}
}

@ -34,12 +34,12 @@ public final class StatusPage {
@NonNull private final String slug; @NonNull private final String slug;
/** /**
* The id to the logo of this status page, if any. * The hash to the logo of this status page, if any.
*/ */
private final String logo; private final String logo;
/** /**
* The id to the banner of this status page, if any. * The hash to the banner of this status page, if any.
*/ */
private final String banner; private final String banner;

@ -39,6 +39,11 @@ public final class User {
*/ */
@NonNull private final String passwordSalt; @NonNull private final String passwordSalt;
/**
* The hash to the avatar of this user, if any.
*/
private final String avatar;
/** /**
* The tier of this user. * The tier of this user.
*/ */
@ -47,13 +52,22 @@ public final class User {
/** /**
* The flags for this user. * The flags for this user.
*/ */
private final int flags; private int flags;
/** /**
* The date this user last logged in. * The date this user last logged in.
*/ */
@NonNull private Date lastLogin; @NonNull private Date lastLogin;
/**
* Add a flag to this user.
*
* @param flag the flag to add
*/
public void addFlag(@NonNull UserFlag flag) {
flags |= flag.ordinal();
}
/** /**
* Check if this user has a given flag. * Check if this user has a given flag.
* *

@ -5,6 +5,7 @@ import cc.pulseapp.api.common.HashUtils;
import cc.pulseapp.api.common.RequestUtils; import cc.pulseapp.api.common.RequestUtils;
import cc.pulseapp.api.common.StringUtils; import cc.pulseapp.api.common.StringUtils;
import cc.pulseapp.api.exception.impl.BadRequestException; import cc.pulseapp.api.exception.impl.BadRequestException;
import cc.pulseapp.api.exception.impl.ResourceNotFoundException;
import cc.pulseapp.api.model.IGenericResponse; import cc.pulseapp.api.model.IGenericResponse;
import cc.pulseapp.api.model.user.Session; import cc.pulseapp.api.model.user.Session;
import cc.pulseapp.api.model.user.User; import cc.pulseapp.api.model.user.User;
@ -17,6 +18,7 @@ import cc.pulseapp.api.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull; import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Base64; import java.util.Base64;
@ -81,7 +83,7 @@ public final class AuthService {
return generateSession(request, userRepository.save(new User( return generateSession(request, userRepository.save(new User(
snowflakeService.generateSnowflake(), input.getEmail(), input.getUsername(), snowflakeService.generateSnowflake(), input.getEmail(), input.getUsername(),
HashUtils.hash(salt, input.getPassword()), Base64.getEncoder().encodeToString(salt), HashUtils.hash(salt, input.getPassword()), Base64.getEncoder().encodeToString(salt),
UserTier.FREE, 0, now null, UserTier.FREE, 0, now
))); )));
} }
@ -111,6 +113,38 @@ public final class AuthService {
return generateSession(request, userRepository.save(user)); return generateSession(request, userRepository.save(user));
} }
/**
* Get the authenticated user.
*
* @return the authenticated user
* @throws ResourceNotFoundException if the user doesn't exist
*/
@NonNull
public User getAuthenticatedUser() throws ResourceNotFoundException {
Session session = (Session) SecurityContextHolder.getContext().getAuthentication().getCredentials();
return getUserFromSnowflake(session.getUserSnowflake());
}
/**
* Get a user from a snowflake, if the user exists.
*
* @param snowflake the snowflake of the user
* @return the user from the snowflake
* @throws BadRequestException if the snowflake is invalid
* @throws ResourceNotFoundException if the user doesn't exist
*/
@NonNull
public User getUserFromSnowflake(long snowflake) {
if (snowflake < 1L) {
throw new ResourceNotFoundException(Error.USER_NOT_FOUND);
}
User user = userRepository.findById(snowflake).orElse(null);
if (user == null) {
throw new ResourceNotFoundException(Error.USER_NOT_FOUND);
}
return user;
}
/** /**
* Generate an auth token for a user. * Generate an auth token for a user.
* *

@ -0,0 +1,34 @@
package cc.pulseapp.api.service;
import cc.pulseapp.api.model.user.User;
import cc.pulseapp.api.model.user.UserDTO;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @author Braydon
*/
@Service
public final class UserService {
@NonNull private final AuthService authService;
/**
* The service to use for snowflake timestamp extraction.
*/
@NonNull private final SnowflakeService snowflakeService;
@Autowired
public UserService(@NonNull AuthService authService, @NonNull SnowflakeService snowflakeService) {
this.authService = authService;
this.snowflakeService = snowflakeService;
}
@NonNull
public UserDTO getUser() {
User user = authService.getAuthenticatedUser();
return UserDTO.asDTO(user, new Date(snowflakeService.extractCreationTime(user.getSnowflake())));
}
}