221 lines
5.6 KiB
Markdown
221 lines
5.6 KiB
Markdown
# Xpensely Server — API Reference
|
||
|
||
> Last updated: 2026-05-09 · Branch: `feature/security-hardening`
|
||
|
||
## Table of Contents
|
||
1. [Overview](#1-overview)
|
||
2. [Authentication](#2-authentication)
|
||
3. [Rate Limiting](#3-rate-limiting)
|
||
4. [Endpoints](#4-endpoints)
|
||
- 4.1 [Home](#41-home)
|
||
- 4.2 [Users](#42-users)
|
||
- 4.3 [Expense Lists](#43-expense-lists)
|
||
5. [Data Models](#5-data-models)
|
||
6. [Error Handling](#6-error-handling)
|
||
7. [Recent Changes — `feature/security-hardening`](#7-recent-changes)
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
Xpensely Server is a Spring Boot REST API that manages shared expense lists for pairs of users. It uses Google OAuth2 JWT tokens for authentication. All protected endpoints require a valid Bearer token in the `Authorization` header.
|
||
|
||
**Base URL (local dev):** `http://localhost:8080`
|
||
|
||
**Content-Type:** `application/json` for all request and response bodies.
|
||
|
||
**Public endpoints (no auth required):**
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| GET | `/` | Health check — returns `"Welcome"` |
|
||
| POST | `/api/users/createUser` | Register a new user |
|
||
| GET | `/api/users/byName` | Look up a user by username |
|
||
|
||
All other endpoints require authentication (see [Section 2](#2-authentication)).
|
||
|
||
## 2. Authentication
|
||
|
||
The server uses **OAuth2 Resource Server** authentication with **Google ID JWT tokens**.
|
||
|
||
### How it works
|
||
|
||
1. The client authenticates with Google and receives a Google ID JWT.
|
||
2. Every protected API request must include this token in the header:
|
||
|
||
```
|
||
Authorization: Bearer <google-id-jwt>
|
||
```
|
||
|
||
3. The server validates the JWT signature and extracts the `sub` claim (Google User ID).
|
||
4. The `sub` value is used to look up the registered `AppUser` in the database via `AuthenticatedUserResolver`.
|
||
5. If no `AppUser` exists for that Google ID, the request is rejected with **403 Forbidden**.
|
||
|
||
### Test profile
|
||
|
||
When the application runs under the `test` Spring profile (`-Dspring.profiles.active=test`), **all security is disabled** — every endpoint is accessible without a token. This is used for automated tests only.
|
||
|
||
### User registration flow
|
||
|
||
Before a user can call any protected endpoint they must first be registered:
|
||
|
||
1. Authenticate with Google to obtain a Google ID JWT.
|
||
2. Call `POST /api/users/createUser` with the JWT's `sub` value as `googleId` and a chosen `username`.
|
||
3. All subsequent protected calls use the same JWT — the server resolves the caller automatically.
|
||
|
||
## 3. Rate Limiting
|
||
|
||
All requests pass through a `RateLimitFilter` (implemented with **Bucket4j**).
|
||
|
||
| Setting | Value |
|
||
|---------|-------|
|
||
| Limit | 60 requests per minute |
|
||
| Window | Rolling 1-minute bucket |
|
||
| Key (authenticated) | JWT `sub` claim (Google User ID) |
|
||
| Key (unauthenticated) | `X-Forwarded-For` header, falling back to remote IP |
|
||
|
||
When the limit is exceeded the server responds with:
|
||
|
||
```
|
||
HTTP 429 Too Many Requests
|
||
```
|
||
|
||
No `Retry-After` header is currently returned. Clients should back off and retry after 60 seconds.
|
||
|
||
> **Note:** Rate limiting applies in the `!test` profile only. Tests run without rate limiting.
|
||
|
||
## 4. Endpoints
|
||
|
||
### 4.1 Home
|
||
|
||
#### `GET /`
|
||
|
||
Health check. No authentication required.
|
||
|
||
**Response:** `200 OK`
|
||
```
|
||
Welcome
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 Users
|
||
|
||
Base path: `/api/users`
|
||
|
||
---
|
||
|
||
#### `POST /api/users/createUser` — Register a user
|
||
|
||
**Auth required:** No
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"username": "alice",
|
||
"googleId": "118400012345678901234"
|
||
}
|
||
```
|
||
|
||
| Field | Type | Constraints |
|
||
|-------|------|-------------|
|
||
| `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.
|
||
|
||
**Error responses:**
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 400 | Validation failure (field errors returned as `{"fieldName": "message"}`) |
|
||
| 409 | `username` already taken |
|
||
|
||
---
|
||
|
||
#### `GET /api/users` — Get user by ID
|
||
|
||
**Auth required:** Yes
|
||
|
||
**Query params:**
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `id` | Long | Yes | Database ID of the user |
|
||
|
||
**Success response:** `200 OK` — returns [AppUser](#appuser).
|
||
|
||
**Error responses:**
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 404 | No user found for `id` |
|
||
|
||
---
|
||
|
||
#### `GET /api/users/byName` — Get user by username
|
||
|
||
**Auth required:** No
|
||
|
||
**Query params:**
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `username` | String | Yes | Exact username (case-sensitive) |
|
||
|
||
**Success response:** `200 OK` — returns [AppUser](#appuser).
|
||
|
||
**Error responses:**
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 404 | No user found for `username` |
|
||
|
||
---
|
||
|
||
#### `GET /api/users/byGoogleId` — Get user by Google ID
|
||
|
||
**Auth required:** Yes
|
||
|
||
**Query params:**
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `id` | String | Yes | Google `sub` claim |
|
||
|
||
**Success response:** `200 OK` — returns [AppUser](#appuser).
|
||
|
||
**Error responses:**
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 404 | No user found for that Google ID |
|
||
|
||
---
|
||
|
||
#### `DELETE /api/users` — Delete a user
|
||
|
||
**Auth required:** Yes
|
||
|
||
**Query params:**
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `id` | Long | Yes | Database ID of the user to delete |
|
||
|
||
**Success response:** `200 OK` — returns the deleted [AppUser](#appuser).
|
||
|
||
**Error responses:**
|
||
| Status | Condition |
|
||
|--------|-----------|
|
||
| 404 | No user found for `id` |
|
||
|
||
---
|
||
|
||
### 4.3 Expense Lists
|
||
|
||
_TODO_
|
||
|
||
## 5. Data Models
|
||
|
||
_TODO_
|
||
|
||
## 6. Error Handling
|
||
|
||
_TODO_
|
||
|
||
## 7. Recent Changes — `feature/security-hardening`
|
||
|
||
_TODO_
|