Skip to content

Commit 8aa62a1

Browse files
authored
Merge: PR #47 from sameboat-platform/feat/week4-backend-notfound-badrequest-openapi-docs
This pull request introduces several backend improvements focused on error handling, endpoint hardening, documentation updates, and test coverage. The main highlights are the standardization of 404 (NOT_FOUND) error semantics, improved BAD_REQUEST mapping, the addition of a gated public user profile endpoint, and synchronization of OpenAPI and documentation with the current backend state.
2 parents 4b12eda + 6b130a0 commit 8aa62a1

29 files changed

+596
-26
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
> git remote set-url origin git@github.com:sameboat-platform/sameboat-backend.git
99
> ```
1010
11-
> Quick Links: [Instructions (setup & migrations)](./docs/instructions.md) | [API Reference](./docs/api.md) | [Risks](./docs/RISKS.md) | [JWT vs Extended Sessions (Spike)](./docs/spikes/jwt_vs_extended_sessions.md) | [Week 3 Plan](./docs/weekly-plan/week-3/week-3-plan.md) | [Contributing](./CONTRIBUTING.md) | Guard Rails: [Copilot Instructions](.github/copilot-instructions.md) | Journals: [Index](./docs/journals/README.md) · [Week 1](./docs/journals/Week1-Journal.md) · [Week 2](./docs/journals/Week2-Journal.md)
11+
> Quick Links: [Instructions (setup & migrations)](./docs/instructions.md) | [API Reference](./docs/api.md) | [Risks](./docs/RISKS.md) | [JWT vs Extended Sessions (Spike)](./docs/spikes/jwt_vs_extended_sessions.md) | [Week 4 Plan (Backend)](./docs/weekly-plan/week-4/week-4-plan-backend.md) | [Week 4 Checkout (Backend)](./docs/weekly-plan/week-4/week-4-checkout-backend.md) | [Contributing](./CONTRIBUTING.md) | Guard Rails: [Copilot Instructions](.github/copilot-instructions.md) | Journals: [Index](./docs/journals/README.md)
12+
13+
## Week 4 Highlights
14+
- Standardized NOT_FOUND: added `ResourceNotFoundException` and mapped to HTTP 404 with error `NOT_FOUND` in the global handler; service throws, controllers stay thin.
15+
- BAD_REQUEST mapping: focused WebMvc test covering `IllegalArgumentException``{ "error": "BAD_REQUEST" }`.
16+
- OpenAPI sync: spec now includes `/api/version` and a reusable `ErrorResponse` schema enumerating current error codes.
17+
- Future endpoint (gated): added a hardened `GET /users/{id}` returning `PublicUserDto` (no email). It’s disabled by default and only enabled when `sameboat.endpoints.user-read=true`; access allowed to self or `ADMIN`.
1218
1319
## Getting Started (Contributors)
1420
Before writing code or using AI assistance:
@@ -36,6 +42,11 @@ Alias Tokens (for AI prompts): `BACKEND_CI_GUARD`, `LAYER_RULE`, `SECURITY_BASEL
3642
- `GET /health` → 200 OK (custom simple health)
3743
- `GET /actuator/health` → 200 OK and `{ "status": "UP" }`
3844

45+
Windows note: prefer `mvn` (not the wrapper) when running multiple CLI properties in one go; wrap all flags in double quotes in cmd.exe, for example:
46+
```cmd
47+
mvn "-Dskip.migration.test=false" verify
48+
```
49+
3950
### Container (Docker) Usage
4051
A multi-stage Dockerfile is provided for local testing and Render deployment.
4152

@@ -203,7 +214,7 @@ All non-2xx errors:
203214
```json
204215
{ "error": "<CODE>", "message": "Human readable explanation" }
205216
```
206-
Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `RATE_LIMITED`, `INTERNAL_ERROR`.
217+
Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `RATE_LIMITED`, `NOT_FOUND`, `INTERNAL_ERROR`.
207218

208219
| Code | Typical Trigger |
209220
|------|-----------------|
@@ -214,6 +225,7 @@ Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_E
214225
| VALIDATION_ERROR | Bean validation (fields, sizes, empty patch body) |
215226
| BAD_REQUEST | Explicit IllegalArgument / future semantics |
216227
| RATE_LIMITED | Too many requests (e.g., repeated failed logins) |
228+
| NOT_FOUND | Requested resource doesn’t exist (service throws `ResourceNotFoundException`) |
217229
| INTERNAL_ERROR | Uncaught exception (trace id logged) |
218230

219231
## Quality Gates

docs/Architecture.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ SAMEBOAT_CORS_ALLOWED_ORIGINS=https://app.sameboatplatform.org
4848
- Potential WAF / distributed rate limiting (e.g., Redis) for multi-instance auth endpoints.
4949

5050
---
51+
52+
## Week 4 Backend Summary
53+
- Introduced `ResourceNotFoundException` mapped to HTTP 404 with error code `NOT_FOUND` via `GlobalExceptionHandler`.
54+
- Standardized controller 404 behavior by adding service method `UserService.getByIdOrThrow(UUID)` and using it in `GET /users/{id}`.
55+
- Added focused tests: service throws `ResourceNotFoundException`; WebMvc test asserts 404 NOT_FOUND envelope; BAD_REQUEST mapping asserted for `IllegalArgumentException`.
56+
- Synced OpenAPI (`openapi/sameboat.yaml`) to include `/api/version` and the error envelope schema with current error codes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Week 4 Checkout Summary — Backend
2+
3+
## Overview (Week 4 — late October)
4+
1. ### Standardized NOT_FOUND semantics
5+
- Introduced `ResourceNotFoundException` in the common layer and mapped it in `GlobalExceptionHandler` to HTTP 404 with error code `NOT_FOUND`.
6+
- Services throw the exception; controllers remain thin and rely on the centralized handler.
7+
- Added tests: service unit test for missing user and WebMvc test asserting the 404 envelope.
8+
9+
2. ### BAD_REQUEST mapping coverage
10+
- Added a focused WebMvc test to assert `IllegalArgumentException` is translated to HTTP 400 with `{ "error": "BAD_REQUEST" }`.
11+
- Keeps error envelope consistent and prevents controller-specific ad hoc responses.
12+
13+
3. ### Endpoint hardening for future admin/public profiles
14+
- Added a gated user read endpoint `GET /users/{id}` returning `PublicUserDto` (no email or sensitive fields).
15+
- Endpoint is disabled by default; enable with `sameboat.endpoints.user-read=true`.
16+
- Access control: only the authenticated user (self) or `ADMIN` may fetch. Tests are present but disabled until the feature is enabled for real environments.
17+
18+
4. ### OpenAPI & Docs
19+
- Updated `openapi/sameboat.yaml` to include `/api/version` and a reusable `ErrorResponse` schema enumerating current error codes (`UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `VALIDATION_ERROR`, `BAD_REQUEST`, `RATE_LIMITED`, `NOT_FOUND`, `INTERNAL_ERROR`).
20+
- `docs/Architecture.md`: added a Week 4 Backend Summary with the above items.
21+
- README: added Week 4 highlights, Windows `mvn` quoting note, and included `NOT_FOUND` in error catalog table.
22+
23+
5. ### Tests & Quality Gates
24+
- All tests pass locally (migration Testcontainers profile skipped without Docker).
25+
- JaCoCo instruction coverage: ~84% total (gate ≥ 70% remains green).
26+
27+
## Key Decisions & Rationale
28+
- Centralize 404 behavior via a domain exception to reduce controller boilerplate and ensure consistent envelopes.
29+
- Keep cross-user reads gated and least-privilege by default; only self or `ADMIN` can fetch when feature is enabled.
30+
- Public profile DTO excludes email to reduce PII exposure; future public profile endpoint can reuse the same DTO or an even slimmer one.
31+
32+
## What Went Well
33+
- Layering preserved (Controller → Service → Repository). Controllers remain thin and secure.
34+
- Focused tests captured envelope behavior for both NOT_FOUND and BAD_REQUEST without bloating integration suites.
35+
- OpenAPI is back in sync, making frontend consumption clearer.
36+
37+
## What I Struggled With
38+
- Security slices in WebMvc tests required explicit authentication setup; solved with `@WithMockUser` or session cookie when appropriate.
39+
- Property-gated endpoint meant careful test enable/disable to keep the suite green by default.
40+
41+
## Suggested Next Steps (Backend)
42+
- When bringing up the admin console:
43+
- Enable `sameboat.endpoints.user-read=true` in admin/staging environments.
44+
- Add role-based method security (`@EnableMethodSecurity`) and migrate inline checks to `@PreAuthorize`.
45+
- Add integration tests for `403` (non-self, non-admin) and `200` for admin fetching other users.
46+
- Consider adding a dedicated public profile route (e.g., `/profiles/{id}`) with no auth but constrained DTO.
47+
- Continue ratcheting coverage toward 75% by adding small unit tests (e.g., more `UserService` edge cases, filter behaviors).
48+
49+
## Verification (Local)
50+
- Run fast tests (Windows cmd):
51+
```cmd
52+
mvn "-Dskip.migration.test=false" test
53+
```
54+
- Full verify (tests + coverage gate):
55+
```cmd
56+
mvn "-Dskip.migration.test=false" verify
57+
```
58+
- Optional migration schema test (requires Docker/Testcontainers):
59+
```cmd
60+
mvn -Pwith-migration-test test
61+
```
62+
63+
## Checklist (Backend Focus)
64+
- [x] ResourceNotFoundException + 404 mapping in GlobalExceptionHandler
65+
- [x] Service method throws on missing resource and unit test
66+
- [x] WebMvc test for NOT_FOUND envelope
67+
- [x] Focused BAD_REQUEST mapping test (`IllegalArgumentException`)
68+
- [x] OpenAPI sync with `/api/version` and error schema
69+
- [x] Architecture.md Week 4 summary
70+
- [x] README updated (Week 4 highlights, Windows mvn note, error catalog includes NOT_FOUND)
71+
- [x] Coverage gate ≥ 70% (current ~84%)
72+
- [x] Gated `GET /users/{id}` returning `PublicUserDto`; access: self or ADMIN; disabled by default
73+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Week 4 – Backend Plan (Checklist)
2+
3+
Context: Backend-only items extracted from the Week 4 plan. Status as of 2025-10-29.
4+
5+
## Done
6+
- [x] Environment onboarding: `.env.example` present at repo root and referenced in README (copy to `.env` for local runs).
7+
- [x] CI pipeline (BACKEND_CI_GUARD): `.github/workflows/backend-ci.yml` runs migration immutability check, `mvn verify` (unit + integration tests), JaCoCo coverage gate (≥ 70%), and a Flyway schema test profile step.
8+
- [x] Auth hardening: password complexity validation, in-memory login rate limiting returning `RATE_LIMITED` on abuse, and scheduled session pruning job.
9+
- [x] Public endpoints: `GET /health`, `GET /actuator/health`, and `GET /api/version` implemented and covered by integration tests.
10+
- [x] Profiles & CORS: `dev` vs `prod` profiles with secure cookie/domains in prod; CORS allowlist configured (credentials enabled, no wildcards).
11+
- [x] Flyway migrations: schema aligned with `UserEntity` (e.g., timezone column) via a new migration; historical migrations protected by immutability scripts.
12+
- [x] NOT_FOUND mapping: Added `ResourceNotFoundException`, mapped to 404 in `GlobalExceptionHandler`, service method `UserService.getByIdOrThrow(UUID)`, gated `GET /users/{id}` uses it when enabled; tests added (service + WebMvc 404 envelope).
13+
- [x] BAD_REQUEST mapping test: Focused WebMvc test asserting `IllegalArgumentException``{ "error": "BAD_REQUEST" }`.
14+
- [x] OpenAPI sync: `openapi/sameboat.yaml` updated to include `/api/version` and `ErrorResponse` schema with current error codes; brief Week 4 backend summary added to `docs/Architecture.md`.
15+
- [x] Coverage gate: verified JaCoCo ≥ 70% (current ~84%).
16+
17+
## Left (Backend-focused)
18+
- [ ] None for Week 4. Carryovers: raise coverage target to 75% in a future cycle; enable gated `/users/{id}` when admin/public profile work lands.
19+
20+
## Scope Notes
21+
- Excluded (frontend-only): health polling backoff/pause, reduced-motion accessibility controls, debug panel utilities, identicon fallback, and visibility-driven session refresh.

docs/weekly-plan/week-4/week-4-plan-draft.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ Theme (Proposed): Quality hardening, accessibility polish, developer ergonomics,
1616
| Visual polish (avatars) | Clarity in demos | Deterministic identicon fallback implemented |
1717
| Session freshness | Better long-lived tab UX | Visibility change triggers conditional refresh |
1818

19+
## Week 4 Status Checklist (as of 2025-10-29)
20+
21+
Notes:
22+
- This repository is the backend (Spring Boot). Several Week 4 items target the frontend app (Vite/React) and its CI; those remain pending here and should be tracked/completed in the frontend repo.
23+
- BACKEND_CI_GUARD respected: main backend CI workflow exists at `.github/workflows/backend-ci.yml`.
24+
25+
Done
26+
- [x] Environment onboarding (backend scope): `.env.example` is present at repo root and referenced in README “Run locally” section (copy `.env.example``.env`).
27+
- [x] CI (backend scope): Backend CI already runs tests (`mvn verify`) and fails on red; coverage gate enforced (JaCoCo ≥ 70%).
28+
29+
Left (to be completed, mostly frontend scope unless noted)
30+
- [ ] Testing & CI (frontend): Add `npm test` step to the frontend CI (`frontend-ci.yml`) before build; add routing/auth UI tests listed below.
31+
- [ ] Health polling resilience (frontend): pause-on-error, exponential backoff, debug surface for failure streak/last success.
32+
- [ ] Accessibility / Motion control (frontend): central reduced‑motion preference, gate animations, document approach in `Architecture.md` (Week 4 section).
33+
- [ ] Developer control utilities (frontend): debug panel interval override, failure/backoff display, “Force full auth refresh” button.
34+
- [ ] Visual / UX polish (frontend): deterministic identicon fallback, copy‑to‑clipboard user ID, spacing sweep.
35+
- [ ] Session freshness (frontend): `visibilitychange`-based conditional refresh with overlap safeguards.
36+
- [ ] Documentation: Add Week 4 summary in `docs/Architecture.md`; update any week‑3 links if metrics scripts change; consider short CHANGELOG segment if scope grows.
37+
38+
---
39+
1940
## Detailed Tasks (Initial Backlog)
2041

2142
### 1. Testing & CI
File renamed without changes.

openapi/sameboat.yaml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,49 @@ paths:
88
summary: Liveness check
99
responses:
1010
'200':
11-
description: OK
11+
description: OK
12+
/api/version:
13+
get:
14+
summary: API version
15+
description: Returns the deployed backend version.
16+
responses:
17+
'200':
18+
description: OK
19+
content:
20+
application/json:
21+
schema:
22+
$ref: '#/components/schemas/VersionResponse'
23+
'500':
24+
description: Internal error
25+
content:
26+
application/json:
27+
schema:
28+
$ref: '#/components/schemas/ErrorResponse'
29+
components:
30+
schemas:
31+
ErrorResponse:
32+
type: object
33+
properties:
34+
error:
35+
type: string
36+
description: Stable error code
37+
enum:
38+
- UNAUTHENTICATED
39+
- BAD_CREDENTIALS
40+
- SESSION_EXPIRED
41+
- VALIDATION_ERROR
42+
- BAD_REQUEST
43+
- RATE_LIMITED
44+
- INTERNAL_ERROR
45+
- NOT_FOUND
46+
message:
47+
type: string
48+
description: Human-readable error message
49+
required: [error, message]
50+
VersionResponse:
51+
type: object
52+
properties:
53+
version:
54+
type: string
55+
description: Semantic version of the deployed backend
56+
required: [version]

src/main/java/com/sameboat/backend/auth/AuthController.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ public class AuthController {
3636
private final SameboatProperties props;
3737
private final RateLimiterService rateLimiter;
3838

39+
/**
40+
* Constructor with dependencies injected.
41+
* @param userService the service managing user accounts
42+
* @param sessionService the service managing user sessions
43+
* @param passwordEncoder the password encoder for hashing and verifying passwords
44+
* @param props application configuration properties
45+
* @param rateLimiter the rate limiter service for login attempts
46+
*/
3947
public AuthController(UserService userService, SessionService sessionService, PasswordEncoder passwordEncoder, SameboatProperties props, RateLimiterService rateLimiter) {
4048
this.userService = userService;
4149
this.sessionService = sessionService;
@@ -44,7 +52,11 @@ public AuthController(UserService userService, SessionService sessionService, Pa
4452
this.rateLimiter = rateLimiter;
4553
}
4654

47-
/** Builds a configured session cookie with security attributes. */
55+
/**
56+
* Builds the session cookie with appropriate attributes.
57+
* @param token the session token value
58+
* @return the constructed Cookie object
59+
*/
4860
private Cookie buildSessionCookie(String token) {
4961
var sessionCfg = props.getSession();
5062
var cookieCfg = props.getCookie();
@@ -58,20 +70,34 @@ private Cookie buildSessionCookie(String token) {
5870
return cookie;
5971
}
6072

61-
/** Convenience builder for a uniform bad credentials response with logging. */
73+
/**
74+
* Convenience builder for a uniform bad credentials response.
75+
* @param email the email used in the failed login attempt
76+
* @return the ResponseEntity with error details
77+
*/
6278
private ResponseEntity<ErrorResponse> badCredentials(String email) {
6379
log.info("Login failed email={} (bad credentials)", email);
6480
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
6581
.body(new ErrorResponse("BAD_CREDENTIALS", "Email or password is incorrect"));
6682
}
6783

68-
/** Convenience builder for a uniform rate limited response. */
84+
/**
85+
* Convenience builder for a rate limited response.
86+
* @param key the rate limit key that was exceeded
87+
* @return the ResponseEntity with error details
88+
*/
6989
private ResponseEntity<ErrorResponse> rateLimited(String key) {
90+
log.info("Rate limited response for key={}", key);
7091
return ResponseEntity.status(429)
7192
.body(new ErrorResponse("RATE_LIMITED", "Too many attempts; try again later"));
7293
}
7394

74-
/** Build a rate key using normalized email + client ip if available. */
95+
/**
96+
* Builds a rate limiting key based on normalized email and request IP.
97+
* @param emailNorm the normalized email address
98+
* @param request the HTTP servlet request
99+
* @return the constructed rate limit key
100+
*/
75101
private String rateKey(String emailNorm, jakarta.servlet.http.HttpServletRequest request) {
76102
String ip = request != null ? request.getRemoteAddr() : "";
77103
return emailNorm + "|" + ip;
@@ -80,6 +106,9 @@ private String rateKey(String emailNorm, jakarta.servlet.http.HttpServletRequest
80106
/**
81107
* Registers a new user account (unless email already exists) and immediately
82108
* creates a session, returning the new user id plus issuing the cookie.
109+
* @param request the registration request payload
110+
* @param response the HTTP servlet response to add the session cookie to
111+
* @return the ResponseEntity with new user id or error details
83112
*/
84113
@PostMapping("/register")
85114
public ResponseEntity<?> register(@RequestBody @Valid RegisterRequest request, HttpServletResponse response) {
@@ -101,6 +130,10 @@ public ResponseEntity<?> register(@RequestBody @Valid RegisterRequest request, H
101130
/**
102131
* Authenticates a user by email + password, optionally auto-creating a dev
103132
* user if configured. Issues a fresh session cookie on success.
133+
* @param request the login request payload
134+
* @param httpRequest the HTTP servlet request for rate limiting info
135+
* @param response the HTTP servlet response to add the session cookie to
136+
* @return the ResponseEntity with user details or error info
104137
*/
105138
@PostMapping("/login")
106139
public ResponseEntity<?> login(@RequestBody @Valid com.sameboat.backend.auth.dto.LoginRequest request,
@@ -109,7 +142,6 @@ public ResponseEntity<?> login(@RequestBody @Valid com.sameboat.backend.auth.dto
109142
String emailNorm = userService.normalizeEmail(request.email());
110143
String key = rateKey(emailNorm, httpRequest);
111144
if (rateLimiter.isLimited(key)) {
112-
log.info("Login attempt blocked by rate limiter for key={}", key);
113145
return rateLimited(key);
114146
}
115147
var userOpt = userService.getByEmailNormalized(emailNorm);
@@ -144,6 +176,9 @@ public ResponseEntity<?> login(@RequestBody @Valid com.sameboat.backend.auth.dto
144176

145177
/**
146178
* Logs out the current session (if present) and expires the cookie.
179+
* @param token the session token from the SBSESSION cookie
180+
* @param response the HTTP servlet response to add the expired cookie to
181+
* @return the ResponseEntity indicating no content
147182
*/
148183
@PostMapping("/logout")
149184
public ResponseEntity<Void> logout(@CookieValue(value = "SBSESSION", required = false) String token, HttpServletResponse response) {

0 commit comments

Comments
 (0)