Skip to content

Commit 4e0326d

Browse files
authored
Merge pull request #87 from ebouchut/docs/mailpit-and-reset-docs
docs: Document Mailpit and the password reset flow
2 parents a3a0c3e + 48c7b31 commit 4e0326d

6 files changed

Lines changed: 192 additions & 21 deletions

File tree

ARCHITECTURE.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ controller, service, entity, and repository live together:
4444

4545
```
4646
com.ericbouchut.learndev
47+
├── audit # AuditService, entity/AuditLog: security audit trail (audit_logs)
4748
├── auth # AuthController, RegistrationService, CustomUserDetailsService,
48-
│ # dto/RegisterForm, exception/Duplicate*Exception
49+
│ # PasswordResetController/Service/Mailer, entity/PasswordResetToken,
50+
│ # dto/*Form, exception/Duplicate*Exception
51+
├── legal # LegalController (privacy policy page)
4952
├── user # entity/User, repository/UserRepository
5053
├── role # entity/Role, repository/RoleRepository
5154
├── common
@@ -92,11 +95,26 @@ A method may instead return a `redirect:` prefix (for example
9295
role, saves) → redirect to the login page. Duplicate username/email surface as
9396
field errors on the re-rendered form.
9497

98+
### Password reset flow
99+
100+
`ForgotPasswordForm``PasswordResetController``PasswordResetService`. The
101+
service answers with the same neutral confirmation whether or not the email
102+
exists (no account enumeration), rate-limits requests per user and per IP,
103+
stores only the **SHA-256 hash** of a 32-byte random token (single active
104+
token per user, 30 minute TTL, configurable under `learndev.password-reset.*`),
105+
and emails the raw link via `PasswordResetMailer` over SMTP — Mailpit in
106+
development (see [ADR-0004](docs/adr/0004-use-mailpit-as-local-smtp-catcher.md)).
107+
Consuming the link stores the new BCrypt hash, marks the token used,
108+
invalidates any others, and records the outcome in `audit_logs` through
109+
`AuditService`. The sequence diagram lives in
110+
[CONTRIBUTING.md](CONTRIBUTING.md#password-reset-sequence-diagram).
111+
95112
## Data architecture
96113

97-
- **Relational core (PostgreSQL).** Users, roles, and (upcoming) courses/lessons.
98-
Users use a **UUID** primary key to avoid enumeration; other tables use `BIGINT`
99-
identity (see [ADR-0003](docs/adr/0003-uuid-pk-for-users-bigint-elsewhere.md)).
114+
- **Relational core (PostgreSQL).** Users, roles, password-reset tokens, the
115+
audit trail, and (upcoming) courses/lessons. Users use a **UUID** primary key
116+
to avoid enumeration; other tables use `BIGINT` identity
117+
(see [ADR-0003](docs/adr/0003-uuid-pk-for-users-bigint-elsewhere.md)).
100118
- **Document store (MongoDB).** Provisioned and configured for future content
101119
storage; not yet used by any feature.
102120
- **Schema evolution.** Managed by Liquibase, run at startup. Migrations are
@@ -134,12 +152,13 @@ as a static singleton container (see [ADR-0008](docs/adr/0008-share-singleton-te
134152

135153
- `make test` — run the suite (Podman-aware Testcontainers wiring).
136154
- `make run` — start the databases and run the app (`http://localhost:8080/`).
137-
- `docker compose up -d` — start Postgres and Mongo (`docker` is Podman here).
155+
- `docker compose up -d` — start Postgres, Mongo, and Mailpit (`docker` is
156+
Podman here). Mailpit's web UI (caught emails) is at `http://localhost:8025`.
138157

139158
## Direction of travel
140159

141-
- Password-reset flow with email (Mailpit locally, see
142-
[ADR-0004](docs/adr/0004-use-mailpit-as-local-smtp-catcher.md)).
160+
- The course and lesson domain (course catalogue, enrollment, Markdown lesson
161+
content).
143162
- Possible extraction of microservices, with service-to-service authentication
144163
([ADR-0002](docs/adr/0002-service-to-service-auth-via-service-token.md)).
145164
- A `SUPERADMIN` role (deferred under YAGNI; issue #65).

CONTRIBUTING.md

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
114116
username/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

118168
The 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
159209
learn-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
9131005
Repository 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

GLOSSAIRE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ pour la justification des décisions de conception, voir les [ADR](docs/adr/READ
3030

3131
## Authentification et sécurité
3232

33+
- **Énumération de comptes (account enumeration)** — Sonder un formulaire de
34+
connexion, d'inscription ou de réinitialisation de mot de passe pour savoir
35+
si un compte existe (par exemple via un message « email inconnu »). Contrée
36+
en répondant le même message neutre dans tous les cas, comme le fait le flux
37+
de réinitialisation de mot de passe.
3338
- **Authority (autorité)** — Dans Spring Security, une permission unitaire
3439
détenue par un utilisateur authentifié. Les rôles sont représentés comme des
3540
autorités préfixées par `ROLE_` (le rôle `ADMIN` devient l'autorité
@@ -54,6 +59,10 @@ pour la justification des décisions de conception, voir les [ADR](docs/adr/READ
5459
drapeau `is_locked`. À distinguer d'un *compte désactivé*.
5560
- **Principal** — L'entité actuellement authentifiée (en général l'utilisateur)
5661
dans un contexte de sécurité.
62+
- **Limitation de débit (rate limiting)** — Plafonner le nombre de fois qu'une
63+
opération peut être effectuée dans une fenêtre de temps, pour ralentir les
64+
abus et la force brute. Ici : les demandes de réinitialisation de mot de
65+
passe sont limitées par utilisateur et par adresse IP.
5766
- **SameSite** — Un attribut de cookie qui contrôle l'envoi du cookie par le
5867
navigateur sur les requêtes inter-sites. Positionné sur `Lax` ici comme
5968
défense en profondeur contre le CSRF.

GLOSSARY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ rationale behind design decisions, see the [ADRs](docs/adr/README.md).
2525

2626
## Authentication and security
2727

28+
- **Account enumeration** — Probing a login, registration, or password reset
29+
form to learn whether an account exists (for example from an "unknown email"
30+
error message). Countered by answering with the same neutral message either
31+
way, as the password reset flow does.
2832
- **Authority** — In Spring Security, a single granted permission string held by an
2933
authenticated user. Roles are represented as authorities prefixed with `ROLE_`
3034
(for example the `ADMIN` role becomes the authority `ROLE_ADMIN`).
@@ -45,6 +49,9 @@ rationale behind design decisions, see the [ADRs](docs/adr/README.md).
4549
from a *disabled account*.
4650
- **Principal** — The currently authenticated entity (typically the user) within a
4751
security context.
52+
- **Rate limiting** — Capping how many times an operation may be performed in a
53+
time window, to slow down abuse and brute force. Here: password reset
54+
requests are limited per user and per IP address.
4855
- **SameSite** — A cookie attribute controlling whether the browser sends the cookie
4956
on cross-site requests. Set to `Lax` here as CSRF defense in depth.
5057
- **Secure (cookie)** — A cookie attribute that restricts the cookie to HTTPS.

0 commit comments

Comments
 (0)