@@ -66,12 +66,14 @@ graph TD
6666 end
6767
6868 DB[("PostgreSQL Database")]
69+ MP["Mailpit (fake SMTP, dev only)"]
6970
7071 U -->|"HTTP GET / form POST"| Security
7172 Security --> Controllers
7273 Controllers --> Services
7374 Services --> Repositories
7475 Repositories -->|"SQL (Hibernate)"| DB
76+ Services -->|"SMTP (password reset email)"| MP
7577 Controllers -->|"view name + model"| Views
7678 Views -->|"HTML page"| U
7779```
@@ -113,6 +115,54 @@ Registration (`POST /auth/register`) is handled by `AuthController` and
113115` RegistrationService ` (server-side bean validation plus duplicate
114116username/email detection).
115117
118+ ##### Password Reset Sequence Diagram
119+
120+ ``` mermaid
121+ sequenceDiagram
122+ actor U as User (Browser)
123+ participant C as PasswordResetController
124+ participant S as PasswordResetService
125+ participant DB as PostgreSQL
126+ participant M as Mailpit (SMTP, dev)
127+
128+ U->>C: GET /auth/forgot-password
129+ C-->>U: Email form (CSRF token)
130+
131+ U->>C: POST /auth/forgot-password (email)
132+ C->>S: requestReset(email, ip, resetUrlBase)
133+ alt Unknown email or rate limit exceeded
134+ S->>DB: Record the attempt in audit_logs
135+ else Known email, within the rate limit
136+ S->>DB: Invalidate the outstanding tokens
137+ S->>DB: Store the SHA-256 hash of a new random token
138+ S->>M: Email the reset link (raw token in the URL)
139+ S->>DB: Record the request in audit_logs
140+ end
141+ C-->>U: 302 redirect to ?sent (same answer either way)
142+
143+ U->>C: GET /auth/reset-password?token=...
144+ C->>S: findUsableToken(raw)
145+ S->>DB: SELECT by SHA-256(token), unused, not expired
146+ C-->>U: New password form (or invalid link message)
147+
148+ U->>C: POST /auth/reset-password (token + new password)
149+ C->>S: resetPassword(raw, newPassword, ip)
150+ S->>DB: Store the BCrypt hash, consume the token,<br/>invalidate the others, audit
151+ C-->>U: 302 redirect to /auth/login?reset
152+ ```
153+
154+ The flow is ** enumeration-safe** : whether the email exists or not (or the
155+ per-user / per-IP rate limit was exceeded), the browser gets the same
156+ neutral confirmation, so an attacker cannot use the form to discover
157+ accounts. Only the ** SHA-256 hash** of the token is stored; the raw token
158+ appears once, in the emailed link. A token is ** single-use** , only one is
159+ active per user at a time, and it expires after ** 30 minutes** (tunable
160+ under ` learndev.password-reset.* ` in ` application.yaml ` ). Every request
161+ and reset attempt is recorded in the ` audit_logs ` table by ` AuditService ` .
162+ In development the email lands in [ Mailpit] ( http://localhost:8025 ) ; the
163+ end-to-end journey (request, email, link, new password) is covered by
164+ ` PasswordResetFlowTest ` .
165+
116166#### Design: Mockups and Wireframes
117167
118168The frontend look and feel is specified before code:
@@ -158,9 +208,14 @@ The Spring Boot application lives at the **repository root** (standard Maven lay
158208``` txt
159209learn-dev/
160210├── .env.example # Template for the local .env file (see README)
211+ ├── .github/
212+ │ └── workflows/ # CI: one workflow per concern (build, test, lint, schema drift)
161213├── .sdkmanrc # Pins the Java and Maven versions (SDKMAN)
162214├── Makefile # Developer shortcuts (run, test, diagrams, ...)
163- ├── docker-compose.yaml # PostgreSQL and MongoDB services
215+ ├── config/
216+ │ └── checkstyle/
217+ │ └── checkstyle.xml # Project Checkstyle ruleset (advisory lint)
218+ ├── docker-compose.yaml # PostgreSQL, MongoDB, and Mailpit services
164219├── mvnw # Maven wrapper
165220├── pom.xml # Dependencies, build configuration, and metadata
166221│
@@ -171,28 +226,49 @@ learn-dev/
171226├── docs/
172227│ ├── adr/ # Architecture Decision Records (MADR)
173228│ ├── database/merise/ # MCD, MLD, MPD diagrams and sources
229+ │ ├── design/ # Design tokens study, HTML mockups, Figma links
174230│ ├── plans/ # Implementation plans
231+ │ ├── rgaa.md # Accessibility (RGAA) criteria map
175232│ └── tech-stacks.md # Catalogue of tools and frameworks
176233│
177234└── src/
178235 ├── main/
179236 │ ├── java/com/ericbouchut/learndev/
180237 │ │ ├── LearnDevApplication.java # Spring Boot entry point
181238 │ │ │
182- │ │ ├── auth/ # Authentication (registration, login support)
239+ │ │ ├── audit/ # Security audit trail (audit_logs table)
240+ │ │ │ ├── AuditService.java # Records auditable events (reset requests, ...)
241+ │ │ │ ├── entity/
242+ │ │ │ │ └── AuditLog.java
243+ │ │ │ └── repository/
244+ │ │ │ └── AuditLogRepository.java
245+ │ │ │
246+ │ │ ├── auth/ # Authentication (registration, login, password reset)
183247 │ │ │ ├── AuthController.java # Web pages: home, login, dashboard, register
184248 │ │ │ ├── CustomUserDetailsService.java # Loads user + roles from DB for Spring Security
249+ │ │ │ ├── PasswordResetController.java # Forgot password and reset password pages
250+ │ │ │ ├── PasswordResetMailer.java # Sends the reset link over SMTP
251+ │ │ │ ├── PasswordResetService.java # Token issue/consume, rate limit, enumeration safety
185252 │ │ │ ├── RegistrationService.java # Creates accounts (hashing, default role, duplicates)
186253 │ │ │ ├── dto/
187- │ │ │ │ └── RegisterForm.java # Registration form backing bean (bean validation)
188- │ │ │ └── exception/
189- │ │ │ ├── DuplicateEmailException.java
190- │ │ │ └── DuplicateUsernameException.java
254+ │ │ │ │ ├── ForgotPasswordForm.java
255+ │ │ │ │ ├── RegisterForm.java # Form backing beans (bean validation)
256+ │ │ │ │ └── ResetPasswordForm.java
257+ │ │ │ ├── entity/
258+ │ │ │ │ └── PasswordResetToken.java # Maps to the reset_tokens table
259+ │ │ │ ├── exception/
260+ │ │ │ │ ├── DuplicateEmailException.java
261+ │ │ │ │ └── DuplicateUsernameException.java
262+ │ │ │ └── repository/
263+ │ │ │ └── PasswordResetTokenRepository.java
191264 │ │ │
192265 │ │ ├── common/ # Concerns shared across features
193266 │ │ │ └── config/
194267 │ │ │ └── SecurityConfig.java # Spring Security filter chain, form login, PasswordEncoder
195268 │ │ │
269+ │ │ ├── legal/ # Legal pages
270+ │ │ │ └── LegalController.java # Privacy policy page (French)
271+ │ │ │
196272 │ │ ├── role/ # Role management
197273 │ │ │ ├── entity/
198274 │ │ │ │ └── Role.java # Maps to the roles table
@@ -206,19 +282,28 @@ learn-dev/
206282 │ │ └── UserRepository.java
207283 │ │
208284 │ └── resources/
209- │ ├── application.yaml # Main config (datasource, Liquibase, session cookie)
285+ │ ├── application.yaml # Main config (datasource, Liquibase, mail, session cookie)
210286 │ ├── application-dev.yaml # Dev profile overrides
211287 │ ├── application-prod.yaml # Prod profile overrides
212288 │ ├── db/
213289 │ │ └── changelog/ # Liquibase migrations
214290 │ │ ├── db.changelog-master.yaml # Master changelog (includeAll on changes/)
215291 │ │ └── changes/
216- │ │ └── V20260608161836-create-users-table.sql # One changeset per file
292+ │ │ ├── V20260608161836-create-users-table.sql # One changeset per file
293+ │ │ └── ... # Tables, indexes, seeds (applied in filename order)
294+ │ ├── static/
295+ │ │ ├── css/ # Design system: base.css, theme and font stylesheets
296+ │ │ └── fonts/ # Self-hosted webfonts (OFL license files alongside)
217297 │ └── templates/ # Thymeleaf views (server-rendered HTML)
298+ │ ├── fragments/
299+ │ │ └── layout.html # Shared head, header (nav), and footer fragments
218300 │ ├── dashboard.html
301+ │ ├── forgot-password.html
219302 │ ├── home.html
220303 │ ├── login.html
221- │ └── register.html
304+ │ ├── privacy.html # Privacy policy (French)
305+ │ ├── register.html
306+ │ └── reset-password.html
222307 │
223308 └── test/
224309 └── java/com/ericbouchut/learndev/
@@ -227,8 +312,12 @@ learn-dev/
227312 ├── auth/
228313 │ ├── AuthFlowTest.java # End-to-end register, login, dashboard flow (MockMvc + Testcontainers)
229314 │ ├── CustomUserDetailsServiceTest.java
315+ │ ├── PasswordResetFlowTest.java # End-to-end password reset through a real Mailpit container
230316 │ └── RegistrationServiceTest.java
231317 │
318+ ├── legal/
319+ │ └── PrivacyPageTest.java # The public privacy page renders
320+ │
232321 ├── role/repository/
233322 │ └── RoleRepositoryTest.java # @DataJpaTest with Testcontainers
234323 │
@@ -244,10 +333,13 @@ The table below explains what each folder entails.
244333| Folder | Purpose |
245334| -------------------------------------| ---------------------------------------------------------------------------|
246335| ` src/main/java/.../learndev/ ` | Spring Boot application root: entry point and top-level package |
247- | ` src/main/java/.../learndev/auth/ ` | Authentication: registration flow, login support, auth pages |
336+ | ` src/main/java/.../learndev/audit/ ` | Security audit trail: records auditable events in ` audit_logs ` |
337+ | ` src/main/java/.../learndev/auth/ ` | Authentication: registration, login support, password reset, auth pages |
248338| ` src/main/java/.../learndev/common/ ` | Cross-cutting concerns: Spring Security configuration |
339+ | ` src/main/java/.../learndev/legal/ ` | Legal pages (privacy policy) |
249340| ` src/main/java/.../learndev/role/ ` | Role entity and data access |
250341| ` src/main/java/.../learndev/user/ ` | User entity and data access |
342+ | ` src/main/resources/static/ ` | Design system stylesheets and self-hosted webfonts |
251343| ` src/main/resources/templates/ ` | Thymeleaf views rendered server-side |
252344| ` src/main/resources/db/changelog/ ` | Liquibase database migrations |
253345| ` src/test/java/.../learndev/ ` | Tests (unit and Testcontainers-backed integration tests, all ` *Test ` ) |
@@ -911,8 +1003,12 @@ TODO: Explain how to write tests, what naming convention and best practices
9111003# ## Running Tests
9121004
9131005Repository and integration tests run against a ** real PostgreSQL** started by
914- [Testcontainers](https://testcontainers.com/) (see ADR-0006), so a container
915- engine must be running. This project uses ** Podman** .
1006+ [Testcontainers](https://testcontainers.com/)
1007+ (see [ADR-0006](docs/adr/0006-test-against-real-postgres-testcontainers.md)),
1008+ so a container engine must be running. This project uses ** Podman** .
1009+ ` PasswordResetFlowTest` also starts a ** Mailpit** container the same way, to
1010+ receive the password reset email: no locally running Mailpit is needed to run
1011+ the tests.
9161012
9171013# ### Run All Tests
9181014
0 commit comments