Added report and voting entities, and api status controller.

This commit is contained in:
Andrew Lalis 2023-02-08 08:30:16 +01:00
parent 080c44c467
commit aa843227d2
28 changed files with 300 additions and 98 deletions

View File

@ -0,0 +1,15 @@
package nl.andrewlalis.gymboard_api;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class StatusController {
@GetMapping(path = "/status")
public ResponseEntity<?> getServiceStatus() {
return ResponseEntity.ok(Map.of("online", true));
}
}

View File

@ -47,7 +47,10 @@ public class SecurityConfig {
"/leaderboards", "/leaderboards",
"/gyms/**", "/gyms/**",
"/submissions/**", "/submissions/**",
"/auth/reset-password" "/auth/reset-password",
"/auth/users/*",
"/auth/users/*/followers",
"/auth/users/*/following"
).permitAll() ).permitAll()
.requestMatchers(// Allow the following POST endpoints to be public. .requestMatchers(// Allow the following POST endpoints to be public.
HttpMethod.POST, HttpMethod.POST,

View File

@ -1,8 +1,8 @@
package nl.andrewlalis.gymboard_api.domains.api.controller; package nl.andrewlalis.gymboard_api.domains.api.controller;
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId; 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.SubmissionPayload;
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.dto.GymResponse; 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.GymService;
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
@ -32,15 +32,15 @@ public class GymController {
} }
@GetMapping(path = "/recent-submissions") @GetMapping(path = "/recent-submissions")
public List<ExerciseSubmissionResponse> getRecentSubmissions(@PathVariable String compoundId) { public List<SubmissionResponse> getRecentSubmissions(@PathVariable String compoundId) {
return gymService.getRecentSubmissions(CompoundGymId.parse(compoundId)); return gymService.getRecentSubmissions(CompoundGymId.parse(compoundId));
} }
@PostMapping(path = "/submissions") @PostMapping(path = "/submissions")
public ExerciseSubmissionResponse createSubmission( public SubmissionResponse createSubmission(
@PathVariable String compoundId, @PathVariable String compoundId,
@AuthenticationPrincipal User user, @AuthenticationPrincipal User user,
@RequestBody ExerciseSubmissionPayload payload @RequestBody SubmissionPayload payload
) { ) {
return submissionService.createSubmission(CompoundGymId.parse(compoundId), user.getId(), payload); return submissionService.createSubmission(CompoundGymId.parse(compoundId), user.getId(), payload);
} }

View File

@ -1,6 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.api.controller; package nl.andrewlalis.gymboard_api.domains.api.controller;
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.service.LeaderboardService; import nl.andrewlalis.gymboard_api.domains.api.service.LeaderboardService;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -21,7 +21,7 @@ public class LeaderboardController {
} }
@GetMapping @GetMapping
public Page<ExerciseSubmissionResponse> getLeaderboard( public Page<SubmissionResponse> getLeaderboard(
@RequestParam(name = "exercise") Optional<String> exerciseShortName, @RequestParam(name = "exercise") Optional<String> exerciseShortName,
@RequestParam(name = "gyms") Optional<String> gymCompoundIdsString, @RequestParam(name = "gyms") Optional<String> gymCompoundIdsString,
@RequestParam(name = "t") Optional<String> timeframe, @RequestParam(name = "t") Optional<String> timeframe,

View File

@ -1,6 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.api.controller; package nl.andrewlalis.gymboard_api.domains.api.controller;
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -17,7 +17,7 @@ public class SubmissionController {
} }
@GetMapping(path = "/{submissionId}") @GetMapping(path = "/{submissionId}")
public ExerciseSubmissionResponse getSubmission(@PathVariable String submissionId) { public SubmissionResponse getSubmission(@PathVariable String submissionId) {
return submissionService.getSubmission(submissionId); return submissionService.getSubmission(submissionId);
} }
} }

View File

@ -1,6 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.api.dao.exercise; package nl.andrewlalis.gymboard_api.domains.api.dao;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;

View File

@ -1,10 +0,0 @@
package nl.andrewlalis.gymboard_api.domains.api.dao.exercise;
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;
@Repository
public interface ExerciseSubmissionRepository extends JpaRepository<ExerciseSubmission, String>, JpaSpecificationExecutor<ExerciseSubmission> {
}

View File

@ -0,0 +1,10 @@
package nl.andrewlalis.gymboard_api.domains.api.dao.submission;
import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface SubmissionRepository extends JpaRepository<Submission, String>, JpaSpecificationExecutor<Submission> {
}

View File

@ -1,6 +1,6 @@
package nl.andrewlalis.gymboard_api.domains.api.dto; package nl.andrewlalis.gymboard_api.domains.api.dto;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
public record ExerciseResponse( public record ExerciseResponse(
String shortName, String shortName,

View File

@ -1,7 +1,10 @@
package nl.andrewlalis.gymboard_api.domains.api.dto; package nl.andrewlalis.gymboard_api.domains.api.dto;
public record ExerciseSubmissionPayload( import java.time.LocalDateTime;
public record SubmissionPayload(
String exerciseShortName, String exerciseShortName,
LocalDateTime performedAt,
float weight, float weight,
String weightUnit, String weightUnit,
int reps, int reps,

View File

@ -1,10 +1,10 @@
package nl.andrewlalis.gymboard_api.domains.api.dto; package nl.andrewlalis.gymboard_api.domains.api.dto;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserResponse; import nl.andrewlalis.gymboard_api.domains.auth.dto.UserResponse;
import nl.andrewlalis.gymboard_api.util.StandardDateFormatter; import nl.andrewlalis.gymboard_api.util.StandardDateFormatter;
public record ExerciseSubmissionResponse( public record SubmissionResponse(
String id, String id,
String createdAt, String createdAt,
GymSimpleResponse gym, GymSimpleResponse gym,
@ -17,7 +17,7 @@ public record ExerciseSubmissionResponse(
double metricWeight, double metricWeight,
int reps int reps
) { ) {
public ExerciseSubmissionResponse(ExerciseSubmission submission) { public SubmissionResponse(Submission submission) {
this( this(
submission.getId(), submission.getId(),
StandardDateFormatter.format(submission.getCreatedAt()), StandardDateFormatter.format(submission.getCreatedAt()),

View File

@ -1,4 +1,4 @@
package nl.andrewlalis.gymboard_api.domains.api.model.exercise; package nl.andrewlalis.gymboard_api.domains.api.model;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;

View File

@ -1,18 +1,18 @@
package nl.andrewlalis.gymboard_api.domains.api.model.exercise; package nl.andrewlalis.gymboard_api.domains.api.model.submission;
import jakarta.persistence.*; import jakarta.persistence.*;
import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import nl.andrewlalis.gymboard_api.domains.api.model.Gym; import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit; import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
import nl.andrewlalis.gymboard_api.domains.auth.model.User; import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Entity @Entity
@Table(name = "exercise_submission") @Table(name = "submission")
public class ExerciseSubmission { public class Submission {
@Id @Id
@Column(nullable = false, updatable = false, length = 26) @Column(nullable = false, updatable = false, length = 26)
private String id; private String id;
@ -53,9 +53,9 @@ public class ExerciseSubmission {
@Column(nullable = false) @Column(nullable = false)
private int reps; private int reps;
public ExerciseSubmission() {} public Submission() {}
public ExerciseSubmission(String id, Gym gym, Exercise exercise, User user, LocalDateTime performedAt, String videoFileId, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) { public Submission(String id, Gym gym, Exercise exercise, User user, LocalDateTime performedAt, String videoFileId, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) {
this.id = id; this.id = id;
this.gym = gym; this.gym = gym;
this.exercise = exercise; this.exercise = exercise;

View File

@ -0,0 +1,63 @@
package nl.andrewlalis.gymboard_api.domains.api.model.submission;
import jakarta.persistence.*;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "submission_report")
public class SubmissionReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
private LocalDateTime createdAt;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Submission submission;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@Column(nullable = false)
private String reason;
@Column(length = 1024)
private String description;
public SubmissionReport() {}
public SubmissionReport(Submission submission, User user, String reason, String description) {
this.submission = submission;
this.user = user;
this.reason = reason;
this.description = description;
}
public Long getId() {
return id;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public Submission getSubmission() {
return submission;
}
public User getUser() {
return user;
}
public String getReason() {
return reason;
}
public String getDescription() {
return description;
}
}

View File

@ -0,0 +1,40 @@
package nl.andrewlalis.gymboard_api.domains.api.model.submission;
import jakarta.persistence.*;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
@Entity
@Table(
name = "submission_vote",
uniqueConstraints = @UniqueConstraint(columnNames = {"submission_id", "user_id"})
)
public class SubmissionVote {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Submission submission;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private User user;
public SubmissionVote() {}
public SubmissionVote(Submission submission, User user) {
this.submission = submission;
this.user = user;
}
public Long getId() {
return id;
}
public Submission getSubmission() {
return submission;
}
public User getUser() {
return user;
}
}

View File

@ -1,7 +1,7 @@
package nl.andrewlalis.gymboard_api.domains.api.service; package nl.andrewlalis.gymboard_api.domains.api.service;
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseResponse;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;

View File

@ -1,10 +1,10 @@
package nl.andrewlalis.gymboard_api.domains.api.service; package nl.andrewlalis.gymboard_api.domains.api.service;
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId; 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.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.dto.GymResponse; 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.GymRepository;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
import nl.andrewlalis.gymboard_api.domains.api.model.Gym; import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
import nl.andrewlalis.gymboard_api.util.PredicateBuilder; import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -22,9 +22,9 @@ public class GymService {
private static final Logger log = LoggerFactory.getLogger(GymService.class); private static final Logger log = LoggerFactory.getLogger(GymService.class);
private final GymRepository gymRepository; private final GymRepository gymRepository;
private final ExerciseSubmissionRepository submissionRepository; private final SubmissionRepository submissionRepository;
public GymService(GymRepository gymRepository, ExerciseSubmissionRepository submissionRepository) { public GymService(GymRepository gymRepository, SubmissionRepository submissionRepository) {
this.gymRepository = gymRepository; this.gymRepository = gymRepository;
this.submissionRepository = submissionRepository; this.submissionRepository = submissionRepository;
} }
@ -37,7 +37,7 @@ public class GymService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<ExerciseSubmissionResponse> getRecentSubmissions(CompoundGymId id) { public List<SubmissionResponse> getRecentSubmissions(CompoundGymId id) {
Gym gym = gymRepository.findByCompoundId(id) Gym gym = gymRepository.findByCompoundId(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return submissionRepository.findAll((root, query, criteriaBuilder) -> { return submissionRepository.findAll((root, query, criteriaBuilder) -> {
@ -49,7 +49,7 @@ public class GymService {
.with(criteriaBuilder.equal(root.get("gym"), gym)) .with(criteriaBuilder.equal(root.get("gym"), gym))
.build(); .build();
}, PageRequest.of(0, 10)) }, PageRequest.of(0, 10))
.map(ExerciseSubmissionResponse::new) .map(SubmissionResponse::new)
.toList(); .toList();
} }
} }

View File

@ -1,13 +1,13 @@
package nl.andrewlalis.gymboard_api.domains.api.service; package nl.andrewlalis.gymboard_api.domains.api.service;
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId; 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.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository; 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.ExerciseRepository;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
import nl.andrewlalis.gymboard_api.domains.api.model.Gym; 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.LeaderboardTimeframe;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import nl.andrewlalis.gymboard_api.util.PredicateBuilder; import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -27,18 +27,18 @@ import java.util.Optional;
*/ */
@Service @Service
public class LeaderboardService { public class LeaderboardService {
private final ExerciseSubmissionRepository submissionRepository; private final SubmissionRepository submissionRepository;
private final ExerciseRepository exerciseRepository; private final ExerciseRepository exerciseRepository;
private final GymRepository gymRepository; private final GymRepository gymRepository;
public LeaderboardService(ExerciseSubmissionRepository submissionRepository, ExerciseRepository exerciseRepository, GymRepository gymRepository) { public LeaderboardService(SubmissionRepository submissionRepository, ExerciseRepository exerciseRepository, GymRepository gymRepository) {
this.submissionRepository = submissionRepository; this.submissionRepository = submissionRepository;
this.exerciseRepository = exerciseRepository; this.exerciseRepository = exerciseRepository;
this.gymRepository = gymRepository; this.gymRepository = gymRepository;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Page<ExerciseSubmissionResponse> getTopSubmissions( public Page<SubmissionResponse> getTopSubmissions(
Optional<String> exerciseShortName, Optional<String> exerciseShortName,
Optional<String> gymCompoundIdsString, Optional<String> gymCompoundIdsString,
Optional<String> optionalTimeframe, Optional<String> optionalTimeframe,
@ -68,7 +68,7 @@ public class LeaderboardService {
} }
return pb.build(); return pb.build();
}, pageable).map(ExerciseSubmissionResponse::new); }, pageable).map(SubmissionResponse::new);
} }
private List<Gym> parseGymCompoundIdsString(String s) { private List<Gym> parseGymCompoundIdsString(String s) {

View File

@ -1,15 +1,15 @@
package nl.andrewlalis.gymboard_api.domains.api.service.submission; package nl.andrewlalis.gymboard_api.domains.api.service.submission;
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository; 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.ExerciseRepository;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId; 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.SubmissionPayload;
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse; import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.model.Gym; import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit; import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository; import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
import nl.andrewlalis.gymboard_api.domains.auth.model.User; import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import nl.andrewlalis.gymboard_api.util.ULID; import nl.andrewlalis.gymboard_api.util.ULID;
@ -21,7 +21,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@ -35,25 +34,25 @@ public class ExerciseSubmissionService {
private final GymRepository gymRepository; private final GymRepository gymRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final ExerciseRepository exerciseRepository; private final ExerciseRepository exerciseRepository;
private final ExerciseSubmissionRepository exerciseSubmissionRepository; private final SubmissionRepository submissionRepository;
private final ULID ulid; private final ULID ulid;
public ExerciseSubmissionService(GymRepository gymRepository, public ExerciseSubmissionService(GymRepository gymRepository,
UserRepository userRepository, ExerciseRepository exerciseRepository, UserRepository userRepository, ExerciseRepository exerciseRepository,
ExerciseSubmissionRepository exerciseSubmissionRepository, SubmissionRepository submissionRepository,
ULID ulid) { ULID ulid) {
this.gymRepository = gymRepository; this.gymRepository = gymRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.exerciseRepository = exerciseRepository; this.exerciseRepository = exerciseRepository;
this.exerciseSubmissionRepository = exerciseSubmissionRepository; this.submissionRepository = submissionRepository;
this.ulid = ulid; this.ulid = ulid;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ExerciseSubmissionResponse getSubmission(String submissionId) { public SubmissionResponse getSubmission(String submissionId) {
ExerciseSubmission submission = exerciseSubmissionRepository.findById(submissionId) Submission submission = submissionRepository.findById(submissionId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new ExerciseSubmissionResponse(submission); return new SubmissionResponse(submission);
} }
/** /**
@ -64,7 +63,7 @@ public class ExerciseSubmissionService {
* @return The saved submission. * @return The saved submission.
*/ */
@Transactional @Transactional
public ExerciseSubmissionResponse createSubmission(CompoundGymId id, String userId, ExerciseSubmissionPayload payload) { public SubmissionResponse createSubmission(CompoundGymId id, String userId, SubmissionPayload payload) {
User user = userRepository.findById(userId) User user = userRepository.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
Gym gym = gymRepository.findByCompoundId(id) Gym gym = gymRepository.findByCompoundId(id)
@ -81,7 +80,7 @@ public class ExerciseSubmissionService {
if (weightUnit == WeightUnit.POUNDS) { if (weightUnit == WeightUnit.POUNDS) {
metricWeight = WeightUnit.toKilograms(rawWeight); metricWeight = WeightUnit.toKilograms(rawWeight);
} }
ExerciseSubmission submission = exerciseSubmissionRepository.saveAndFlush(new ExerciseSubmission( Submission submission = submissionRepository.saveAndFlush(new Submission(
ulid.nextULID(), ulid.nextULID(),
gym, gym,
exercise, exercise,
@ -93,6 +92,6 @@ public class ExerciseSubmissionService {
metricWeight, metricWeight,
payload.reps() payload.reps()
)); ));
return new ExerciseSubmissionResponse(submission); return new SubmissionResponse(submission);
} }
} }

View File

@ -0,0 +1,60 @@
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_report")
public class UserReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
private LocalDateTime createdAt;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
private User reportedBy;
@Column(nullable = false)
private String reason;
@Column(length = 1024)
private String description;
public UserReport(User user, User reportedBy, String reason, String description) {
this.user = user;
this.reportedBy = reportedBy;
this.reason = reason;
this.description = description;
}
public Long getId() {
return id;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public User getUser() {
return user;
}
public User getReportedBy() {
return reportedBy;
}
public String getReason() {
return reason;
}
public String getDescription() {
return description;
}
}

View File

@ -1,7 +1,7 @@
package nl.andrewlalis.gymboard_api.util.sample_data; package nl.andrewlalis.gymboard_api.util.sample_data;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import nl.andrewlalis.gymboard_api.util.CsvUtil; import nl.andrewlalis.gymboard_api.util.CsvUtil;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@ -1,12 +1,12 @@
package nl.andrewlalis.gymboard_api.util.sample_data; package nl.andrewlalis.gymboard_api.util.sample_data;
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository; 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.ExerciseRepository;
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository; import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
import nl.andrewlalis.gymboard_api.domains.api.model.Gym; import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit; import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise; import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission; import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient; import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService; import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository; import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
@ -29,13 +29,13 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
private final UserRepository userRepository; private final UserRepository userRepository;
private final ExerciseRepository exerciseRepository; private final ExerciseRepository exerciseRepository;
private final ExerciseSubmissionService submissionService; private final ExerciseSubmissionService submissionService;
private final ExerciseSubmissionRepository submissionRepository; private final SubmissionRepository submissionRepository;
private final ULID ulid; private final ULID ulid;
@Value("${app.cdn-origin}") @Value("${app.cdn-origin}")
private String cdnOrigin; private String cdnOrigin;
public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, ExerciseSubmissionService submissionService, ExerciseSubmissionRepository submissionRepository, ULID ulid) { public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, ExerciseSubmissionService submissionService, SubmissionRepository submissionRepository, ULID ulid) {
this.gymRepository = gymRepository; this.gymRepository = gymRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.exerciseRepository = exerciseRepository; this.exerciseRepository = exerciseRepository;
@ -94,7 +94,7 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
rawWeight = metricWeight.multiply(new BigDecimal("2.2046226218")); rawWeight = metricWeight.multiply(new BigDecimal("2.2046226218"));
} }
submissionRepository.save(new ExerciseSubmission( submissionRepository.save(new Submission(
ulid.nextULID(), ulid.nextULID(),
randomChoice(gyms, random), randomChoice(gyms, random),
randomChoice(exercises, random), randomChoice(exercises, random),

View File

@ -7,6 +7,8 @@ export interface User {
activated: boolean; activated: boolean;
email: string; email: string;
name: string; name: string;
personalDetails?: UserPersonalDetails;
preferences?: UserPreferences;
} }
export enum PersonSex { export enum PersonSex {
@ -20,6 +22,7 @@ export interface UserPersonalDetails {
birthDate?: string; birthDate?: string;
currentWeight?: number; currentWeight?: number;
currentWeightUnit?: number; currentWeightUnit?: number;
currentMetricWeight?: number;
sex: PersonSex; sex: PersonSex;
} }
@ -45,9 +48,18 @@ class AuthModule {
private tokenRefreshTimer?: Timeout; private tokenRefreshTimer?: Timeout;
/**
* Attempts to use the given credentials to obtain an access token for
* sending authenticated requests.
* @param authStore The auth store to use to update app state.
* @param credentials The credentials for logging in.
*/
public async login(authStore: AuthStoreType, credentials: TokenCredentials) { public async login(authStore: AuthStoreType, credentials: TokenCredentials) {
authStore.token = await this.fetchNewToken(credentials); authStore.token = await this.getNewToken(credentials);
authStore.user = await this.fetchMyUser(authStore); authStore.user = await this.getMyUser(authStore);
// Load the user's attached data right away too.
authStore.user.personalDetails = await this.getMyPersonalDetails(authStore);
authStore.user.preferences = await this.getMyPreferences(authStore);
clearTimeout(this.tokenRefreshTimer); clearTimeout(this.tokenRefreshTimer);
this.tokenRefreshTimer = setTimeout( this.tokenRefreshTimer = setTimeout(
@ -71,7 +83,7 @@ class AuthModule {
return response.data; return response.data;
} }
public async fetchNewToken(credentials: TokenCredentials): Promise<string> { public async getNewToken(credentials: TokenCredentials): Promise<string> {
const response = await api.post('/auth/token', credentials); const response = await api.post('/auth/token', credentials);
return response.data.token; return response.data.token;
} }
@ -81,12 +93,12 @@ class AuthModule {
authStore.token = response.data.token; authStore.token = response.data.token;
} }
public async fetchMyUser(authStore: AuthStoreType): Promise<User> { public async getMyUser(authStore: AuthStoreType): Promise<User> {
const response = await api.get('/auth/me', authStore.axiosConfig); const response = await api.get('/auth/me', authStore.axiosConfig);
return response.data; return response.data;
} }
public async fetchUser(userId: string, authStore: AuthStoreType): Promise<User> { public async getUser(userId: string, authStore: AuthStoreType): Promise<User> {
const response = await api.get(`/auth/users/${userId}`, authStore.axiosConfig); const response = await api.get(`/auth/users/${userId}`, authStore.axiosConfig);
return response.data; return response.data;
} }

View File

@ -6,7 +6,7 @@
{{ submission.exercise.displayName }} {{ submission.exercise.displayName }}
</q-item-label> </q-item-label>
<q-item-label caption> <q-item-label caption>
{{ submission.submitterName }} {{ submission.user.name }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side top> <q-item-section side top>

View File

@ -39,6 +39,12 @@ export default {
submit: 'Submit', submit: 'Submit',
}, },
}, },
userPage: {
notFound: {
title: 'User Not Found',
description: 'We couldn\'t find the user you\'re looking for.'
}
},
accountMenuItem: { accountMenuItem: {
logIn: 'Login', logIn: 'Login',
myAccount: 'My Account', myAccount: 'My Account',

View File

@ -6,7 +6,7 @@
{{ submission.exercise.displayName }} {{ submission.exercise.displayName }}
</h3> </h3>
<p>{{ submission.reps }} reps</p> <p>{{ submission.reps }} reps</p>
<p>by {{ submission.submitterName }}</p> <p>by <router-link :to="'/users/' + submission.user.id">{{ submission.user.name }}</router-link></p>
<p>At <router-link :to="getGymRoute(submission.gym)">{{ submission.gym.displayName }}</router-link></p> <p>At <router-link :to="getGymRoute(submission.gym)">{{ submission.gym.displayName }}</router-link></p>
<p> <p>
{{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }} {{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}

View File

@ -5,6 +5,10 @@
<p>{{ user?.email }}</p> <p>{{ user?.email }}</p>
<p v-if="isOwnUser">This is your account!</p> <p v-if="isOwnUser">This is your account!</p>
</StandardCenteredPage> </StandardCenteredPage>
<StandardCenteredPage v-if="userNotFound">
<h3>{{ $t('userPage.notFound.title') }}</h3>
<p>{{ $t('userPage.notFound.description') }}</p>
</StandardCenteredPage>
</q-page> </q-page>
</template> </template>
@ -29,10 +33,22 @@ const user: Ref<User | undefined> = ref();
*/ */
const isOwnUser = ref(false); const isOwnUser = ref(false);
/**
* Flag used to indicate whether we should show a "not found" message instead
* of the usual user page.
*/
const userNotFound = ref(false);
onMounted(async () => { onMounted(async () => {
const userId = route.params.userId as string; const userId = route.params.userId as string;
user.value = await api.auth.fetchUser(userId, authStore); try {
isOwnUser.value = user.value.id === authStore.user?.id; user.value = await api.auth.getUser(userId, authStore);
} catch (error: any) {
if (error.response && error.response.code === 404) {
userNotFound.value = true;
}
}
isOwnUser.value = authStore.loggedIn && user.value.id === authStore.user?.id;
}); });
</script> </script>

View File

@ -1,15 +0,0 @@
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
actions: {
increment() {
this.counter++;
},
},
});