feat(config): make profile and activity text length limits configurable
Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
parent
c84377b05a
commit
a7d38f7f8a
20 changed files with 274 additions and 18 deletions
|
|
@ -1,5 +1,9 @@
|
|||
package net.javahippie.fitpub.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.javahippie.fitpub.service.ActivityDescriptionValidationService;
|
||||
import net.javahippie.fitpub.service.ActivityTitleValidationService;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
|
@ -10,8 +14,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||
*/
|
||||
@Controller
|
||||
@RequestMapping("/activities")
|
||||
@RequiredArgsConstructor
|
||||
public class ActivitiesViewController {
|
||||
|
||||
private final ActivityDescriptionValidationService activityDescriptionValidationService;
|
||||
private final ActivityTitleValidationService activityTitleValidationService;
|
||||
|
||||
/**
|
||||
* Show activities list page
|
||||
*/
|
||||
|
|
@ -24,7 +32,9 @@ public class ActivitiesViewController {
|
|||
* Show activity upload page
|
||||
*/
|
||||
@GetMapping("/upload")
|
||||
public String uploadActivity() {
|
||||
public String uploadActivity(Model model) {
|
||||
model.addAttribute("activityTitleMaxLength", activityTitleValidationService.getMaxLength());
|
||||
model.addAttribute("activityDescriptionMaxLength", activityDescriptionValidationService.getMaxLength());
|
||||
return "activities/upload";
|
||||
}
|
||||
|
||||
|
|
@ -41,8 +51,10 @@ public class ActivitiesViewController {
|
|||
* Show activity edit page
|
||||
*/
|
||||
@GetMapping("/{id}/edit")
|
||||
public String editActivity(@PathVariable String id) {
|
||||
public String editActivity(@PathVariable String id, Model model) {
|
||||
// The activity data will be loaded via JavaScript API calls
|
||||
model.addAttribute("activityTitleMaxLength", activityTitleValidationService.getMaxLength());
|
||||
model.addAttribute("activityDescriptionMaxLength", activityDescriptionValidationService.getMaxLength());
|
||||
return "activities/edit";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import net.javahippie.fitpub.model.entity.PrivacyZone;
|
|||
import net.javahippie.fitpub.model.entity.User;
|
||||
import net.javahippie.fitpub.repository.FollowRepository;
|
||||
import net.javahippie.fitpub.repository.UserRepository;
|
||||
import net.javahippie.fitpub.service.ActivityDescriptionValidationService;
|
||||
import net.javahippie.fitpub.service.ActivityFileService;
|
||||
import net.javahippie.fitpub.service.ActivityImageService;
|
||||
import net.javahippie.fitpub.service.ActivityPostProcessingService;
|
||||
import net.javahippie.fitpub.service.ActivityTitleValidationService;
|
||||
import net.javahippie.fitpub.service.FederationService;
|
||||
import net.javahippie.fitpub.service.WeatherService;
|
||||
import net.javahippie.fitpub.service.FitFileService;
|
||||
|
|
@ -48,6 +50,8 @@ import java.util.UUID;
|
|||
@Slf4j
|
||||
public class ActivityController {
|
||||
|
||||
private final ActivityDescriptionValidationService activityDescriptionValidationService;
|
||||
private final ActivityTitleValidationService activityTitleValidationService;
|
||||
private final ActivityFileService activityFileService;
|
||||
private final FitFileService fitFileService;
|
||||
private final UserRepository userRepository;
|
||||
|
|
@ -153,6 +157,9 @@ public class ActivityController {
|
|||
) {
|
||||
log.info("User {} uploading activity file: {}", userDetails.getUsername(), file.getOriginalFilename());
|
||||
|
||||
activityTitleValidationService.validate(request.getTitle());
|
||||
activityDescriptionValidationService.validate(request.getDescription());
|
||||
|
||||
User user = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
|
|
@ -311,6 +318,9 @@ public class ActivityController {
|
|||
|
||||
UUID userId = getUserId(userDetails);
|
||||
|
||||
activityTitleValidationService.validate(request.getTitle());
|
||||
activityDescriptionValidationService.validate(request.getDescription());
|
||||
|
||||
try {
|
||||
Activity updated = fitFileService.updateActivity(
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package net.javahippie.fitpub.controller;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import net.javahippie.fitpub.model.dto.ApiError;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* Central exception mapping for REST APIs.
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ApiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ApiValidationException.class)
|
||||
public ResponseEntity<ApiError> handleApiValidation(ApiValidationException e) {
|
||||
return ResponseEntity.badRequest().body(new ApiError("BAD_REQUEST", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package net.javahippie.fitpub.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.javahippie.fitpub.service.UserBioValidationService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
|
@ -9,8 +11,11 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||
* Controller for user profile view pages.
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ProfileViewController {
|
||||
|
||||
private final UserBioValidationService userBioValidationService;
|
||||
|
||||
/**
|
||||
* Current user's profile page.
|
||||
* Shows own profile with edit capabilities.
|
||||
|
|
@ -34,6 +39,7 @@ public class ProfileViewController {
|
|||
@GetMapping("/profile/edit")
|
||||
public String editProfile(Model model) {
|
||||
model.addAttribute("pageTitle", "Edit Profile");
|
||||
model.addAttribute("bioMaxLength", userBioValidationService.getMaxLength());
|
||||
return "profile/edit";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import net.javahippie.fitpub.repository.RemoteActorRepository;
|
|||
import net.javahippie.fitpub.repository.UserRepository;
|
||||
import net.javahippie.fitpub.service.FederationService;
|
||||
import net.javahippie.fitpub.service.WebFingerClient;
|
||||
import net.javahippie.fitpub.service.UserBioValidationService;
|
||||
import net.javahippie.fitpub.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
|
@ -47,6 +48,7 @@ public class UserController {
|
|||
private final WebFingerClient webFingerClient;
|
||||
private final FederationService federationService;
|
||||
private final UserService userService;
|
||||
private final UserBioValidationService userBioValidationService;
|
||||
private final net.javahippie.fitpub.repository.ActivityPeakRepository activityPeakRepository;
|
||||
|
||||
@Value("${fitpub.base-url}")
|
||||
|
|
@ -101,6 +103,8 @@ public class UserController {
|
|||
) {
|
||||
log.info("User {} updating profile", userDetails.getUsername());
|
||||
|
||||
userBioValidationService.validate(request.getBio());
|
||||
|
||||
User user = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package net.javahippie.fitpub.exception;
|
||||
|
||||
/**
|
||||
* Exception for API request validation that depends on runtime configuration.
|
||||
*/
|
||||
public class ApiValidationException extends RuntimeException {
|
||||
|
||||
public ApiValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package net.javahippie.fitpub.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -18,10 +17,8 @@ import net.javahippie.fitpub.model.entity.Activity;
|
|||
public class ActivityUpdateRequest {
|
||||
|
||||
@NotNull(message = "Title is required")
|
||||
@Size(min = 1, max = 200, message = "Title must be between 1 and 200 characters")
|
||||
private String title;
|
||||
|
||||
@Size(max = 5000, message = "Description must not exceed 5000 characters")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "Visibility is required")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
package net.javahippie.fitpub.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -17,10 +15,8 @@ import net.javahippie.fitpub.model.entity.Activity;
|
|||
@AllArgsConstructor
|
||||
public class ActivityUploadRequest {
|
||||
|
||||
@Size(max = 200, message = "Title must not exceed 200 characters")
|
||||
private String title;
|
||||
|
||||
@Size(max = 5000, message = "Description must not exceed 5000 characters")
|
||||
private String description;
|
||||
|
||||
private Activity.Visibility visibility;
|
||||
|
|
|
|||
13
src/main/java/net/javahippie/fitpub/model/dto/ApiError.java
Normal file
13
src/main/java/net/javahippie/fitpub/model/dto/ApiError.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package net.javahippie.fitpub.model.dto;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Standard error payload for API responses.
|
||||
*/
|
||||
@Value
|
||||
public class ApiError {
|
||||
|
||||
String code;
|
||||
String message;
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ public class UserUpdateRequest {
|
|||
@Size(max = 100, message = "Display name must not exceed 100 characters")
|
||||
private String displayName;
|
||||
|
||||
@Size(max = 500, message = "Bio must not exceed 500 characters")
|
||||
private String bio;
|
||||
|
||||
@URL(message = "Avatar URL must be a valid URL")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Central validation policy for activity descriptions.
|
||||
*/
|
||||
@Service
|
||||
public class ActivityDescriptionValidationService {
|
||||
|
||||
private final int maxLength;
|
||||
|
||||
public ActivityDescriptionValidationService(
|
||||
@Value("${fitpub.activity.description.max-length:5000}") int maxLength
|
||||
) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public int getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
public void validate(String description) {
|
||||
if (description != null && description.length() > maxLength) {
|
||||
throw new ApiValidationException(
|
||||
"Description must not exceed " + maxLength + " characters"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Central validation policy for activity titles.
|
||||
*/
|
||||
@Service
|
||||
public class ActivityTitleValidationService {
|
||||
|
||||
private final int maxLength;
|
||||
|
||||
public ActivityTitleValidationService(
|
||||
@Value("${fitpub.activity.title.max-length:200}") int maxLength
|
||||
) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public int getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
public void validate(String title) {
|
||||
if (title != null && title.length() > maxLength) {
|
||||
throw new ApiValidationException(
|
||||
"Title must not exceed " + maxLength + " characters"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Central validation policy for user bio fields.
|
||||
*/
|
||||
@Service
|
||||
public class UserBioValidationService {
|
||||
|
||||
private final int maxLength;
|
||||
|
||||
public UserBioValidationService(@Value("${fitpub.user.bio.max-length:500}") int maxLength) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public int getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
public void validate(String bio) {
|
||||
if (bio != null && bio.length() > maxLength) {
|
||||
throw new ApiValidationException("Bio must not exceed " + maxLength + " characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,18 @@ fitpub:
|
|||
# Leave empty to allow open registration
|
||||
password: ${REGISTRATION_PASSWORD:}
|
||||
|
||||
# User settings
|
||||
user:
|
||||
bio:
|
||||
max-length: ${FITPUB_USER_BIO_MAX_LENGTH:500}
|
||||
|
||||
# Activity settings
|
||||
activity:
|
||||
title:
|
||||
max-length: ${FITPUB_ACTIVITY_TITLE_MAX_LENGTH:200}
|
||||
description:
|
||||
max-length: ${FITPUB_ACTIVITY_DESCRIPTION_MAX_LENGTH:5000}
|
||||
|
||||
# Storage settings
|
||||
storage:
|
||||
fit-files:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g., Morning Run"
|
||||
maxlength="200"
|
||||
th:attr="maxlength=${activityTitleMaxLength}"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
Please provide a title for your activity.
|
||||
|
|
@ -83,9 +83,9 @@
|
|||
name="description"
|
||||
rows="4"
|
||||
placeholder="Share details about your activity..."
|
||||
maxlength="5000"></textarea>
|
||||
th:attr="maxlength=${activityDescriptionMaxLength}"></textarea>
|
||||
<div class="form-text">
|
||||
<span id="descCharCount">0</span>/5000 characters
|
||||
<span id="descCharCount">0</span>/<span th:text="${activityDescriptionMaxLength}">5000</span> characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g., Morning Run"
|
||||
maxlength="200">
|
||||
th:attr="maxlength=${activityTitleMaxLength}">
|
||||
<div class="invalid-feedback">
|
||||
Please provide a title for your activity.
|
||||
</div>
|
||||
|
|
@ -126,9 +126,9 @@
|
|||
name="description"
|
||||
rows="4"
|
||||
placeholder="Share details about your activity..."
|
||||
maxlength="5000"></textarea>
|
||||
th:attr="maxlength=${activityDescriptionMaxLength}"></textarea>
|
||||
<div class="form-text">
|
||||
<span id="descCharCount">0</span>/5000 characters
|
||||
<span id="descCharCount">0</span>/<span th:text="${activityDescriptionMaxLength}">5000</span> characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,9 +39,10 @@
|
|||
<!-- Bio -->
|
||||
<div class="mb-3">
|
||||
<label for="bio" class="form-label">Bio</label>
|
||||
<textarea class="form-control" id="bio" name="bio" rows="4" maxlength="500"></textarea>
|
||||
<textarea class="form-control" id="bio" name="bio" rows="4"
|
||||
th:attr="maxlength=${bioMaxLength}"></textarea>
|
||||
<div class="form-text">
|
||||
<span id="bioCharCount">0</span>/500 characters
|
||||
<span id="bioCharCount">0</span>/<span th:text="${bioMaxLength}">500</span> characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@DisplayName("ActivityDescriptionValidationService Tests")
|
||||
class ActivityDescriptionValidationServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should allow descriptions up to configured max length")
|
||||
void shouldAllowDescriptionsUpToConfiguredMaxLength() {
|
||||
ActivityDescriptionValidationService service = new ActivityDescriptionValidationService(20);
|
||||
|
||||
assertDoesNotThrow(() -> service.validate("12345678901234567890"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject descriptions longer than configured max length")
|
||||
void shouldRejectDescriptionsLongerThanConfiguredMaxLength() {
|
||||
ActivityDescriptionValidationService service = new ActivityDescriptionValidationService(20);
|
||||
|
||||
assertThrows(ApiValidationException.class, () -> service.validate("123456789012345678901"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@DisplayName("ActivityTitleValidationService Tests")
|
||||
class ActivityTitleValidationServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should allow titles up to configured max length")
|
||||
void shouldAllowTitlesUpToConfiguredMaxLength() {
|
||||
ActivityTitleValidationService service = new ActivityTitleValidationService(10);
|
||||
|
||||
assertDoesNotThrow(() -> service.validate("1234567890"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject titles longer than configured max length")
|
||||
void shouldRejectTitlesLongerThanConfiguredMaxLength() {
|
||||
ActivityTitleValidationService service = new ActivityTitleValidationService(10);
|
||||
|
||||
assertThrows(ApiValidationException.class, () -> service.validate("12345678901"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package net.javahippie.fitpub.service;
|
||||
|
||||
import net.javahippie.fitpub.exception.ApiValidationException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@DisplayName("UserBioValidationService Tests")
|
||||
class UserBioValidationServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should allow bios up to configured max length")
|
||||
void shouldAllowBiosUpToConfiguredMaxLength() {
|
||||
UserBioValidationService service = new UserBioValidationService(12);
|
||||
|
||||
assertDoesNotThrow(() -> service.validate("123456789012"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject bios longer than configured max length")
|
||||
void shouldRejectBiosLongerThanConfiguredMaxLength() {
|
||||
UserBioValidationService service = new UserBioValidationService(12);
|
||||
|
||||
assertThrows(ApiValidationException.class, () -> service.validate("1234567890123"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue