# Secure Notes Vault API ## System overview Secure Notes Vault API is a Spring Boot REST service for creating, managing, and sharing private notes. Users register and log in with username/password credentials, then authenticate subsequent requests with a JWT bearer token. The application is organized around a standard controller-service-repository flow: - Controllers expose the authentication and notes REST endpoints. - Spring Security and a JWT filter authenticate protected requests. - Services enforce note ownership and read-only shared-note access rules. - JPA repositories persist users, notes, and note shares. - Flyway migrations manage the database schema for both local H2 and PostgreSQL-backed runs. ## Running the program This project requires Java 21 and Maven. The API starts on `http://localhost:8080`. For the fastest local setup, run the app with the in-memory H2 database: ```bash make dev ``` To run the test suite: ```bash make test ``` To run the API and PostgreSQL with Docker Compose: ```bash make docker-up ``` Stop the Docker Compose stack with: ```bash make docker-down ``` You can also run the Spring Boot app directly: ```bash JWT_SECRET=dev-secret-change-me-do-not-use-in-production mvn spring-boot:run ``` ## Design decisions - Java 21 was selected because the challenge is for a Java-oriented backend role and because modern Java records work well for request/response DTOs. - Spring Boot provides a compact way to build a production-style REST API with validation, dependency injection, security, persistence, and test support without adding much custom framework code. - Maven is used for dependency management and repeatable local builds because it is widely understood in Java backend environments. - Spring Security handles authentication boundaries, while a custom JWT filter keeps the API stateless after login. This makes the service easier to run behind a load balancer because requests do not depend on server-side sessions. - Passwords are stored as BCrypt hashes, never as plaintext. The JWT signing secret is supplied through configuration and should come from a secret manager or environment variable outside local development. - H2 is used for fast local development and automated tests. PostgreSQL is supported through Docker Compose and a Spring profile because it is a better fit for a production relational datastore. - JPA/Hibernate keeps the persistence layer concise for this size of application. The schema is still managed explicitly with Flyway so database changes are versioned and repeatable. - The data model follows the required `User`, `Note`, and `NoteShare` entities, with an added `title` field on notes for a more realistic note-taking API. - Share records grant read-only access to a specific user. Owners can create, update, delete, and share their notes; shared recipients can only read them. - UUID primary keys are used so identifiers are opaque and do not expose record counts or ordering. - The service returns structured error responses for application-level failures so clients can handle validation, authentication, authorization, and conflict cases predictably. - This implementation favors clarity over extra infrastructure. Features such as refresh tokens, token revocation, audit logging, pagination, rate limiting, and OpenAPI generation would be good production follow-ups. ## Security considerations - Passwords are hashed with BCrypt before being persisted. Plaintext passwords are accepted only in registration/login request bodies and are never returned by the API. - Authentication uses JWT bearer tokens. Protected endpoints require an `Authorization: Bearer ` header. - The JWT signing secret is supplied through configuration. The local development value should not be used in production; production deployments should load it from an environment variable or secret manager. - Access control is enforced in the service layer. Users can read and modify only their own notes unless a note has been explicitly shared with them. - Shared notes are read-only for recipients. Only the owner can update, delete, or share a note. - The API returns `404 Not Found` for another user's unshared note so callers cannot distinguish between a missing note and a note they are not allowed to see. - Production deployments should run behind HTTPS, avoid logging credentials or tokens, and consider token expiration, rotation, revocation, rate limiting, and audit logging. ## Assumptions, tradeoffs, and future improvements - Usernames are treated as unique login identifiers. Email verification, password reset flows, and account recovery are outside the scope of this challenge. - Note sharing is user-to-user and read-only. The implementation does not include groups, roles, expiring shares, or write permissions for shared users. - H2 is used for fast local development and tests, while PostgreSQL is available through Docker Compose for a more production-like relational database. - JWT authentication keeps the API stateless, but this implementation does not include refresh tokens, token revocation, or a logout mechanism. - The API currently returns all visible notes from `GET /notes`. Pagination, sorting, filtering, and search would be important as the dataset grows. - Additional production-grade coverage would include update/delete/share mutation tests, PostgreSQL integration tests, and coverage reporting. - Future improvements could include OpenAPI documentation, audit logging, rate limiting, structured request logging, health checks, metrics, and CI/CD automation. ## Production considerations - Deployment: package the Spring Boot service as a container, run it behind HTTPS, and connect it to a managed PostgreSQL database. Secrets such as `JWT_SECRET` should come from the deployment platform's secret manager rather than source control or image defaults. - Monitoring and alerts: track request latency, 4xx/5xx rates, authentication failures, database connection pool health, migration failures, and unexpected exception counts. Alert on sustained 5xx responses, database connectivity issues, and spikes in failed logins. - Database migrations: continue using Flyway versioned migrations. In production, migrations should be reviewed, tested against staging data, and applied as part of a controlled deployment process. - Scaling: the API is stateless after login, so multiple application instances can run behind a load balancer. To support higher concurrency, tune the database connection pool, add pagination to list endpoints, keep indexes aligned with access patterns, and consider caching only after measuring bottlenecks. ## Testing The automated tests run with Spring Boot, MockMvc, Spring Security, JPA, validation, and Flyway-backed H2 enabled. This keeps the tests close to real API behavior while still making them fast enough to run locally. Run the test suite with: ```bash make test ``` Current automated coverage includes: - Registration creates users, hashes passwords, rejects duplicate usernames, and returns validation errors for invalid payloads. - Note creation requires a bearer token, persists notes for the authenticated owner, and rejects invalid note payloads. - Note listing returns only notes owned by the caller plus notes explicitly shared with the caller. - Note retrieval returns owned notes, returns shared notes as `SHARED_READ`, and verifies that a user cannot access another user's unshared note. - Protected endpoints return `401 Unauthorized` when no JWT is supplied. - Invalid UUID path variables return a structured `400 Bad Request` response. The required access-control test is covered by `NoteRetrievalControllerTest.getNoteReturnsNotFoundForUnsharedNoteOwnedByAnotherUser`, which creates a note as one user, authenticates as a different user, and asserts that the second user receives `404 Not Found` for the unshared note. Additional production-grade coverage would include update/delete/share mutation tests, PostgreSQL integration tests, and coverage reporting. A React app located at https://git.seanstarkey.dev/starkey/notes was also used for manual API testing. It exercises the API from a client perspective and displays REST requests/responses for debugging. It runs in a Docker container. This app was worth the investment because of the problems I had with some REST communications. ## API Endpoints ### Authentication | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/auth/register` | Register a new user | | `POST` | `/auth/login` | Authenticate and receive a JWT | ### Notes | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/notes` | Create a new note | | `GET` | `/notes` | List owned notes and notes shared with you | | `GET` | `/notes/{id}` | Get a note by ID (owner or shared recipient) | | `PUT` | `/notes/{id}` | Update a note (owner only) | | `DELETE` | `/notes/{id}` | Delete a note (owner only) | | `POST` | `/notes/{id}/share` | Share a note with another user (read-only) | ## API Usage Examples The following examples assume the API is running at `http://localhost:8080`. Register two users: ```bash curl -i -X POST http://localhost:8080/auth/register \ -H 'Content-Type: application/json' \ -d '{"username":"alice","password":"password123"}' curl -i -X POST http://localhost:8080/auth/register \ -H 'Content-Type: application/json' \ -d '{"username":"bob","password":"password123"}' ``` Log in and save each JWT: ```bash ALICE_TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"alice","password":"password123"}' | jq -r '.token') BOB_TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"bob","password":"password123"}' | jq -r '.token') ``` Create a note as Alice: ```bash NOTE_ID=$(curl -s -X POST http://localhost:8080/notes \ -H "Authorization: Bearer $ALICE_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"title":"Launch checklist","content":"Rotate credentials before launch."}' | jq -r '.id') ``` List Alice's visible notes: ```bash curl -i http://localhost:8080/notes \ -H "Authorization: Bearer $ALICE_TOKEN" ``` Share Alice's note with Bob: ```bash curl -i -X POST http://localhost:8080/notes/$NOTE_ID/share \ -H "Authorization: Bearer $ALICE_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"username":"bob"}' ``` Bob can read the shared note: ```bash curl -i http://localhost:8080/notes/$NOTE_ID \ -H "Authorization: Bearer $BOB_TOKEN" ``` Bob cannot update the shared note because shared access is read-only: ```bash curl -i -X PUT http://localhost:8080/notes/$NOTE_ID \ -H "Authorization: Bearer $BOB_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"title":"Edited by Bob","content":"This update should be rejected."}' ``` ## HTTP return codes Application-defined HTTP status codes are generated in `AuthController` for successful registration and in `GlobalExceptionHandler` for API error responses. Spring Security and Spring MVC may still produce framework-level responses for requests that do not reach a controller, such as unauthenticated protected requests or unsupported methods. | HTTP code | Status | Generated in | Trigger | Response body | |---:|---|---|---|---| | `200` | OK | `AuthController.login`; `NoteController.listNotes`; `NoteController.getNote`; `NoteController.updateNote` | Login succeeds, visible notes are listed, an authorized note is retrieved, or an owned note is updated. | `AuthResponse`, `NoteResponse`, or list of `NoteResponse` values depending on the endpoint. | | `201` | Created | `AuthController.register`; `NoteController.createNote`; `NoteController.shareNote` | A user is registered, a note is created, or a note is shared. | `UserResponse`, `NoteResponse`, or `ShareResponse` depending on the endpoint. | | `204` | No Content | `NoteController.deleteNote` | Owner successfully deletes a note. | Empty body. | | `400` | Bad Request | `GlobalExceptionHandler.handleMalformedJson` | Request body cannot be parsed or converted from JSON. | `ErrorResponse` with `Malformed request body`. | | `400` | Bad Request | `GlobalExceptionHandler.handleTypeMismatch` | Path variable or query parameter cannot be converted to the expected type. | `ErrorResponse` with `Invalid request parameter`. | | `401` | Unauthorized | `GlobalExceptionHandler.handleAuthentication`; `SecurityConfig` authentication entry point via `ApiErrorWriter` | Login authentication fails or an unauthenticated request targets a protected route. | `ErrorResponse` with `Invalid username or password` for failed login; `ErrorResponse` with `Unauthorized` for protected routes without valid authentication. | | `403` | Forbidden | `GlobalExceptionHandler.handleForbidden`; `SecurityConfig` access denied handler via `ApiErrorWriter` | Authenticated caller is not allowed to perform the requested operation. | `ErrorResponse` with the exception message or `Forbidden`. | | `404` | Not Found | `GlobalExceptionHandler.handleNotFound` | Requested domain resource does not exist or should be hidden from the caller. | `ErrorResponse` with the exception message. | | `404` | Not Found | `GlobalExceptionHandler.handleNoResourceFound` | No controller route or static resource matches the request. | `ErrorResponse` with `Not found`. | | `409` | Conflict | `GlobalExceptionHandler.handleConflict`; `AuthController.register` throws `ConflictException` | Duplicate username or other persistence/domain uniqueness conflict. | `ErrorResponse` with the exception message. | | `422` | Unprocessable Entity | `GlobalExceptionHandler.handleValidation` | Bean Validation fails for a request body annotated with `@Valid`. | `ValidationErrorResponse` with `error` and per-field `fields`. | ## Database - The schema is the basic requirements for a User, Note and NoteShare. - H2 database is used for development. - Flyway will allow us to migrate to Postgres for "production". | Table | Column | Type | Required | Key / Constraint | Description | |---|---|---:|:---:|---|---| | `users` | `id` | `UUID` | Yes | Primary key | Unique user identifier. | | `users` | `username` | `VARCHAR(100)` | Yes | Unique: `uq_users_username` | User login handle. | | `users` | `password_hash` | `VARCHAR(255)` | Yes | | BCrypt password hash. | | `users` | `created_at` | `TIMESTAMP WITH TIME ZONE` | Yes | | Account creation timestamp. | | | | `notes` | `id` | `UUID` | Yes | Primary key | Unique note identifier. | | `notes` | `owner_id` | `UUID` | Yes | FK to `users(id)`; indexed by `idx_notes_owner_id` | User who owns the note. | | `notes` | `title` | `VARCHAR(255)` | Yes | | Note title. | | `notes` | `content` | `TEXT` | Yes | | Note body content. | | `notes` | `created_at` | `TIMESTAMP WITH TIME ZONE` | Yes | | Note creation timestamp. | | `notes` | `updated_at` | `TIMESTAMP WITH TIME ZONE` | Yes | | Last note update timestamp. | | | | `note_shares` | `id` | `UUID` | Yes | Primary key | Unique share identifier. | | `note_shares` | `note_id` | `UUID` | Yes | FK to `notes(id)` with `ON DELETE CASCADE`; indexed by `idx_note_shares_note_id` | Note being shared. | | `note_shares` | `shared_with_user_id` | `UUID` | Yes | FK to `users(id)`; indexed by `idx_note_shares_shared_with_user_id` | User receiving read-only access. | | `note_shares` | `created_at` | `TIMESTAMP WITH TIME ZONE` | Yes | | Share creation timestamp. | | `note_shares` | `note_id`, `shared_with_user_id` | `UUID`, `UUID` | Yes | Unique: `uq_note_shares_note_user` | Prevents duplicate shares for the same note and user. | ## References A list of projects looked at for reference. - https://github.com/spring-petclinic/spring-petclinic-rest - https://github.com/spring-projects/spring-petclinic - https://github.com/gothinkster/spring-boot-realworld-example-app - https://github.com/spring-projects/spring-data-examples