The project is a browser-based blog with a React single-page application, a stateless Spring Boot API, and PostgreSQL. The browser calls the API directly. Azure resources and static frontend upload are represented in infra/.
flowchart TB
subgraph Browser[Browser]
UI[React SPA]
Auth[AuthProvider]
API[Axios ApiService]
Editor[TipTap Editor]
Sanitize[DOMPurify Renderer]
Token[(localStorage JWT)]
UI --> Auth
UI --> Editor
UI --> Sanitize
UI --> API
Auth <--> Token
API -. reads .-> Token
end
subgraph Backend[Spring Boot API]
Filter[Security Filter Chain]
Controllers[REST Controllers]
Mappers[MapStruct Mappers]
Services[Application Services]
Repositories[Spring Data Repositories]
Filter --> Controllers
Controllers --> Services
Controllers <--> Mappers
Services --> Repositories
end
DB[(PostgreSQL)]
API -->|HTTPS + JSON| Filter
Repositories -->|JPA / SQL| DB
config/: Spring Security and CORS configuration.controllers/: HTTP routes and DTO conversion.domain/dtos/: external request/response contracts.domain/entities/: JPA persistence model.mappers/: MapStruct mappings and published-post counts.repositories/: Spring Data query interfaces.security/: user-details adapter and JWT request filter.services/: business interfaces and implementations.
For a protected request, JwtAuthenticationFilter reads a Bearer token, asks AuthenticationService to parse it, loads the user by email, places authentication in the security context, and adds the user's UUID as the userId request attribute. Controllers use that attribute when ownership context is needed, currently for creating posts and listing drafts.
Controllers call services with domain request objects. Services resolve related entities, apply rules, and call repositories. MapStruct converts entities to API DTOs. ErrorController translates common exceptions into API errors.
sequenceDiagram
autonumber
actor Browser
participant API as Axios ApiService
participant JWT as JwtAuthenticationFilter
participant Auth as AuthenticationService
participant Users as UserDetailsService
participant Controller
participant Service
participant DB as PostgreSQL
Browser->>API: Protected request
API->>JWT: Authorization Bearer token
JWT->>Auth: validateToken(token)
Auth->>Users: loadUserByUsername(email)
Users->>DB: Find user by email
DB-->>Users: User
Users-->>JWT: BlogUserDetails
JWT->>JWT: Set SecurityContext and userId
JWT->>Controller: Continue filter chain
Controller->>Service: Invoke use case
Service->>DB: Read or write entities
DB-->>Service: Result
Service-->>Controller: Domain result
Controller-->>API: JSON response
API-->>Browser: Render result
- The API is stateless and CSRF is disabled.
- Login, public post reads, category reads, and tag reads are anonymous.
- All other routes require authentication.
- JWTs are HMAC-SHA256 signed and expire after 24 hours.
- Every authenticated user receives
ROLE_USER; there is no admin role or per-resource authorization. - A default user is seeded during
UserDetailsServicebean creation.
Two independent CORS configurations contain the same hard-coded production origins. Consolidating these into one environment-driven configuration would reduce drift and enable localhost development.
AuthProvider owns the token and coarse authenticated state. apiService is a singleton Axios wrapper that adds the JWT and redirects to /login after a 401.
| Route | Component | Access |
|---|---|---|
/ |
HomePage |
Public |
/login |
LoginPage |
Public |
/posts/new |
EditPostPage |
Protected in the client |
/posts/:id |
PostPage |
Public |
/posts/:id/edit |
EditPostPage |
Protected in the client |
/categories |
CategoriesPage |
Public; mutations shown when authenticated |
/tags |
TagsPage |
Public; mutations shown when authenticated |
/posts/drafts |
DraftsPage |
Protected in the client and API |
The editor stores TipTap-generated HTML in the post content field. Display components sanitize it with DOMPurify, currently allowing only p, strong, em, and br; headings and lists produced by the editor are therefore stripped when rendered.
flowchart LR
Router[React Router] --> Public{Public routes}
Router --> Protected[ProtectedRoute]
Public --> Home[HomePage]
Public --> Login[LoginPage]
Public --> Post[PostPage]
Public --> Categories[CategoriesPage]
Public --> Tags[TagsPage]
Protected -->|JWT present| Drafts[DraftsPage]
Protected -->|JWT present| Edit[EditPostPage]
Protected -->|No JWT| Login
Home --> PostList[PostList]
Drafts --> PostList
Edit --> PostForm[PostForm + TipTap]
- A
Userauthors manyPostrecords. - A
Categorycontains many posts; each post requires one category. PostandTaghave a many-to-many relationship throughpost_tags.Post.statusisDRAFTorPUBLISHED.- Entity lifecycle callbacks maintain created/updated timestamps.
See data-model.md for conceptual, physical, lifecycle, and DTO-mapping diagrams, or ERD.md for the compact relationship reference.
- The client posts credentials to
/auth/login. - Spring's authentication manager validates them against the database and password encoder.
- The service signs a JWT whose subject is the email.
- The client stores the token in
localStorage.
sequenceDiagram
actor User
participant Login as LoginPage
participant API as AuthController
participant Manager as AuthenticationManager
participant Users as UserDetailsService
participant JWT as AuthenticationService
participant Store as localStorage
User->>Login: Submit email and password
Login->>API: POST /api/v1/auth/login
API->>Manager: Authenticate credentials
Manager->>Users: Load user and verify password
Users-->>Manager: BlogUserDetails
Manager-->>API: Authenticated
API->>JWT: generateToken(userDetails)
JWT-->>API: Signed 24-hour JWT
API-->>Login: token and expiresIn
Login->>Store: Save token
- The client submits rich-text HTML, category ID, tag IDs, and status.
- The JWT filter exposes the authenticated user ID.
- The service loads the user/category/tags and calculates reading time.
- JPA persists the post and join-table rows.
sequenceDiagram
actor Author
participant Form as PostForm
participant Controller as PostController
participant Users as UserService
participant Posts as PostService
participant Categories as CategoryService
participant Tags as TagService
participant DB as PostgreSQL
Author->>Form: Enter content, category, tags, status
Form->>Controller: POST /api/v1/posts + JWT
Controller->>Users: getUserById(userId)
Users->>DB: Load author
Controller->>Posts: createPost(author, request)
Posts->>Categories: Resolve categoryId
Categories->>DB: Load category
Posts->>Tags: Resolve tagIds
Tags->>DB: Load tags
Posts->>Posts: Calculate reading time
Posts->>DB: Save post and post_tags
DB-->>Controller: Persisted post
Controller-->>Form: 201 Created + PostDto
- The client requests posts plus optional category/tag IDs.
- The service selects one of four repository queries.
- All queries constrain status to
PUBLISHED. - Mappers construct nested author/category/tag DTOs.
The default Spring profile is dev. It points to local PostgreSQL and enables SQL logging. The prod profile reads database credentials and the JWT secret from environment variables.
The frontend currently uses a hard-coded Render API URL. Although Vite defines a proxy, the Axios base URL bypasses it. An intended improvement is:
const baseURL = import.meta.env.VITE_API_BASE_URL ?? '/api/v1';Terraform provisions a resource group, PostgreSQL Flexible Server, Linux App Service plan and backend Web App, and a storage-account static website for the built frontend. See terraform-readme.md for exact inputs and warnings.
flowchart LR
Developer[Developer / CI] -->|Deploy JAR| WebApp[Azure Linux Web App]
Developer -->|Upload dist assets| Static[Azure Storage Static Website]
Browser([Browser]) -->|Static files| Static
Browser -->|REST API| WebApp
WebApp -->|JDBC| PG[(Azure PostgreSQL Flexible Server)]
subgraph RG[Azure Resource Group]
Plan[App Service Plan] --> WebApp
Static
PG
end
- Add post ownership/role authorization to update and delete operations.
- Prevent public retrieval of drafts by ID.
- Replace hard-coded API URLs, CORS origins, and seeded credentials with environment configuration.
- Implement pagination/sorting contracts shared by backend and frontend.
- Add a user-profile endpoint so the client can validate tokens and display the signed-in user.
- Add database migrations (Flyway or Liquibase) instead of production
ddl-auto=update. - Add service/controller tests and frontend component/integration tests.
- Consolidate error handling, especially bean-validation failures.