added support for a new builds/detials page and endpoints

This commit is contained in:
2025-12-26 20:52:47 -05:00
parent d8c1cf6799
commit 5d25176f0d
6 changed files with 66 additions and 7 deletions

View File

@@ -37,19 +37,29 @@ public class SecurityConfig {
.cors(c -> c.configurationSource(corsConfigurationSource())) .cors(c -> c.configurationSource(corsConfigurationSource()))
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// public
// ----------------------------
// Public
// ----------------------------
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/health", "/actuator/info").permitAll() .requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/api/products/gunbuilder/**").permitAll() .requestMatchers("/api/products/gunbuilder/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/builds").permitAll()
// protected // Public builds feed + public build detail (1 path segment only)
.requestMatchers(HttpMethod.GET, "/api/v1/builds").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/builds/*").permitAll()
// ----------------------------
// Protected
// ----------------------------
.requestMatchers("/api/v1/builds/me/**").authenticated() .requestMatchers("/api/v1/builds/me/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// everything else (adjust later as you lock down)
// Everything else (adjust later as you lock down)
.anyRequest().permitAll() .anyRequest().permitAll()
) )
// run JWT before AnonymousAuth sets principal="anonymousUser" // run JWT before AnonymousAuth sets principal="anonymousUser"
.addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class);

View File

@@ -5,9 +5,9 @@ import group.goforward.battlbuilder.web.dto.BuildDto;
import group.goforward.battlbuilder.web.dto.BuildFeedCardDto; import group.goforward.battlbuilder.web.dto.BuildFeedCardDto;
import group.goforward.battlbuilder.web.dto.BuildSummaryDto; import group.goforward.battlbuilder.web.dto.BuildSummaryDto;
import group.goforward.battlbuilder.web.dto.UpdateBuildRequest; import group.goforward.battlbuilder.web.dto.UpdateBuildRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -34,6 +34,15 @@ public class BuildV1Controller {
return ResponseEntity.ok(buildService.listPublicBuilds(limit == null ? 50 : limit)); return ResponseEntity.ok(buildService.listPublicBuilds(limit == null ? 50 : limit));
} }
/**
* Public build detail for /builds/{uuid}
* GET /api/v1/builds/{uuid}
*/
@GetMapping("/{uuid}")
public ResponseEntity<BuildDto> getPublicBuild(@PathVariable("uuid") UUID uuid) {
return ResponseEntity.ok(buildService.getPublicBuild(uuid));
}
/** /**
* Vault builds (authenticated user). * Vault builds (authenticated user).
* GET /api/v1/builds/me?limit=100 * GET /api/v1/builds/me?limit=100
@@ -75,7 +84,6 @@ public class BuildV1Controller {
return ResponseEntity.ok(buildService.updateMyBuild(uuid, req)); return ResponseEntity.ok(buildService.updateMyBuild(uuid, req));
} }
/** /**
* Delete a build (authenticated user; must own build). * Delete a build (authenticated user; must own build).
* DELETE /api/v1/builds/me/{uuid} * DELETE /api/v1/builds/me/{uuid}

View File

@@ -17,5 +17,8 @@ public interface BuildRepository extends JpaRepository<Build, Integer> {
Page<Build> findByUserIdAndDeletedAtIsNullOrderByUpdatedAtDesc(Integer userId, Pageable pageable); Page<Build> findByUserIdAndDeletedAtIsNullOrderByUpdatedAtDesc(Integer userId, Pageable pageable);
Optional<Build> findByUuidAndIsPublicTrueAndDeletedAtIsNull(UUID uuid);
Optional<Build> findByUuidAndDeletedAtIsNull(UUID uuid); Optional<Build> findByUuidAndDeletedAtIsNull(UUID uuid);
} }

View File

@@ -20,5 +20,7 @@ public interface BuildService {
BuildDto updateMyBuild(UUID uuid, UpdateBuildRequest req); BuildDto updateMyBuild(UUID uuid, UpdateBuildRequest req);
BuildDto getPublicBuild(UUID uuid);
void deleteMyBuild(UUID uuid); void deleteMyBuild(UUID uuid);
} }

View File

@@ -112,6 +112,31 @@ public class BuildServiceImpl implements BuildService {
.toList(); .toList();
} }
// ---------------------------
// Public build detail (/builds/{uuid})
// GET /api/v1/builds/public/{uuid}
// ---------------------------
@Override
public BuildDto getPublicBuild(UUID uuid) {
if (uuid == null) throw new IllegalArgumentException("uuid is required");
Build build = buildRepository.findByUuidAndDeletedAtIsNull(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Build not found"));
// Only allow public builds here (and not deleted)
if (!Boolean.TRUE.equals(build.getIsPublic())) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Build not found");
}
List<BuildItem> items = buildItemRepository.findByBuild_Id(build.getId());
BuildDto dto = toBuildDto(build, items);
hydrateBuildDtoItems(dto); // keep consistent with getMyBuild
return dto;
}
// --------------------------- // ---------------------------
// Vault list (/builds/me) // Vault list (/builds/me)
// --------------------------- // ---------------------------

View File

@@ -7,7 +7,18 @@ public interface EmailService {
EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody); EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody);
EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody, String templateKey); // ✅ convenience overload for templates
default EmailRequest sendEmailHtml(
String recipient,
String subject,
String htmlBody,
String textBody,
String templateKey
) {
EmailRequest req = sendEmailHtml(recipient, subject, htmlBody, textBody);
req.setTemplateKey(templateKey);
return req;
}
void deleteById(Integer id); void deleteById(Integer id);
} }