diff --git a/pom.xml b/pom.xml index 7997679..2a39a8b 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/java/cc/pulseapp/api/PulseAPI.java b/src/main/java/cc/pulseapp/api/PulseAPI.java index d35e733..57bcdc9 100644 --- a/src/main/java/cc/pulseapp/api/PulseAPI.java +++ b/src/main/java/cc/pulseapp/api/PulseAPI.java @@ -5,6 +5,7 @@ import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import java.io.File; import java.nio.file.Files; @@ -14,7 +15,7 @@ import java.util.Objects; /** * @author Braydon */ -@SpringBootApplication +@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class) @Log4j2(topic = "PulseApp") public class PulseAPI { @SneakyThrows diff --git a/src/main/java/cc/pulseapp/api/controller/v1/AuthController.java b/src/main/java/cc/pulseapp/api/controller/v1/AuthController.java index 3e13d84..668fb96 100644 --- a/src/main/java/cc/pulseapp/api/controller/v1/AuthController.java +++ b/src/main/java/cc/pulseapp/api/controller/v1/AuthController.java @@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping(value = "/v1/auth", produces = MediaType.APPLICATION_JSON_VALUE) public final class AuthController { /** - * The user service to use. + * The auth service to use. */ @NonNull private final AuthService authService; diff --git a/src/main/java/cc/pulseapp/api/controller/v1/UserController.java b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java new file mode 100644 index 0000000..15acd4b --- /dev/null +++ b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java @@ -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 getUser() { + return ResponseEntity.ok(userService.getUser()); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/exception/impl/ResourceNotFoundException.java b/src/main/java/cc/pulseapp/api/exception/impl/ResourceNotFoundException.java new file mode 100644 index 0000000..d002fdd --- /dev/null +++ b/src/main/java/cc/pulseapp/api/exception/impl/ResourceNotFoundException.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/page/StatusPage.java b/src/main/java/cc/pulseapp/api/model/page/StatusPage.java index 958968b..f398ea5 100644 --- a/src/main/java/cc/pulseapp/api/model/page/StatusPage.java +++ b/src/main/java/cc/pulseapp/api/model/page/StatusPage.java @@ -34,12 +34,12 @@ public final class StatusPage { @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; /** - * 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; diff --git a/src/main/java/cc/pulseapp/api/model/user/User.java b/src/main/java/cc/pulseapp/api/model/user/User.java index a5732fb..4a88567 100644 --- a/src/main/java/cc/pulseapp/api/model/user/User.java +++ b/src/main/java/cc/pulseapp/api/model/user/User.java @@ -39,6 +39,11 @@ public final class User { */ @NonNull private final String passwordSalt; + /** + * The hash to the avatar of this user, if any. + */ + private final String avatar; + /** * The tier of this user. */ @@ -47,13 +52,22 @@ public final class User { /** * The flags for this user. */ - private final int flags; + private int flags; /** * The date this user last logged in. */ @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. * diff --git a/src/main/java/cc/pulseapp/api/service/AuthService.java b/src/main/java/cc/pulseapp/api/service/AuthService.java index 6cedd75..f495a58 100644 --- a/src/main/java/cc/pulseapp/api/service/AuthService.java +++ b/src/main/java/cc/pulseapp/api/service/AuthService.java @@ -5,6 +5,7 @@ import cc.pulseapp.api.common.HashUtils; import cc.pulseapp.api.common.RequestUtils; import cc.pulseapp.api.common.StringUtils; 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.user.Session; import cc.pulseapp.api.model.user.User; @@ -17,6 +18,7 @@ import cc.pulseapp.api.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.Base64; @@ -81,7 +83,7 @@ public final class AuthService { return generateSession(request, userRepository.save(new User( snowflakeService.generateSnowflake(), input.getEmail(), input.getUsername(), 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)); } + /** + * 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. * diff --git a/src/main/java/cc/pulseapp/api/service/UserService.java b/src/main/java/cc/pulseapp/api/service/UserService.java new file mode 100644 index 0000000..370eda1 --- /dev/null +++ b/src/main/java/cc/pulseapp/api/service/UserService.java @@ -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()))); + } +} \ No newline at end of file