Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
31a277a
chore(deps): Add validation and testcontainers dependencies
ebouchut Jun 18, 2026
2076d2c
docs(plan): Add dev implementation plan for session authentication
ebouchut Jun 18, 2026
5baacdc
docs(adr): Add ADR-0005 hand-written Liquibase database migrations
ebouchut Jun 18, 2026
269493b
test(persistence): Add Testcontainers PostgreSQL base class
ebouchut Jun 18, 2026
cacc673
docs(adr): Add ADR-0006 to test against real PostgreSQL instead of H2
ebouchut Jun 18, 2026
2488d8d
docs(adr): Add ADR-0007 to use PostgreSQL instead of MySQL
ebouchut Jun 19, 2026
801b767
feat(role): Add Role entity and repository
ebouchut Jun 19, 2026
5c30445
docs(contributing): Document running tests with Podman and Testcontai…
ebouchut Jun 19, 2026
960cbbd
refactor(test): Start the smoke test on its own Testcontainers Postgr…
ebouchut Jun 19, 2026
cb43bd0
chore(make): Add make test target with Podman Testcontainers support
ebouchut Jun 19, 2026
71893f7
docs(adr): Add ADR-0008 to share a singleton Testcontainers PostgreSQL
ebouchut Jun 23, 2026
ecb7d93
test(persistence): Share a singleton Testcontainers PostgreSQL across…
ebouchut Jun 23, 2026
f310421
feat(user): Add User entity and repository
ebouchut Jun 23, 2026
132274a
docs(contributing): clarify ADR guidelines
ebouchut Jun 23, 2026
78979ab
feat(role): Seed the STUDENT, INSTRUCTOR and ADMIN roles
ebouchut Jun 23, 2026
c46b701
docs(user): Reword javadoc for userId
ebouchut Jun 23, 2026
4a48ae5
test(role): Clarify tests use containerized Postgres DB instead of H2
ebouchut Jun 23, 2026
ab22e49
feat(security): Add session form login config under the /auth prefix
ebouchut Jun 23, 2026
7a7ba11
feat(auth): Add CustomUserDetailsService to load users for Spring Sec…
ebouchut Jun 25, 2026
db0f7ba
feat(auth): Add registration service with default STUDENT role
ebouchut Jun 25, 2026
b101a3b
docs(readme): Fix formatting of contributing section
ebouchut Jun 26, 2026
60c25fc
feat(auth): Add auth controller and Thymeleaf pages
ebouchut Jun 30, 2026
0cee5aa
chore(config): Disable Thymeleaf cache in dev for hot reload
ebouchut Jun 30, 2026
aa3df5d
docs(adr): Add ADR-0009 to run tests under Surefire, not Failsafe
ebouchut Jun 30, 2026
66b7693
test(auth): Add end-to-end register, login, dashboard flow test
ebouchut Jun 30, 2026
8150a8c
feat(security): Harden the session cookie (HttpOnly, SameSite=Lax)
ebouchut Jun 30, 2026
f270f36
docs(readme): Add links to CONTRIBUTING sections
ebouchut Jun 30, 2026
55016a6
docs: Add GLOSSARY, tech-stacks, and ARCHITECTURE
ebouchut Jul 1, 2026
606f506
chore(make): Add run target to start databases and the app
ebouchut Jul 1, 2026
5b87bfc
docs(plan): Mark session-auth plan steps complete
ebouchut Jul 1, 2026
531de98
docs(readme): Expand run instructions and link project docs
ebouchut Jul 1, 2026
e69a293
docs(contributing): Refine dependency and testing guidance
ebouchut Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ docs/database/merise/learn-dev_mld.mcd
docs/database/merise/learn-dev_mld.md
docs/database/merise/learn-dev_mld_geo.json

docs/database/ddl/learn-dev_geo.json
docs/database/ddl/learn-dev.mcd
docs/database/ddl/learn-dev.svg

### ~~~~~~~~~~~~~~~~~~~~~~~~~~
### Python Virtual Environment
### ~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
145 changes: 145 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Architecture

How learn-dev is structured and how a request flows through it. This file answers
**"how do the pieces fit together, and why is it built this way?"** For the list of
tools and versions see [docs/tech-stacks.md](docs/tech-stacks.md); for term
definitions see [GLOSSARY.md](GLOSSARY.md); for individual decisions and their
trade-offs see the [ADRs](docs/adr/README.md).

## Overview

learn-dev is a server-rendered, layered Spring Boot monolith. The browser talks to
Spring MVC controllers; controllers delegate to services; services use Spring Data
JPA repositories over a PostgreSQL relational core. HTML is produced server-side by
Thymeleaf. The app is a monolith today, with a longer-term intent to split selected
concerns into microservices (which is why service-to-service auth is already being
considered in [ADR-0002](docs/adr/0002-service-to-service-auth-via-service-token.md)).

## Architectural style

- **Server-rendered MVC.** Controllers return logical view names, not HTML or JSON;
a Thymeleaf `ViewResolver` renders the corresponding template.
- **Layered.** Each layer depends only on the one below it:

```
Browser
│ HTTP (form posts, GETs)
Controller (web layer: @Controller, request mapping, validation)
│ calls
Service (business logic, @Transactional boundaries)
│ calls
Repository (Spring Data JPA interfaces)
│ SQL via Hibernate
PostgreSQL (relational core)
```

## Package structure

Packages are organised by **feature**, not by technical layer, so a feature's
controller, service, entity, and repository live together:

```
com.ericbouchut.learndev
├── auth # AuthController, RegistrationService, CustomUserDetailsService,
│ # dto/RegisterForm, exception/Duplicate*Exception
├── user # entity/User, repository/UserRepository
├── role # entity/Role, repository/RoleRepository
├── common
│ └── config # SecurityConfig (filter chain, PasswordEncoder)
└── (test) support # AbstractPostgresIT (shared Testcontainers base)
```

## Request and rendering flow

1. A controller method (for example `AuthController.home()`) returns a **view name**
such as `"home"` (a lookup key, not HTML).
2. The Thymeleaf `ViewResolver` maps the name to `src/main/resources/templates/home.html`.
3. The template engine renders the HTML, evaluating `th:*` attributes, escaping
output (XSS defence), and injecting the CSRF token into forms that use `th:action`.
4. The `DispatcherServlet` writes the HTML as the response body with the appropriate
headers and status.

A method may instead return a `redirect:` prefix (for example
`redirect:/auth/login?registered`), which produces a `302` rather than rendering a view.

## Authentication and authorization

- **Session-based form login.** Credentials are verified once; the session is kept
server-side and referenced by the `JSESSIONID` cookie
(see [ADR-0001](docs/adr/0001-use-server-side-sessions-over-jwt.md)). JWT was
rejected for the browser flow.
- **Filter chain.** `SecurityConfig` defines the `SecurityFilterChain`: public paths
(`/`, `/auth/**`, static assets) are permitted; everything else requires
authentication. Auth endpoints are grouped under the `/auth/` URL prefix.
- **User loading.** `CustomUserDetailsService` loads a `User` by username and maps
each `Role` to a Spring Security authority prefixed with `ROLE_` (so `ADMIN`
becomes `ROLE_ADMIN`). Account flags map to `disabled` (`is_active`) and
`accountLocked` (`is_locked`).
- **Passwords.** Hashed with BCrypt; the raw password is never persisted.
- **CSRF.** Enabled by default; Thymeleaf injects a per-form token. `SameSite=Lax`
on the session cookie adds browser-level defence in depth.
- **Session cookie hardening.** `HttpOnly` and `SameSite=Lax` are set in the base
config; `Secure` is enabled once served over HTTPS.

### Registration flow

`RegisterForm` (validated with Bean Validation) → `AuthController` → `RegistrationService`
(checks unique username/email, hashes the password, assigns the default `STUDENT`
role, saves) → redirect to the login page. Duplicate username/email surface as
field errors on the re-rendered form.

## Data architecture

- **Relational core (PostgreSQL).** Users, roles, and (upcoming) courses/lessons.
Users use a **UUID** primary key to avoid enumeration; other tables use `BIGINT`
identity (see [ADR-0003](docs/adr/0003-uuid-pk-for-users-bigint-elsewhere.md)).
- **Document store (MongoDB).** Provisioned and configured for future content
storage; not yet used by any feature.
- **Schema evolution.** Managed by Liquibase, run at startup. Migrations are
hand-written formatted-SQL files, one atomic changeset per file, append-only
(see [ADR-0005](docs/adr/0005-handwrite-liquibase-migrations-over-mcd-ddl.md)).
- **Modelling.** The schema is designed in Merise (MCD → MLD → MPD) and cross-checked
against the migrations by a schema-drift CI job.

## Configuration and environments

- **Profiles.** `application.yaml` holds base config; `application-dev.yaml` holds
dev overrides. `SPRING_PROFILES_ACTIVE=dev` selects the profile and the Liquibase
`dev` context.
- **Secrets.** Loaded from `./.env` (a 1Password-filled FIFO) via spring-dotenv at
startup, so the working directory must be the project root.

## Testing strategy

Tests form a pyramid, all run under Surefire in `mvn test`
(see [ADR-0009](docs/adr/0009-run-tests-under-surefire-not-failsafe.md)):

- **Unit tests** — mock collaborators (Mockito), no I/O
(`RegistrationServiceTest`, `CustomUserDetailsServiceTest`).
- **Slice tests** — `@DataJpaTest` against a real Postgres container
(`UserRepositoryTest`, `RoleRepositoryTest`).
- **Integration test** — full context + MockMvc for the end-to-end auth journey
(`AuthFlowTest`).
- **Smoke test** — verifies the context starts (`LearnDevApplicationTests`).

Tests use a real PostgreSQL via Testcontainers rather than H2
(see [ADR-0006](docs/adr/0006-test-against-real-postgres-testcontainers.md)), shared
as a static singleton container (see [ADR-0008](docs/adr/0008-share-singleton-testcontainers-postgres.md)).

## Build and run

- `make test` — run the suite (Podman-aware Testcontainers wiring).
- `make run` — start the databases and run the app (`http://localhost:8080/`).
- `docker compose up -d` — start Postgres and Mongo (`docker` is Podman here).

## Direction of travel

- Password-reset flow with email (Mailpit locally, see
[ADR-0004](docs/adr/0004-use-mailpit-as-local-smtp-catcher.md)).
- Possible extraction of microservices, with service-to-service authentication
([ADR-0002](docs/adr/0002-service-to-service-auth-via-service-token.md)).
- A `SUPERADMIN` role (deferred under YAGNI; issue #65).
75 changes: 62 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ The code reference documentation is not yet available and will be added to this

#### Architecture Decision Records (ADR)

Significant architectural and design decisions are recorded as **ADRs** under
[`docs/adr/`](docs/adr/), using the **MADR** short form. ADRs are an
append-only, numbered log: a decision is never rewritten; a new ADR supersedes
an old one. Files are named `NNNN-short-title-in-kebab-case.md` (4-digit
zero-padded sequence). To add one, copy [`docs/adr/template.md`](docs/adr/template.md)
and add it to the index in [`docs/adr/README.md`](docs/adr/README.md).
Significant Architectural and design Decisions are Recorded as **ADRs** under
[`docs/adr/`](docs/adr/), as Markdown files using the **[MADR](https://adr.github.io/madr/)** structure.
ADRs are an append-only, numbered log: a decision is never rewritten.
A new ADR supersedes an old one.
Files are named `NNNN-short-title-in-kebab-case.md` (4-digit zero-padded sequence).

When creating an ADR use [`docs/adr/template.md`](docs/adr/template.md) as a template,
then add a link to the new ADR to the index in [`docs/adr/README.md`](docs/adr/README.md).

#### Architecture Overview

Expand Down Expand Up @@ -335,6 +337,17 @@ The main advantages in my opinion are:
> e.g.: **`learnDevApplication`**


#### URL / Routing Conventions

- **Authentication endpoints are grouped under the `/auth/` prefix**:
`/auth/login`, `/auth/register`, `/auth/logout` (and future `/auth/reset-password`).
This centralizes everything related to authentication and mirrors the
feature-based package layout (the `auth` package owns `/auth/**`).
- **Application pages stay at the root or under their own feature prefix**
(for example `/dashboard`, `/courses/**`), not under `/auth/`, since they are
not authentication actions.


#### Database

The *learn-dev* platform uses a **[PostgreSQL](https://www.postgresql.org/)** relational database to persist entities.
Expand Down Expand Up @@ -886,10 +899,7 @@ TODO: Explain how and where to update the database schema

### Add a Dependency

We use different package/dependencies managers on the backend and the frontend:

- `Maven` on the backend
- `npm` on the frontend
We use `Maven` as a packages/dependencies manager on the backend.


### Add a Backend Dependency
Expand All @@ -915,21 +925,60 @@ We use different package/dependencies managers on the backend and the frontend:
5. Verify the dependency resolves correctly:

```shell
cd backend && mvn dependency:resolve
mvn dependency:resolve
```


### Writing Tests

TODO: Explain how to write tests, what naming convention and best practices

#### Test Naming Conventions

- The file name of a test class should end in `Test`.
Although this is counterintuitive and the opposite of the standard Java
method naming convention, it makes the test output easier to read.


### Running Tests

TODO:
Repository and integration tests run against a **real PostgreSQL** started by
[Testcontainers](https://testcontainers.com/) (see ADR-0006), so a container
engine must be running. This project uses **Podman**.

#### Run All Tests

TODO: Explain how to run tests
The simplest way is to use ` make test`, which configures *Testcontainers* for
*Podman* automatically:

```bash
make test
```

It is equivalent to `./mvnw test` plus the Podman wiring described below.

#### Podman setup for Testcontainers

*Testcontainers* looks for a _Docker_ socket at `/var/run/docker.sock`,
which does not exist under _Podman_.

The workaround is to define two environment variables:

```bash
# Point Testcontainers at the Podman socket (resolved dynamically):
export DOCKER_HOST="unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')"

# Ryuk (the Testcontainers reaper) misbehaves under rootless Podman, so disable it:
export TESTCONTAINERS_RYUK_DISABLED=true
```

Add these to your shell profile (for example `~/.zshrc`),
source it, then run `./mvnw test` directly, or just use `make test`,
which sets them for you.
Make sure the Podman machine is started first: `podman machine start`.

On real Docker (for example in CI) neither variable is needed; `make test`
falls back to a plain `./mvnw test`.


### Generating the Documentation
Expand Down
127 changes: 127 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Glossary

Definitions of the domain and technical terms used across the learn-dev
project. For the concrete tools and versions, see [docs/tech-stacks.md](docs/tech-stacks.md);
for how the pieces fit together, see [ARCHITECTURE.md](ARCHITECTURE.md); for the
rationale behind design decisions, see the [ADRs](docs/adr/README.md).

## Domain terms

- **Archive** — Unpublish a course or lesson so it is no longer available to
students, without deleting it.
- **Course** — A unit of learning content owned by an instructor; contains lessons.
- **Deactivate** — Disable an account (for example an instructor or student) so it
can no longer be used, without deleting it. See also *disabled account*.
- **Drop a course** — A student withdrawing from a course before finishing it.
- **Enrollment** — The relationship linking a student to a course they have joined.
- **Lesson** — An individual piece of content within a course.
- **Role** — A named set of permissions granted to a user. The seeded roles are
`STUDENT`, `INSTRUCTOR`, and `ADMIN`; `SUPERADMIN` is planned (see issue #65).

## Authentication and security

- **Authority** — In Spring Security, a single granted permission string held by an
authenticated user. Roles are represented as authorities prefixed with `ROLE_`
(for example the `ADMIN` role becomes the authority `ROLE_ADMIN`).
- **BCrypt** — An adaptive password-hashing function. Passwords are stored as BCrypt
hashes, never in clear text.
- **CSRF (Cross-Site Request Forgery)** — An attack that tricks a logged-in user's
browser into submitting an unwanted request. Defended with a per-form token
(injected by Thymeleaf) and the `SameSite` cookie attribute.
- **Disabled account** — An account that exists but is not allowed to authenticate
(mapped from the `is_active = false` flag). Distinct from a *locked account*.
- **HttpOnly** — A cookie attribute that hides the cookie from client-side
JavaScript, mitigating session theft via XSS.
- **IDOR (Insecure Direct Object Reference)** — An access-control flaw where a
client-supplied identifier is trusted without an authorization check. Using UUID
primary keys for users mitigates enumeration (see [ADR-0003](docs/adr/0003-uuid-pk-for-users-bigint-elsewhere.md)).
- **Locked account** — An account temporarily blocked from authenticating (for
example after too many failed logins), mapped from the `is_locked` flag. Distinct
from a *disabled account*.
- **Principal** — The currently authenticated entity (typically the user) within a
security context.
- **SameSite** — A cookie attribute controlling whether the browser sends the cookie
on cross-site requests. Set to `Lax` here as CSRF defense in depth.
- **Secure (cookie)** — A cookie attribute that restricts the cookie to HTTPS.
Enabled only once the app is served over TLS.
- **Session (server-side)** — Authentication state kept on the server and referenced
by a session cookie (`JSESSIONID`), rather than a self-contained token
(see [ADR-0001](docs/adr/0001-use-server-side-sessions-over-jwt.md)).
- **XSS (Cross-Site Scripting)** — Injection of malicious scripts into pages viewed
by other users. Mitigated by Thymeleaf's automatic output escaping and `HttpOnly`.

## Persistence and data modelling

- **Changelog / Changeset (Liquibase)** — A changelog is the ordered list of
migrations; a changeset is one atomic migration, identified by `path::id::author`.
- **ERD (Entity-Relationship Diagram)** — A diagram of entities and their
relationships (rendered here with Mermaid).
- **Hibernate** — The JPA implementation (ORM) used to map Java entities to tables.
- **JPA (Jakarta Persistence API)** — The standard Java API for object-relational
mapping; implemented by Hibernate.
- **JSESSIONID** — The default name of the servlet session cookie.
- **Liquibase** — The database schema migration tool. Migrations are hand-written
formatted-SQL files applied at startup (see [ADR-0005](docs/adr/0005-handwrite-liquibase-migrations-over-mcd-ddl.md)).
- **Merise** — A French data-modelling method producing three views: MCD, MLD, MPD.
- **MCD (Modele Conceptuel de Donnees)** — Conceptual data model; the entities and
relationships independent of any database.
- **MLD (Modele Logique des Donnees)** — Logical data model; the relational schema
(tables, keys) derived from the MCD.
- **MPD (Modele Physique des Donnees)** — Physical data model; the concrete schema
as implemented in PostgreSQL.
- **ORM (Object-Relational Mapping)** — Mapping between Java objects and relational
tables; provided by Hibernate/JPA.
- **UUID** — A 128-bit identifier used as the primary key for users to avoid
sequential-id enumeration.

## Build, testing, and tooling

- **ADR (Architecture Decision Record)** — A short, numbered, append-only document
capturing one design decision and its trade-offs, in MADR format.
- **Bean Validation** — The Jakarta standard for declaring constraints
(`@NotBlank`, `@Email`, `@Size`) on form/DTO fields, enforced with `@Valid`.
- **DTO (Data Transfer Object)** — An object carrying data across a boundary,
deliberately separate from entities. A `...Form` DTO backs an HTML form.
- **Failsafe** — The Maven plugin that runs `*IT` integration tests in the `verify`
phase. This project does **not** use it (see [ADR-0009](docs/adr/0009-run-tests-under-surefire-not-failsafe.md)).
- **FIFO (named pipe)** — A special file that streams data on read. The project's
`.env` is a FIFO filled by 1Password; shell `source` cannot read it (0-byte stat).
- **HikariCP** — The JDBC connection pool bundled with Spring Boot.
- **Integration test** — A test that boots a Spring context and exercises multiple
layers together (here `@SpringBootTest` against a real Postgres container).
- **Lombok** — A library that generates boilerplate (getters, constructors) from
annotations at compile time.
- **MADR (Markdown ADR)** — The lightweight ADR template format used in `docs/adr/`.
- **Slice test** — A test that loads only one layer of the context (for example
`@DataJpaTest` for the persistence layer).
- **Smoke test** — A minimal test that the application context starts at all
(`LearnDevApplicationTests`).
- **Surefire** — The Maven plugin that runs `*Test`/`*Tests` unit and integration
tests in the `test` phase. All tests here run under Surefire.
- **Testcontainers** — A library that starts throwaway Docker/Podman containers for
tests; used to run a real PostgreSQL (see [ADR-0006](docs/adr/0006-test-against-real-postgres-testcontainers.md)).
- **Ryuk** — Testcontainers' companion container that cleans up resources; disabled
under Podman in this project.
- **YAGNI (You Aren't Gonna Need It)** — The principle of not building features
until they are actually needed (for example deferring the `SUPERADMIN` role).

## Infrastructure and process

- **Docker Compose** — Declarative multi-container orchestration; here it runs
Postgres and Mongo. `docker` on the dev machine is Podman.
- **GitButler** — The version-control tool wrapping Git; used via the `but` CLI when
the current branch is `gitbutler/workspace`.
- **Podman** — A daemonless container engine, used as the `docker` drop-in.
- **Spring profile** — A named configuration set (for example `dev`) selecting
profile-specific properties and Liquibase contexts.
- **Thymeleaf** — The server-side HTML template engine. Its Spring Security
**dialect** (`sec:` namespace) exposes the authenticated user to templates.

## Certification

- **CCP (Certificat de Competences Professionnelles)** — A competency block of a
French Titre Professionnel; the DWWM has a front-end and a back-end CCP.
- **DWWM (Developpeur Web et Web Mobile)** — The French Titre Professionnel this
capstone targets.
- **REAC (Referentiel Emploi Activites Competences)** — The official competency
reference framework defining what the certification assesses.
Loading
Loading