Improved user profile endpoints and validated usernames

This commit is contained in:
2026-04-19 12:50:54 -04:00
parent 1e97f75924
commit b99a6c50b0
7 changed files with 143 additions and 17 deletions

View File

@@ -1,14 +1,29 @@
package net.kpuig.concord.backend.controllers; package net.kpuig.concord.backend.controllers;
import org.springframework.beans.factory.annotation.Autowired; import java.net.URI;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import net.kpuig.concord.backend.datamodels.userprofile.GetAllUserProfilesResponse; import net.kpuig.concord.backend.datamodels.userprofile.GetAllUserProfilesResponse;
import net.kpuig.concord.backend.datamodels.userprofile.GetUserProfileResponse; import net.kpuig.concord.backend.datamodels.userprofile.GetUserProfileResponse;
import net.kpuig.concord.backend.datamodels.userprofile.PostUserProfileRequest;
import net.kpuig.concord.backend.datamodels.userprofile.PostUserProfileResponse;
import net.kpuig.concord.backend.services.UserProfileService; import net.kpuig.concord.backend.services.UserProfileService;
import org.springframework.web.bind.annotation.GetMapping; import net.kpuig.concord.backend.services.exceptions.BadRequestException;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
@@ -18,14 +33,61 @@ public class UserProfileController {
@Autowired @Autowired
private UserProfileService userProfileService; private UserProfileService userProfileService;
@Operation(summary = "Get all user profiles", description = "Retrieves a list of all user profiles")
@ApiResponse(responseCode = "200", description = "User profiles retrieved",
content = { @Content(mediaType = "application/json") })
@GetMapping("/") @GetMapping("/")
public GetAllUserProfilesResponse getAllUserProfiles() { public ResponseEntity<GetAllUserProfilesResponse> getAllUserProfiles() {
return userProfileService.getAllUserProfiles(); return ResponseEntity.ok(userProfileService.getAllUserProfiles());
} }
@GetMapping("/{id}") @Operation(summary = "Get user profile by ID or username", description = "Retrieves a user profile by its ID or username")
public GetUserProfileResponse getMethodName(@RequestParam String id) { @ApiResponses(value = {
return userProfileService.getUserProfile(id); @ApiResponse(responseCode = "200", description = "User profile retrieved",
content = { @Content(mediaType = "application/json") }),
@ApiResponse(responseCode = "404", description = "User profile not found")
})
@GetMapping("")
public ResponseEntity<GetUserProfileResponse> getUserProfileById(
@RequestParam(name = "id", required = false) Long id,
@RequestParam(name = "username", required = false) String username) {
if (id == null && username == null) { // Must provide at least one of them
return ResponseEntity.badRequest().build();
} else if (id != null && username != null) { // Both have been provided
return ResponseEntity.badRequest().build();
} else if (id != null) { // ID has been provided
try {
var response = userProfileService.getUserProfileById(id);
return ResponseEntity.ok(response);
} catch (NumberFormatException e) {
return ResponseEntity.notFound().build();
}
} else { // Username has been provided
try {
var response = userProfileService.getUserProfileByUsername(username);
return ResponseEntity.ok(response);
} catch (NumberFormatException e) {
return ResponseEntity.notFound().build();
}
}
}
@Operation(summary = "Create user profile", description = "Creates a new user profile")
@ApiResponse(responseCode = "201", description = "User profile created",
content = { @Content(mediaType = "application/json") })
@PostMapping("/")
public ResponseEntity<PostUserProfileResponse> createUserProfile(@RequestBody @Valid PostUserProfileRequest request) {
try {
var response = userProfileService.createUserProfile(request.getUsername());
URI location = UriComponentsBuilder
.fromPath("/user-profile")
.queryParam("username", response.getUsername())
.build()
.encode()
.toUri();
return ResponseEntity.created(location).body(response);
} catch (BadRequestException e) {
return ResponseEntity.badRequest().build();
}
} }
} }

View File

@@ -0,0 +1,10 @@
package net.kpuig.concord.backend.datamodels.userprofile;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class PostUserProfileRequest {
@NotBlank
private String username;
}

View File

@@ -0,0 +1,9 @@
package net.kpuig.concord.backend.datamodels.userprofile;
import lombok.Data;
@Data
public class PostUserProfileResponse {
private Long id;
private String username;
}

View File

@@ -1,6 +1,6 @@
package net.kpuig.concord.backend.repositories; package net.kpuig.concord.backend.repositories;
import java.util.List; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -9,5 +9,5 @@ import net.kpuig.concord.backend.datamodels.userprofile.UserProfile;
@Repository @Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> { public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
Optional<UserProfile> findByUsername(String username);
} }

View File

@@ -1,13 +1,14 @@
package net.kpuig.concord.backend.services; package net.kpuig.concord.backend.services;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import net.kpuig.concord.backend.datamodels.userprofile.GetAllUserProfilesResponse; import net.kpuig.concord.backend.datamodels.userprofile.GetAllUserProfilesResponse;
import net.kpuig.concord.backend.datamodels.userprofile.GetUserProfileResponse; import net.kpuig.concord.backend.datamodels.userprofile.GetUserProfileResponse;
import net.kpuig.concord.backend.datamodels.userprofile.PostUserProfileResponse;
import net.kpuig.concord.backend.datamodels.userprofile.UserProfile;
import net.kpuig.concord.backend.repositories.UserProfileRepository; import net.kpuig.concord.backend.repositories.UserProfileRepository;
import net.kpuig.concord.backend.services.exceptions.BadRequestException;
@Service @Service
public class UserProfileService { public class UserProfileService {
@@ -25,12 +26,42 @@ public class UserProfileService {
return response; return response;
} }
public GetUserProfileResponse getUserProfile(String id) { public GetUserProfileResponse getUserProfileById(Long id) {
GetUserProfileResponse response = new GetUserProfileResponse(); GetUserProfileResponse response = new GetUserProfileResponse();
userProfileRepository.findById(Long.parseLong(id)).ifPresent(userProfile -> { userProfileRepository.findById(id).ifPresentOrElse(userProfile -> {
response.setId(userProfile.getId()); response.setId(userProfile.getId());
response.setUsername(userProfile.getUsername()); response.setUsername(userProfile.getUsername());
}, () -> {
throw new net.kpuig.concord.backend.services.exceptions.NotFoundException("User profile not found");
}); });
return response; return response;
} }
public GetUserProfileResponse getUserProfileByUsername(String username) {
GetUserProfileResponse response = new GetUserProfileResponse();
userProfileRepository.findByUsername(username).ifPresentOrElse(userProfile -> {
response.setId(userProfile.getId());
response.setUsername(userProfile.getUsername());
}, () -> {
throw new net.kpuig.concord.backend.services.exceptions.NotFoundException("User profile not found");
});
return response;
}
public PostUserProfileResponse createUserProfile(String username) {
if (userProfileRepository.findByUsername(username).isPresent()) {
throw new BadRequestException("Username is already taken");
}
UserProfile userProfile = new UserProfile();
userProfile.setUsername(username);
userProfile = userProfileRepository.save(userProfile);
PostUserProfileResponse response = new PostUserProfileResponse();
response.setId(userProfile.getId());
response.setUsername(userProfile.getUsername());
return response;
}
} }

View File

@@ -0,0 +1,7 @@
package net.kpuig.concord.backend.services.exceptions;
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package net.kpuig.concord.backend.services.exceptions;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}