diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4bb51fb --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)", + "Bash(git commit *)" + ] + } +} diff --git a/docker-compose.yml b/docker-compose.yml index c319a7d..64b6e0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,6 @@ services: pull_policy: always restart: always environment: - GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} - DB_PORT: 5432 DB_P_NAME: ${POSTGRES_DB} DB_USERNAME: ${POSTGRES_USER} diff --git a/docs/API.md b/docs/API.md index 68fe07c..c4ffb65 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # Xpensely Server — API Reference -> Last updated: 2026-05-09 · Branch: `feature/security-hardening` +> Last updated: 2026-05-14 · Branch: `dev` ## Table of Contents 1. [Overview](#1-overview) @@ -28,6 +28,7 @@ Xpensely Server is a Spring Boot REST API that manages shared expense lists for | Method | Path | Description | |--------|------|-------------| | GET | `/` | Health check — returns `"Welcome"` | +| GET | `/api/version` | Returns build version and timestamp | | POST | `/api/users/createUser` | Register a new user | | GET | `/api/users/byName` | Look up a user by username | @@ -98,6 +99,25 @@ Welcome --- +#### `GET /api/version` + +Returns the application version and build timestamp. No authentication required. + +**Response:** `200 OK` +```json +{ + "version": "0.0.1-SNAPSHOT", + "builtAt": "2026-05-09T10:00:00Z" +} +``` + +| Field | Type | Notes | +|-------|------|-------| +| `version` | String | Maven project version | +| `builtAt` | String (ISO-8601) | UTC timestamp of the build | + +--- + ### 4.2 Users Base path: `/api/users` @@ -121,7 +141,7 @@ Base path: `/api/users` | `username` | String | Required. 3–30 chars. Pattern: `^[a-zA-Z0-9_.\-]+$` | | `googleId` | String | Required. Non-blank. Must match the JWT `sub` from Google. | -**Success response:** `200 OK` — returns the created [AppUser](#appuser) object. +**Success response:** `201 Created` — returns the created [AppUser](#appuser) object. **Error responses:** | Status | Condition | @@ -145,6 +165,7 @@ Base path: `/api/users` **Error responses:** | Status | Condition | |--------|-----------| +| 403 | Authenticated user's ID does not match the requested `id` | | 404 | No user found for `id` | --- @@ -181,6 +202,7 @@ Base path: `/api/users` **Error responses:** | Status | Condition | |--------|-----------| +| 403 | Requested Google ID does not match the authenticated user's Google ID | | 404 | No user found for that Google ID | --- @@ -194,11 +216,12 @@ Base path: `/api/users` |-------|------|----------|-------------| | `id` | Long | Yes | Database ID of the user to delete | -**Success response:** `200 OK` — returns the deleted [AppUser](#appuser). +**Success response:** `200 OK` — returns a plain string: `"User deleted: "`. **Error responses:** | Status | Condition | |--------|-----------| +| 403 | Authenticated user's ID does not match the requested `id` | | 404 | No user found for `id` | --- @@ -219,7 +242,11 @@ Returns all expense lists where the caller is the owner **or** has been shared t **Request body:** None -**Success response:** `200 OK` — array of [ExpenseList](#expenselist). +**Success responses:** +| Status | Condition | +|--------|-----------| +| 200 OK | Returns array of [ExpenseList](#expenselist) | +| 204 No Content | Caller has no expense lists | ```json [ @@ -337,7 +364,7 @@ Only the **owner** may delete a list. Deleting a list cascades to all its expens | `date` | String (ISO-8601) | Required. Format: `YYYY-MM-DD`. | | `category` | String | Required. Non-blank category name. | -**Success response:** `200 OK` — returns the created [Expense](#expense). +**Success response:** `201 Created` — returns the created [Expense](#expense). **Error responses:** | Status | Condition | @@ -411,7 +438,7 @@ Caller must be a member of the list. Expense must belong to the specified list. Caller must be a member of the list. -**Success response:** `200 OK` — returns the deleted [Expense](#expense). +**Success response:** `204 No Content` **Error responses:** | Status | Condition | @@ -465,13 +492,14 @@ Joins the caller to a shared expense list using an invite code. |-------|------|-------------| | `inviteCode` | String | Required. Exactly 6 characters. | -**Success response:** `200 OK` — returns the [ExpenseList](#expenselist) the caller joined. +**Success response:** `200 OK` — returns a plain string: `"User added to the list"`. **Error responses:** | Status | Condition | |--------|-----------| -| 400 | Validation failure or invite code not found / expired | -| 403 | Caller is already the owner of this list | +| 400 | Validation failure or caller is already the owner of the list | +| 404 | Invite code not found or expired | +| 226 IM Used | List already has a second member (`sharedWith` is not null) | --- diff --git a/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java b/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java index 2b85f99..97ef8b8 100644 --- a/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java +++ b/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java @@ -32,7 +32,6 @@ public class SecurityConfig { .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults())) - .oauth2Login(Customizer.withDefaults()) .addFilterAfter(new RateLimitFilter(), BearerTokenAuthenticationFilter.class) .csrf(csrf -> csrf.disable()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 61116e5..ad7db8d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,8 +3,6 @@ spring.application.name=XpenselyServer #Security spring.security.enabled=false -spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} -spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com # PostgreSQL Configuration