Status: Week 3 backend hardening complete (password complexity, rate limiting, session pruning). This document covers implemented endpoints and response contracts.
- All responses (success or error) are JSON.
- Authentication: opaque session cookie (primary name
SBSESSION, legacy/alias accepted:sb_session) containing a UUID. - Dev default TTL: 7 days. Prod profile sets
Securecookie, domain.sameboatplatform.org, TTL 14 days. - Error envelope format:
{ "error": "<CODE>", "message": "Human readable explanation" } - Bio max length is 500 characters (intentional spec choice).
| Code | Meaning | Typical Source |
|---|---|---|
| UNAUTHENTICATED | No / invalid / garbage cookie | EntryPoint / controllers |
| BAD_CREDENTIALS | Bad email or password on login | /auth or /api/auth login |
| SESSION_EXPIRED | Cookie valid but session expired | Filter -> EntryPoint |
| EMAIL_EXISTS | Registration attempt with existing email (409) | /auth/register |
| VALIDATION_ERROR | Body validation failure (400) | Controllers |
| BAD_REQUEST | Explicit IllegalArgument (service) | Services / controllers |
| RATE_LIMITED | Too many requests (e.g., repeated failed logins) (429) | /auth/login |
| INTERNAL_ERROR | Unhandled exception (500) | Global handler |
{
"id": "<uuid>",
"email": "user@example.com",
"displayName": "user@example.com",
"avatarUrl": null,
"bio": null,
"timezone": null,
"role": "USER"
}{ "error": "<CODE>", "message": "Human readable explanation" }Current error codes now include: UNAUTHENTICATED, BAD_CREDENTIALS, SESSION_EXPIRED, EMAIL_EXISTS, VALIDATION_ERROR, BAD_REQUEST, RATE_LIMITED, INTERNAL_ERROR.
Authenticate with email/password. (Dev auto-create optional under test/profile if enabled.)
Request:
{ "email": "user@example.com", "password": "********" }Success (200):
{ "user": { "id": "...", "email": "user@example.com", "displayName": "user@example.com", "avatarUrl": null, "bio": null, "timezone": null, "role": "USER" } }Failure (401 BAD_CREDENTIALS):
{ "error": "BAD_CREDENTIALS", "message": "Email or password is incorrect" }Failure (429 RATE_LIMITED):
{ "error": "RATE_LIMITED", "message": "Too many attempts; try again later" }Registers a new user (email must be unique). Returns a session cookie and minimal body containing the userId.
Password policy: minimum 8 characters and must include at least one uppercase, one lowercase, and one digit.
Request:
{
"email": "dev@example.com",
"password": "Abcdef12",
"displayName": "Dev"
}Responses:
- 200:
{ "userId": "<uuid>" } - 409 EMAIL_EXISTS:
{ "error": "EMAIL_EXISTS", "message": "Email already registered" } - 400 VALIDATION_ERROR (e.g., password too weak)
{ "error": "VALIDATION_ERROR", "message": "password must be at least 8 characters and include upper, lower, and digit" }
Invalidates current session and clears cookie.
Responses:
- 204 No Content (idempotent)
Returns authenticated user.
401 cases:
- Missing/garbage cookie →
UNAUTHENTICATED - Expired cookie →
SESSION_EXPIRED
Partial profile update (requires at least one field; empty body rejected).
Request body (all shown fields optional – only included keys are changed):
{
"displayName": "New Name",
"avatarUrl": "https://cdn.example.com/avatar.png",
"bio": "Short bio (<=500 chars)",
"timezone": "America/Chicago"
}Validation rules:
displayName: 2..50 charsavatarUrl: <=255 charsbio: <=500 charstimezone: <=100 chars (placeholder; not yet validated against IANA list)- Non-empty update: at least one field must be provided
Responses:
- 200 Updated
UserDto - 400 Validation error →
VALIDATION_ERROR - 401 If not authenticated / expired (distinct codes as above)
Returns the deployed backend version. Public and unauthenticated.
Success (200):
{ "version": "0.1.0" }Notes:
- In production, the value reflects the JAR manifest when available; falls back to
project.versionduring development/test.
| Scenario | Status | error code | Notes |
|---|---|---|---|
| Missing token / invalid UUID | 401 | UNAUTHENTICATED | No valid session cookie |
| Expired session | 401 | SESSION_EXPIRED | Session expiresAt passed |
| Bad credentials | 401 | BAD_CREDENTIALS | Login only |
| Registration duplicate | 409 | EMAIL_EXISTS | Email normalized & already present |
| Validation failure | 400 | VALIDATION_ERROR | Field constraints |
| Empty PATCH body | 400 | VALIDATION_ERROR | Enforced explicitly |
| Rate limited login attempts | 429 | RATE_LIMITED | 5 failures within 5 minutes |
| Generic uncaught exception | 500 | INTERNAL_ERROR | Trace id logged server-side |
| Illegal argument (service) | 400 | BAD_REQUEST | Future usage |
- Creation: at login or register (UUID as token value).
- Validation: filter loads session by UUID; sets request attribute for expired → code
SESSION_EXPIRED; missing/invalid →UNAUTHENTICATED. - Touch:
lastSeenAtupdated on authenticated requests. - Expiry: 7 days dev / 14 days prod.
- Pruning: scheduled hourly job deletes expired sessions (server-side cleanup; expiry also enforced at request time).
Login:
curl -i -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"me@example.com","password":"Abcdef12"}'Register:
curl -i -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"me2@example.com","password":"Abcdef12","displayName":"Me Two"}'Expired (simulate): manually adjust DB row expires_at earlier and call /me with cookie to observe SESSION_EXPIRED.
For broader architecture overview see docs/Architecture.md.