Refactored API organization, added functionality for password resets.
This commit is contained in:
parent
d60f7142e8
commit
5d18da6ebe
|
@ -4,8 +4,18 @@ An HTTP/REST API powered by Java and Spring Boot. This API serves as the main en
|
|||
|
||||
## Development
|
||||
|
||||
To get started, follow these steps:
|
||||
1. Clone the repository, and open this project it in your editor. (Open the pom.xml file if you're using a maven-aware editor).
|
||||
2. Run `./gen_keys.d` to generate the keys that will be used by this application for signing JWT tokens. (Requires a D lang installation).
|
||||
3. Make sure the *gymboard-cdn* service is running locally.
|
||||
4. Boot up the project.
|
||||
|
||||
### Sample Data
|
||||
|
||||
To ease development, `nl.andrewlalis.gymboard_api.util.SampleDataLoader` will run on startup and populate the database with some sample entities. You can regenerate this data by manually deleting the database, and deleting the `.sample_data` marker file that's generated in the project directory.
|
||||
|
||||
You should have the *gymboard-cdn* project running when starting up this API, since the sample data includes videos that will be uploaded as part of some sample submissions.
|
||||
|
||||
## ULIDs
|
||||
|
||||
For entities that don't need a human-readable primary key (or keys), we choose to use [ULID](https://github.com/ulid/spec) strings, which are like UUIDs, but use a timestamp based preamble such that their values are monotonically increasing, and lexicographically ordered by creation time. The result is a pseudorandom string of 26 characters which appears random to a human, yet is efficient as a primary key. It's also near-impossible for automated systems to guess previous/next ids.
|
||||
|
|
|
@ -77,8 +77,20 @@
|
|||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<!-- TODO: Change this to "test" once the SampleDataLoader is refactored to the tests. -->
|
||||
<scope>compile</scope>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.1.214</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -46,7 +46,8 @@ public class SecurityConfig {
|
|||
"/exercises",
|
||||
"/leaderboards",
|
||||
"/gyms/**",
|
||||
"/submissions/**"
|
||||
"/submissions/**",
|
||||
"/auth/reset-password"
|
||||
).permitAll()
|
||||
.requestMatchers(// Allow the following POST endpoints to be public.
|
||||
HttpMethod.POST,
|
||||
|
@ -54,7 +55,8 @@ public class SecurityConfig {
|
|||
"/gyms/*/submissions/upload",
|
||||
"/auth/token",
|
||||
"/auth/register",
|
||||
"/auth/activate"
|
||||
"/auth/activate",
|
||||
"/auth/reset-password"
|
||||
).permitAll()
|
||||
// Everything else must be authenticated, just to be safe.
|
||||
.anyRequest().authenticated();
|
||||
|
|
|
@ -6,9 +6,9 @@ import jakarta.servlet.FilterChain;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.TokenAuthentication;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.TokenService;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.TokenAuthentication;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.TokenService;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record TokenResponse(String token) {}
|
|
@ -1,3 +0,0 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record UserActivationPayload(String code) {}
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseResponse;
|
||||
import nl.andrewlalis.gymboard_api.service.ExerciseService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.ExerciseService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
|
||||
import nl.andrewlalis.gymboard_api.service.GymService;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.GymResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.GymService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.service.LeaderboardService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.LeaderboardService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.dao;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.City;
|
||||
import nl.andrewlalis.gymboard_api.model.CityId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.City;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.CityId;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.Country;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Country;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package nl.andrewlalis.gymboard_api.dao;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.model.GymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.GymId;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.exercise;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dao.exercise;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.exercise;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dao.exercise;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||
|
||||
public record ExerciseResponse(
|
||||
String shortName,
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
public record ExerciseSubmissionPayload(
|
||||
String name,
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.GeoPoint;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.GeoPoint;
|
||||
|
||||
public record GeoPointResponse(
|
||||
double latitude,
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
|
||||
public record GymSimpleResponse(
|
||||
String countryCode,
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model.exercise;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model.exercise;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.model.exercise;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.model.exercise;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.math.BigDecimal;
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.service;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
|
@ -1,11 +1,11 @@
|
|||
package nl.andrewlalis.gymboard_api.service;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.GymResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.GymResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
|
@ -1,13 +1,13 @@
|
|||
package nl.andrewlalis.gymboard_api.service;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.model.LeaderboardTimeframe;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.LeaderboardTimeframe;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.service.cdn_client;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.service.cdn_client;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package nl.andrewlalis.gymboard_api.service.submission;
|
||||
package nl.andrewlalis.gymboard_api.domains.api.service.submission;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
|
@ -1,15 +1,13 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.*;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.TokenService;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.UserService;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.*;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.TokenService;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class AuthController {
|
||||
|
@ -80,4 +78,16 @@ public class AuthController {
|
|||
public UserResponse getMyUser(@AuthenticationPrincipal User user) {
|
||||
return new UserResponse(user);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/auth/reset-password")
|
||||
public ResponseEntity<Void> generatePasswordResetCode(@RequestParam String email) {
|
||||
userService.generatePasswordResetCode(email);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(path = "/auth/reset-password")
|
||||
public ResponseEntity<Void> resetPassword(@RequestBody PasswordResetPayload payload) {
|
||||
userService.resetUserPassword(payload);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.PasswordResetCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
public interface PasswordResetCodeRepository extends JpaRepository<PasswordResetCode, String> {
|
||||
@Modifying
|
||||
void deleteAllByCreatedAtBefore(LocalDateTime cutoff);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.UserActivationCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record PasswordResetPayload(String code, String newPassword) {}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record TokenCredentials(
|
||||
String email,
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record TokenResponse(String token) {}
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record UserActivationPayload(String code) {}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
public record UserCreationPayload(
|
||||
String email,
|
|
@ -1,6 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
|
||||
public record UserResponse(
|
||||
String id,
|
|
@ -0,0 +1,39 @@
|
|||
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_user_password_reset_code")
|
||||
public class PasswordResetCode {
|
||||
@Id
|
||||
@Column(nullable = false, updatable = false, length = 127)
|
||||
private String code;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private User user;
|
||||
|
||||
public PasswordResetCode() {}
|
||||
|
||||
public PasswordResetCode(String code, User user) {
|
||||
this.code = code;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
@ -72,6 +72,10 @@ public class User {
|
|||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
|
@ -1,15 +1,15 @@
|
|||
package nl.andrewlalis.gymboard_api.service.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.service;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.TokenAuthentication;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.TokenCredentials;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.TokenResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.Role;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.TokenAuthentication;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
|
@ -1,14 +1,18 @@
|
|||
package nl.andrewlalis.gymboard_api.service.auth;
|
||||
package nl.andrewlalis.gymboard_api.domains.auth.service;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserActivationPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserActivationCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.UserActivationCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.PasswordResetCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.PasswordResetPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserActivationPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserResponse;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserActivationCodeRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.PasswordResetCode;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
||||
import nl.andrewlalis.gymboard_api.util.StringGenerator;
|
||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -31,6 +35,7 @@ public class UserService {
|
|||
|
||||
private final UserRepository userRepository;
|
||||
private final UserActivationCodeRepository activationCodeRepository;
|
||||
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
||||
private final ULID ulid;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
|
@ -40,12 +45,15 @@ public class UserService {
|
|||
|
||||
public UserService(
|
||||
UserRepository userRepository,
|
||||
UserActivationCodeRepository activationCodeRepository, ULID ulid,
|
||||
UserActivationCodeRepository activationCodeRepository,
|
||||
PasswordResetCodeRepository passwordResetCodeRepository,
|
||||
ULID ulid,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
this.activationCodeRepository = activationCodeRepository;
|
||||
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
||||
this.ulid = ulid;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
|
@ -117,13 +125,74 @@ public class UserService {
|
|||
public UserResponse activateUser(UserActivationPayload payload) {
|
||||
UserActivationCode activationCode = activationCodeRepository.findByCode(payload.code())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
|
||||
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
||||
}
|
||||
User user = activationCode.getUser();
|
||||
user.setActivated(true);
|
||||
userRepository.save(user);
|
||||
if (!user.isActivated()) {
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
|
||||
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
||||
}
|
||||
user.setActivated(true);
|
||||
userRepository.save(user);
|
||||
}
|
||||
activationCodeRepository.delete(activationCode);
|
||||
return new UserResponse(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void generatePasswordResetCode(String email) {
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!user.isActivated()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
PasswordResetCode passwordResetCode = passwordResetCodeRepository.save(new PasswordResetCode(
|
||||
StringGenerator.randomString(127, StringGenerator.Alphabet.ALPHANUMERIC),
|
||||
user
|
||||
));
|
||||
|
||||
// Send email.
|
||||
String resetLink = webOrigin + "/password-reset?code=" + passwordResetCode.getCode();
|
||||
String emailContent = String.format(
|
||||
"""
|
||||
<p>Hello %s,</p>
|
||||
|
||||
<p>
|
||||
You've just requested to reset your password.
|
||||
</p>
|
||||
<p>
|
||||
Please click <a href="%s">here</a> to reset your password.
|
||||
</p>
|
||||
""",
|
||||
user.getName(),
|
||||
resetLink
|
||||
);
|
||||
MimeMessage msg = mailSender.createMimeMessage();
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8");
|
||||
helper.setFrom("Gymboard <noreply@gymboard.io>");
|
||||
helper.setSubject("Gymboard Account Password Reset");
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setText(emailContent, true);
|
||||
mailSender.send(msg);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Error sending user password reset email.", e);
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void resetUserPassword(PasswordResetPayload payload) {
|
||||
PasswordResetCode code = passwordResetCodeRepository.findById(payload.code())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusMinutes(30);
|
||||
if (code.getCreatedAt().isBefore(cutoff)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// TODO: Validate password.
|
||||
|
||||
code.getUser().setPasswordHash(passwordEncoder.encode(payload.newPassword()));
|
||||
passwordResetCodeRepository.delete(code);
|
||||
}
|
||||
}
|
|
@ -1,25 +1,25 @@
|
|||
package nl.andrewlalis.gymboard_api.util;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.dao.CityRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.CountryRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.RoleRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.City;
|
||||
import nl.andrewlalis.gymboard_api.model.Country;
|
||||
import nl.andrewlalis.gymboard_api.model.GeoPoint;
|
||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.UserService;
|
||||
import nl.andrewlalis.gymboard_api.service.cdn_client.CdnClient;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.CityRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.CountryRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.RoleRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.City;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Country;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.GeoPoint;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.Role;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.slf4j.Logger;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package nl.andrewlalis.gymboard_api.util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
|
||||
public class StringGenerator {
|
||||
public enum Alphabet {
|
||||
ALPHANUMERIC("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
|
||||
|
||||
public final String value;
|
||||
|
||||
Alphabet(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static String randomString(int length, Alphabet alphabet) {
|
||||
Random random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(alphabet.value.charAt(random.nextInt(alphabet.value.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -1,6 +1 @@
|
|||
spring.jpa.open-in-view=false
|
||||
|
||||
# TODO: Find a better way than dumping files into memory.
|
||||
spring.servlet.multipart.enabled=true
|
||||
spring.servlet.multipart.max-file-size=1GB
|
||||
spring.servlet.multipart.max-request-size=2GB
|
||||
|
|
|
@ -5,7 +5,6 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||
|
||||
@SpringBootTest
|
||||
class GymboardApiApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
|
||||
spring.mail.host=127.0.0.1
|
||||
spring.mail.port=1025
|
||||
spring.mail.protocol=smtp
|
||||
spring.mail.properties.mail.smtp.timeout=10000
|
||||
|
||||
app.auth.private-key-location=./private_key.der
|
||||
app.web-origin=http://localhost:9000
|
||||
app.cdn-origin=http://localhost:8082
|
Loading…
Reference in New Issue