Add Spring Boot backend module for DPC community data API with account management#110
Add Spring Boot backend module for DPC community data API with account management#110
Conversation
…Flyway migration, tests, Dockerfile, and Docker Compose integration Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/bd6589fe-5971-41d2-842b-e5a071d82195 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…ey comparison, and fail-fast on missing API key Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/bd6589fe-5971-41d2-842b-e5a071d82195 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new dpc-api/ Spring Boot module to serve a community data REST API (starting with Factions) alongside the existing Next.js website, plus local-dev infrastructure via Docker Compose.
Changes:
- Introduces a Spring Boot 3.2.x + JPA + Flyway + PostgreSQL-backed API module with
/api/v1/factionsendpoints and API-key auth on write methods. - Adds Flyway migration + JPA entity/DTO/service/controller layers for faction ingestion and public reads.
- Adds Docker Compose services for
dpc-db(Postgres) anddpc-api, plus MockMvc + H2 integration tests.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| dpc-api/src/test/resources/application-test.yml | Test profile config for H2 + disables Flyway. |
| dpc-api/src/test/java/com/dansplugins/api/controller/FactionControllerTest.java | MockMvc integration tests for auth/validation/paging/read endpoints. |
| dpc-api/src/test/java/com/dansplugins/api/DpcApiApplicationTests.java | Basic Spring context load test. |
| dpc-api/src/main/resources/db/migration/V1__create_factions_table.sql | Flyway migration to create factions table + index. |
| dpc-api/src/main/resources/application.yml | Runtime configuration for Postgres/Flyway and API key property. |
| dpc-api/src/main/java/com/dansplugins/api/service/FactionService.java | Service layer for create/list/get faction(s). |
| dpc-api/src/main/java/com/dansplugins/api/repository/FactionRepository.java | Spring Data JPA repository for Faction. |
| dpc-api/src/main/java/com/dansplugins/api/filter/ApiKeyAuthFilter.java | API-key auth filter applied to write HTTP methods. |
| dpc-api/src/main/java/com/dansplugins/api/entity/Faction.java | JPA entity mapping and timestamp lifecycle hooks. |
| dpc-api/src/main/java/com/dansplugins/api/dto/FactionResponse.java | API response DTO mapping from entity. |
| dpc-api/src/main/java/com/dansplugins/api/dto/FactionRequest.java | Request DTO with Bean Validation constraints. |
| dpc-api/src/main/java/com/dansplugins/api/controller/FactionController.java | REST controller for create/list/get endpoints. |
| dpc-api/src/main/java/com/dansplugins/api/config/ApiKeyConfig.java | Fail-fast validation that API key is configured. |
| dpc-api/src/main/java/com/dansplugins/api/DpcApiApplication.java | Spring Boot application entrypoint. |
| dpc-api/pom.xml | Maven build definition and dependencies. |
| dpc-api/mvnw.cmd | Maven wrapper script (Windows). |
| dpc-api/mvnw | Maven wrapper script (Unix). |
| dpc-api/README.md | Module documentation (local dev, config, endpoints). |
| dpc-api/Dockerfile | Container image definition for the API. |
| dpc-api/.mvn/wrapper/maven-wrapper.properties | Maven wrapper distribution configuration. |
| compose.yml | Adds dpc-db and dpc-api services for local dev. |
| .gitignore | Ignores Maven build outputs and un-ignores dpc-api/pom.xml. |
dpc-api/Dockerfile
Outdated
| FROM eclipse-temurin:17-jre-alpine | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY target/dpc-api-*.jar app.jar |
There was a problem hiding this comment.
The image build expects a pre-built JAR at target/dpc-api-*.jar, but docker compose up --build will run docker build without running Maven first (and dpc-api/target/ is gitignored). Use a multi-stage Dockerfile that builds the JAR (e.g., run ./mvnw -DskipTests package in a builder stage) or adjust Compose to build the JAR before building the image.
| FROM eclipse-temurin:17-jre-alpine | |
| WORKDIR /app | |
| COPY target/dpc-api-*.jar app.jar | |
| FROM eclipse-temurin:17-jdk-alpine AS builder | |
| WORKDIR /build | |
| COPY mvnw pom.xml ./ | |
| COPY .mvn .mvn | |
| COPY src src | |
| RUN ./mvnw -DskipTests package | |
| FROM eclipse-temurin:17-jre-alpine | |
| WORKDIR /app | |
| COPY --from=builder /build/target/dpc-api-*.jar app.jar |
compose.yml
Outdated
| DB_NAME: dpc | ||
| DB_USERNAME: dpc | ||
| DB_PASSWORD: dpc | ||
| DPC_API_KEY: ${DPC_API_KEY:-changeme} |
There was a problem hiding this comment.
DPC_API_KEY defaults to changeme in Compose, which makes it easy to accidentally deploy/run with a known API key. Consider removing the default and failing fast (let the API refuse to start when the key isn’t provided), or at minimum generate a random dev-only key and document it.
| DPC_API_KEY: ${DPC_API_KEY:-changeme} | |
| DPC_API_KEY: ${DPC_API_KEY:?DPC_API_KEY must be set} |
| CREATE TABLE factions ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| name VARCHAR(255) NOT NULL, |
There was a problem hiding this comment.
gen_random_uuid() requires the pgcrypto extension in Postgres; on a fresh postgres:16-alpine instance this migration can fail unless the extension is enabled. Either add CREATE EXTENSION IF NOT EXISTS pgcrypto; in the migration or remove the DB-side default if UUIDs are generated by Hibernate.
|
|
||
| @Column(name = "member_count", nullable = false) | ||
| private int memberCount; | ||
|
|
There was a problem hiding this comment.
The migration defines description as TEXT, but the entity maps it as a default String (typically VARCHAR(255)), and spring.jpa.hibernate.ddl-auto is set to validate. This can cause schema validation failures at startup. Align the mappings (e.g., mark the field as @Lob / @Column(columnDefinition = "TEXT") or change the column type in the migration).
| @Column(columnDefinition = "TEXT") |
| public void validateApiKey() { | ||
| if (apiKey == null || apiKey.isBlank()) { | ||
| throw new IllegalStateException( | ||
| "DPC_API_KEY environment variable must be set. " |
There was a problem hiding this comment.
The startup validation message says the DPC_API_KEY environment variable must be set, but the actual requirement is that dpc.api.key is configured (which could come from other property sources). Consider wording this as “dpc.api.key must be configured (e.g., via DPC_API_KEY env var)” to avoid misleading operators.
| "DPC_API_KEY environment variable must be set. " | |
| "`dpc.api.key` must be configured (e.g., via DPC_API_KEY environment variable). " |
| ddl-auto: validate | ||
| open-in-view: false | ||
| flyway: | ||
| enabled: true |
There was a problem hiding this comment.
Pageable parameters are fully client-controlled here; without a configured max page size, callers can request extremely large size values and force expensive DB queries. Consider setting spring.data.web.pageable.max-page-size (and possibly a default page size) in application.yml to put an upper bound on size.
| enabled: true | |
| enabled: true | |
| data: | |
| web: | |
| pageable: | |
| default-page-size: 20 | |
| max-page-size: 200 |
| <dependency> | ||
| <groupId>org.flywaydb</groupId> | ||
| <artifactId>flyway-core</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.postgresql</groupId> | ||
| <artifactId>postgresql</artifactId> | ||
| <scope>runtime</scope> | ||
| </dependency> |
There was a problem hiding this comment.
With Spring Boot 3.2.x the managed Flyway version is in the 10.x line, where several databases (including PostgreSQL) require an additional flyway-database-postgresql dependency. With only flyway-core present, Flyway can fail at runtime with an “Unsupported Database: PostgreSQL …” error. Add the PostgreSQL Flyway database module (or pin Flyway to a version where Postgres support is bundled).
|
@copilot address comments |
…emove gen_random_uuid, TEXT column type, pageable limits, flyway-database-postgresql Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/b23ddfca-6a02-4589-9023-ab80ea2b9f0b Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
All 7 review comments addressed in
All 9 tests pass. |
|
@copilot How do operators get an API key are we gonna have account creation in this PR? |
No account creation in this PR. The current auth model is a single shared secret: the person deploying the API sets If per-operator keys or self-service registration is needed down the road, that would be a separate feature (e.g., an |
|
@copilot the ability to auto register is a requirement; ensure this PR aligns with Dans-Plugins/Medieval-Factions#1959 |
- POST /api/v1/register: self-service API key registration (no auth required) - API keys stored as SHA-256 hashes in new api_keys table (V2 migration) - Auth filter validates against DB-stored keys instead of single env var - POST /api/v1/factions now accepts JSON array (batch sync) with upsert - Added serverIp and discordLink optional fields to faction model - Removed single shared DPC_API_KEY requirement from compose.yml - All 14 tests pass Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/2aca508b-3b95-439a-a987-ad12f6e61b0b Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…6 private Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/2aca508b-3b95-439a-a987-ad12f6e61b0b Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
|
@copilot there should only be one register endpoint |
…v1/register Remove the legacy API key registration endpoint (POST /api/v1/register) in favor of the single account-based registration flow (POST /api/v1/accounts/register). Files removed: - RegisterController.java - RegisterControllerTest.java (8 tests) - RegisterRequest.java - RegisterResponse.java Files updated: - ApiKeyService: removed register() method (keep isValidKey/sha256) - ApiKeyServiceTest: refactored to 5 tests without register dependency - ApiKeyAuthFilter: removed /api/v1/register exemption - SecurityConfig: removed /api/v1/register permit - ApiKey entity: removed unused 2-arg constructor - FactionControllerTest: create test keys via AccountService - README.md: removed legacy register section 69 tests pass (down from 79 — 8 removed RegisterControllerTest, 2 removed ApiKeyServiceTest register tests). Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/240f05e4-a70e-4840-a80c-e060d766a4fb Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Consolidated to a single register endpoint in Removed: RegisterController, RegisterRequest, RegisterResponse, RegisterControllerTest (8 tests). Updated: SecurityConfig, ApiKeyAuthFilter, ApiKeyService (removed
Understood — since this is all new code, I'm not maintaining backwards compatibility with any prior API contract. The standalone |
| if (res.ok) { | ||
| setProfile(await res.json()) | ||
| } else if (res.status === 401) { | ||
| setToken(null) | ||
| localStorage.removeItem('dpc-token') | ||
| setError('Session expired. Please log in again.') | ||
| } |
There was a problem hiding this comment.
fetchProfile only handles res.ok and 401; for other non-OK statuses it silently does nothing, leaving the UI without feedback. Consider handling the else case by setting an error message (and optionally clearing profile) so failed requests don’t appear to succeed.
pages/account.tsx
Outdated
| const copyToClipboard = (text: string) => { | ||
| navigator.clipboard.writeText(text) | ||
| setSuccess('Copied to clipboard!') | ||
| setTimeout(() => setSuccess(null), 2000) | ||
| } |
There was a problem hiding this comment.
navigator.clipboard.writeText returns a Promise and can reject (e.g., insecure context/permissions). This call isn’t awaited or caught, which can cause unhandled promise rejections; handle the Promise and surface a failure message when copy isn’t available.
| private String sha256(String input) { | ||
| try { | ||
| MessageDigest digest = MessageDigest.getInstance("SHA-256"); | ||
| byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); | ||
| return HexFormat.of().formatHex(hash); | ||
| } catch (NoSuchAlgorithmException e) { | ||
| throw new IllegalStateException("SHA-256 not available", e); | ||
| } |
There was a problem hiding this comment.
AccountService re-implements SHA-256 hashing logic that already exists in ApiKeyService, creating duplicated security-critical code paths. Consolidate hashing into a single shared method/service so changes (e.g., salting/encoding) can’t accidentally diverge.
| public JwtService( | ||
| @Value("${dpc.jwt.secret}") String secret, | ||
| @Value("${dpc.jwt.expiration:24h}") Duration expiration) { | ||
| this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); | ||
| this.expiration = expiration; | ||
| } |
There was a problem hiding this comment.
Keys.hmacShaKeyFor(...) will throw at runtime if dpc.jwt.secret is too short for the chosen HMAC algorithm; right now that failure will be a somewhat opaque startup exception. Add an explicit length/strength check with a clear error message so misconfiguration is easier to diagnose.
| ALTER TABLE api_keys ADD COLUMN owner_id UUID REFERENCES accounts(id); | ||
| CREATE INDEX idx_api_keys_owner_id ON api_keys (owner_id); |
There was a problem hiding this comment.
owner_id is nullable in the schema, but the application logic assumes API keys are account-owned (e.g., deletion checks key.getOwner() != null). Consider enforcing owner_id as NOT NULL (after any needed backfill) to prevent creating keys that can’t be managed through the account APIs.
|
@copilot address comments and ensure that the backend has a global exception handler |
…WT secret validation, NOT NULL owner_id, UI fixes Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/f424f365-3ce5-4f97-8046-71bbd9014ce0 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
All 5 review comments addressed and global exception handler added in
Global exception handler ( 69 tests pass, 0 CodeQL alerts. |
| public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
| http | ||
| .csrf(csrf -> csrf.disable()) | ||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
| .exceptionHandling(ex -> ex.authenticationEntryPoint( | ||
| new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) | ||
| .authorizeHttpRequests(auth -> auth | ||
| // Public endpoints | ||
| .requestMatchers(HttpMethod.POST, "/api/v1/accounts/register").permitAll() | ||
| .requestMatchers(HttpMethod.POST, "/api/v1/accounts/login").permitAll() | ||
| .requestMatchers(HttpMethod.GET, "/api/v1/factions/**").permitAll() | ||
| // Swagger/OpenAPI | ||
| .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll() | ||
| // Account endpoints require JWT auth | ||
| .requestMatchers("/api/v1/accounts/me/**").authenticated() | ||
| // Write endpoints require API key (handled by ApiKeyAuthFilter) | ||
| .requestMatchers(HttpMethod.POST, "/api/v1/factions").permitAll() | ||
| .anyRequest().permitAll() | ||
| ) | ||
| .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) | ||
| .addFilterAfter(apiKeyAuthFilter, JwtAuthFilter.class); |
There was a problem hiding this comment.
The frontend calls the API on a different origin by default (API_BASE falls back to http://localhost:8080 while the site runs on :3000). The API currently has no CORS configuration, so browser requests from the website will be blocked. Add explicit CORS support (e.g., enable http.cors() and provide a CorsConfigurationSource that allows the website origin(s) and required headers like Authorization and X-API-Key).
| if (WRITE_METHODS.contains(request.getMethod())) { | ||
| String providedKey = request.getHeader(API_KEY_HEADER); | ||
| if (providedKey == null || !apiKeyService.isValidKey(providedKey)) { | ||
| response.setStatus(HttpStatus.UNAUTHORIZED.value()); | ||
| response.setContentType("application/json"); | ||
| response.getWriter().write("{\"error\":\"Invalid or missing API key\"}"); | ||
| return; |
There was a problem hiding this comment.
This filter writes a hard-coded JSON body ("{"error":...}") directly to the response for invalid/missing API keys, which is inconsistent with the structured error format returned elsewhere (GlobalExceptionHandler). Consider routing this through Spring’s exception handling (e.g., throw an authentication/authorization exception) or use a shared error-response builder/ObjectMapper so clients get consistent fields (timestamp/status/message) and content-type/encoding are handled uniformly.
pages/account.tsx
Outdated
| <IconButton size="small" onClick={() => copyToClipboard(newApiKey)} sx={{ml: 1}}> | ||
| <ContentCopyIcon fontSize="small"/> | ||
| </IconButton> |
There was a problem hiding this comment.
The copy IconButton is missing an accessible label. Add an aria-label (and/or title) so screen readers can announce the action (e.g., "Copy API key to clipboard").
pages/account.tsx
Outdated
| <IconButton edge="end" onClick={() => handleDeleteApiKey(key.id)}> | ||
| <DeleteIcon/> | ||
| </IconButton> |
There was a problem hiding this comment.
The delete IconButton is missing an accessible label. Add an aria-label (and/or title) so assistive tech users know this deletes the API key.
|
@copilot address comments |
…-labels on IconButtons Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/b3ede653-7dcc-4c9f-907c-75f4a75ab9fb Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…redential support Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/b3ede653-7dcc-4c9f-907c-75f4a75ab9fb Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
All 4 review comments addressed in
69 tests pass, 0 CodeQL alerts. |
|
@copilot Ran into the following while building Docker image Add maven wrapper to prevent this |
Agent-Logs-Url: https://github.com/Dans-Plugins/dansplugins-dot-com/sessions/63e4fbe7-375f-4c5b-9376-2b3e80ea73aa Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
... Added the Maven wrapper JAR (v3.3.4) to |
Adds a
dpc-apiSpring Boot module providing RESTful endpoints for cross-server community data, starting with Medieval Factions. Minecraft servers running DPC plugins can POST faction data; the website (and anyone) can GET it. Aligned with Medieval Factions PR #1959.New module:
dpc-api/flyway-database-postgresql), Lombok, springdoc-openapi, JJWT 0.12.6, Java 17POST /api/v1/factions— batch faction sync (API key required); accepts a JSON array of factions, de-duplicates and upserts by(name, serverId)GET /api/v1/factions— paginated listing (public, default 20 / max 200 per page)GET /api/v1/factions/{id}— single faction (public)POST /api/v1/accounts/register— account registration (public); accepts{ "username": "...", "password": "..." }, returns JWT tokenPOST /api/v1/accounts/login— account login (public); accepts{ "username": "...", "password": "..." }, returns JWT token (supports headless Minecraft plugin auth via command line)GET /api/v1/accounts/me— account profile (JWT required); returns account details and linked API keysPOST /api/v1/accounts/me/api-keys— create API key linked to account (JWT required); accepts{ "serverName": "..." }, returns one-time raw keyDELETE /api/v1/accounts/me/api-keys/{keyId}— delete an API key (JWT required)X-API-Keyheader filter on write methods, validated against SHA-256 hashes in theapi_keystable; Spring Security with stateless JWT for account management endpoints. BCrypt password hashing for accounts. Single registration flow through account system (no standalone API key registration endpoint).V1__create_factions_table.sql,V2__create_api_keys_and_faction_fields.sql,V3__create_accounts_table.sql), UNIQUE constraint on(name, server_id),api_keys.owner_idNOT NULL FK toaccounts, entity uses@PrePersist/@PreUpdatelifecycle callbacks, UUIDs generated by HibernateTEXT), optionalserverIpanddiscordLink, timestamps; accounts with username/password (BCrypt), API keys linked to accounts viaowner_id@Getter/@Setter/@NoArgsConstructor/@RequiredArgsConstructoron entities, services, controllers, and filters to reduce boilerplate/swagger-ui.html, OpenAPI spec at/v3/api-docs,@Operation/@Tag/@SecuritySchemeannotations on all endpoints (both API key and JWT bearer token)@RestControllerAdvicewith handlers for validation errors (400), malformed JSON (400), type mismatches (400), and a catch-all (500) returning consistent JSON error responses.ApiKeyAuthFiltererror responses also use the same structured format (timestamp/status/error/message) for consistency.HashUtilconsolidates SHA-256 hashing used by bothAccountServiceandApiKeyServiceinto a single security-critical code pathdpc.jwt.secretmeets the minimum 256-bit (32-byte) length requirement for HS256, with a clear error message on misconfigurationdpc.cors.allowed-originsproperty (defaults to*), enabling the Next.js frontend to call the API from a different origin. AllowsAuthorization,X-API-Key, andContent-Typeheaders.maven-wrapper.jarin.mvn/wrapper/for reliable Docker builds on Alpine Linux without requiringcurlorwgetAccount management
owner_idNOT NULL foreign key/account(Next.js/MUI) with login, registration, profile view, and API key CRUD. Clipboard copy with error handling, proper error feedback for all non-OK HTTP responses, and accessiblearia-labelattributes on all interactive IconButtons.Infrastructure
dpc-db(PostgreSQL 16 Alpine) anddpc-apiservices with multi-stage Dockerfile (JDK builder + JRE runtime). Maven wrapper JAR included for offline builds.build-apijob inbuild.ymlruns./mvnw verifywith Java 17 (Temurin) on push/PR tomainanddevelopAccountControllerTest(20),FactionControllerTest(20),AccountServiceTest(8),FactionServiceTest(9),ApiKeyServiceTest(5),JwtServiceTest(6),DpcApiApplicationTests(1). Integration tests use MockMvc + H2 in-memory DB.✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.