Веб-приложение для учёта читателей и книг: выдача и возврат, поиск в шапке (в разделе книг — по каталогу, в разделе читателей — по читателям), отдельные страницы /books/search и /people/search, публичная страница /about («О библиотеке») и футер с контактами на всех страницах с общим layout (templates/fragments/footer.html), пагинация и множественная сортировка списка книг (на странице /books — форма с чекбоксами; порядок ключей сортировки на бэкенде: название → автор → жанр → год). Читатель (роль USER) в карточке книги может отметить книгу прочитанной или снять отметку; список таких книг виден в /me. В каталоге и полнотекстовом поиске по книгам для читателя доступен фильтр «без прочитанных» (hide_read=true): в списке /books исключение выполняется в запросе к БД (корректная пагинация), в /books/search — после выборки по запросу. У каждой книги задаётся жанр из фиксированного списка (Genre, JPA ENUM строкой); при создании и редактировании жанр выбирается в форме, в списке, поиске и карточке жанр показывается текстом. При добавлении читателя (/people/new) создаётся учётка каталога со случайным временным паролем; читатель задаёт свой пароль по одноразовой ссылке из приветственного письма (Spring Mail, см. «Почта») или по ссылке, показанной библиотекарю на /people, если письмо не отправилось. Вход в каталог: логин в БД — нормализованный email (нижний регистр, без лишних пробелов); на форме входа можно указать email или номер читательского билета (как в карточке). Читатель с ролью USER может воспользоваться /forgot-password: при включённой почте уходит письмо с одноразовой ссылкой на /catalog/setup-password, при этом не раскрывается, существует ли такая учётка (см. «Сброс пароля»). К публичным формам (забыли пароль, установка пароля по ссылке) применяется лимит запросов с одного IP (in-memory, настраивается в library.rate-limit; в тестовом профиле отключён). Spring Boot 3, Spring Data JPA, Hibernate 6, Thymeleaf, PostgreSQL; миграции Flyway; Spring Security включён с CSRF для форм; тесты на JUnit 5, Mockito, H2.
- JDK 17+ (для Spring Boot 3; на JDK 24 тесты используют
-Dnet.bytebuddy.experimental=trueв Surefire, см.pom.xml). - PostgreSQL с базой
library(или своё имя — в конфиге). - Maven Wrapper:
./mvnw(отдельно Maven не обязателен).
-
Создайте БД, например:
CREATE DATABASE library;
-
Скопируйте пример локальных свойств:
cp src/main/resources/application-local.yml.example src/main/resources/application-local.yml
Задайте пароль в файле или через
LIBRARY_DB_PASSWORD. Файлapplication-local.ymlрекомендуется добавить в.gitignore(секреты). -
Запуск с профилем
localподхватитapplication-local.yml:./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=local" -
Схема БД создаётся и обновляется Flyway (
src/main/resources/db/migration). JPA в основном профиле:spring.jpa.hibernate.ddl-auto=validate(сущности и миграции должны совпадать).
МиграцияV5__book_genre.sqlдобавляет колонкуgenreв таблицуbook(значение по умолчаниюOTHERдля уже существующих записей). ТаблицаV8__catalog_password_setup_token.sqlхранит одноразовые токены для страницы/catalog/setup-password. МиграцияV10__person_read_book.sqlсоздаёт связующую таблицу «читатель — отмеченные как прочитанные книги» (отдельно от выдачиbook.person_id).
Если база уже существовала с кастомной схемой без истории Flyway, выполнитеbaselineили используйте чистую БД — см. документацию Flyway.
По умолчанию отправка выключена (library.mail.welcome-reader.enabled: false). После /people/new читателю нужна ссылка /catalog/setup-password?token=…: она попадает в письмо (если всё настроено) или отображается библиотекарю на списке /people во всплывающем сообщении, если письмо отправить не удалось (нет SMTP, нет публичного URL и т.д.).
Нужны публичный URL сайта (для корректной ссылки в письме) и SMTP (если письмо должно уходить само). Пример в application-local.yml:
library:
app:
public-base-url: "http://localhost:8080"
catalog-password-setup:
token-validity-hours: 168
mail:
welcome-reader:
enabled: true
from: "Библиотека <noreply@example.com>"
spring:
mail:
host: smtp.example.com
port: 587
username: your-user
password: your-secret
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: trueПисьмо ставится в очередь после успешного коммита. В письме: логин (email), номер читательского билета и ссылка на установку пароля. Токен одноразовый, срок жизни задаётся library.catalog-password-setup.token-validity-hours.
Без spring.mail.host или без library.app.public-base-url автоотправка не выполняется — используйте ссылку из баннера на /people.
Страница /register по-прежнему выдаёт одноразовый пароль на экране (отдельный сценарий, без карточки читателя и без письма со ссылкой).
Публичная форма отправляет POST на /forgot-password. Если для указанного email есть включённая учётка каталога с ролью USER, после коммита транзакции создаётся одноразовый токен (как при приветствии) и при library.mail.password-reset.enabled: true и настроенном spring.mail уходит письмо со ссылкой на /catalog/setup-password?token=…. Если учётки нет или это не читатель — пользователю показывается тот же успешный сценарий (без намёка на перечисление логинов).
Пример фрагмента конфигурации:
library:
app:
public-base-url: "http://localhost:8080"
mail:
password-reset:
enabled: true
from: "Библиотека <noreply@example.com>"
spring:
mail:
host: smtp.example.com
port: 587
username: your-user
password: your-secret
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: trueСнижает злоупотребления по адресам POST /forgot-password, GET/POST /catalog/setup-password: фиксированное окно одна минута на ключ «IP + тип запроса» (in-memory, один экземпляр приложения). Параметры в application.yml:
enabled— глобальное включение (вapplication-test.ymlдля тестов заданоfalse).forgot-password-post-per-minute,catalog-setup-get-per-minute,catalog-setup-post-per-minute— пороги.trust-x-forwarded-for— если приложение за reverse proxy и нужно брать первый адрес изX-Forwarded-Forкак идентификатор клиента.
При превышении лимита выполняется редирект на /rate-limit-exceeded (шаблон error/rate-limit.html). В открытом интернете дополнительно имеет смысл настроить лимиты на стороне proxy или общий store (Redis), если инстансов несколько.
./mvnw clean package # JAR: target/com.springdatajpa.library.jar
./mvnw testЧерез Makefile: make package, make test, make clean (по умолчанию ./mvnw).
- Исполняемый JAR: после
./mvnw clean packageзапуск:
java -jar target/com.springdatajpa.library.jar
Переменные БД (SPRING_DATASOURCE_*илиLIBRARY_DB_PASSWORD) задайте в окружении или через профиль (см. ниже). - Профиль
prod: сужает детализацию health-ответа и уровни логов (см.application-prod.yml):
--spring.profiles.active=prod - Actuator: по HTTP открыты
/actuator/health(GET без авторизации — для проб/load balancer) и/actuator/info(только для аутентифицированных пользователей вместе с остальными actuator-путями). При необходимости ограничьте доступ на уровне reverse proxy. - Откройте
http://localhost:8080/people,http://localhost:8080/books(порт по умолчанию — 8080).
Проект изначально учебный; для публичного сервиса дополнительно настраивают HTTPS, резервное копирование БД, централизованные логи и мониторинг на стороне инфраструктуры.
| Область | Описание |
|---|---|
| Стек | Spring Boot 3.3, jakarta.* , Hibernate 6, Spring Security (CSRF в формах), опционально Spring Mail для приветственного письма читателю. |
| Шаблон layout | Данные для шапки (параметр q, путь запроса, цель поиска) приходят из LayoutModelAdvice (@ControllerAdvice), чтобы не использовать в Thymeleaf 3.1+ отключённые по умолчанию объекты #request. Подключён футер с контактами; разметка контактов — fragments/footer. |
| О библиотеке | GET /about открыт без входа (permitAll в SecurityConfig). Текст и реквизиты в шаблонах about.html и fragments/footer.html (футер на всех страницах с layout.html). |
| Поиск в шапке | У вошедшего пользователя форма на sticky-панели: /books/search или /people/search в зависимости от текущего раздела (/people… vs остальное). Плейсхолдер и подпись для скринридеров переключаются (книги / читатели). |
| Книги | Список с Page<Book> и ссылками «Назад / Вперёд»; сортировка — форма на странице: можно отметить сразу несколько критериев (параметры sort_by_year, sort_by_genre, sort_by_title, sort_by_author; отсутствие в query = false). Фильтр по выдаче: availability_preset = all | free | issued; если он есть в запросе, он важнее сочетания sort_by_availability и availability_issued_first. Для читателя: hide_read=true — не показывать книги, отмеченные им как прочитанные (совмещается с сортировкой и availability_preset). Поиск Containing по названию и автору; на странице результатов — ссылки в карточку и редактирование; карточка книги без лишнего второго запроса (JOIN FETCH владельца). Поле genre (EnumType.STRING); в /books/new и /books/{id}/edit — выпадающий список жанров (модель genres). PATCH /books/{id}/read (форма с read=true/false) — только ROLE_USER: личная отметка «прочитано». |
| Карточка читателя в каталоге | GET /me — профиль, взятые книги, прочитанные (отмеченные самим читателем), смена пароля (/me/password). |
| Читатели | Поиск по подстроке имени, фамилии, отчества, email или номера читательского билета (/people/search, q). Создание на /people/new: Person + учётка каталога, одноразовый токен, приветственное письмо со ссылкой на пароль или ссылка библиотекарю (см. «Почта»). Страница /catalog/setup-password (без входа) — установка пароля по токену. getBooksByPersonId при отсутствии читателя даёт 404, как и остальные операции. |
| Обновление книги | Поля обновляются на управляемой сущности (без «слепого» save копии). |
| Выдача | personId с @Min(1) на параметре + проверка в сервисе. После успешной выдачи редирект на карточку книги /books/{id} (как после возврата). |
| Ошибки | GlobalExceptionHandler: в том числе NoResourceFoundException → 404, ServletRequestBindingException → обобщённое сообщение пользователю, детали в debug-лог. |
| Лимиты | PublicEndpointRateLimitFilter + MinuteBucketRateLimiter для публичных форм пароля (см. «Ограничение частоты запросов»). |
Шаблоны: src/main/resources/templates/ (не webapp/WEB-INF/views).
| Путь | Описание |
|---|---|
/about |
Описание библиотеки и стека; контакты продублированы в футере на каждой странице (без входа) |
/people |
Список читателей |
/people/search |
Поиск по имени, фамилии, отчеству, email или номеру билета (q=); карточка / правка (роль библиотекаря) |
/people/new |
Новый читатель (без поля пароля у библиотекаря) |
/catalog/setup-password |
Установка пароля каталога по токену из письма (GET с token= или форма POST) |
/forgot-password |
Запрос ссылки на сброс пароля для читателя (роль USER); см. «Сброс пароля» |
/register |
Регистрация библиотекаря (роль LIBRARIAN); только для уже вошедшего библиотекаря; начальный пароль показывается однократно после отправки формы |
/login, /logout |
Вход и выход |
/me, /me/password |
Профиль читателя (ROLE_USER): данные, взятые и прочитанные книги, смена пароля каталога |
/rate-limit-exceeded |
Страница при превышении лимита запросов к публичным формам сброса/установки пароля |
/books |
Список; пример query: ?page=1&books_per_page=10&sort_by_year=true&sort_by_author=true&availability_preset=free&hide_read=true (читатель: скрыть уже прочитанные; несколько сортировок и фильтры совместимы) PATCH /books/{id}/read?read=true — отметить прочитанной; read=false — убрать отметку |
/books/search |
Поиск по названию или автору (q=); опционально hide_read=true (читатель); у каждой позиции — переход в карточку / правка |
/books/new |
Новая книга (название, автор, год, жанр из списка) |
GET /actuator/health |
Проверка живости приложения (без входа) |
src/main/java/com/springdatajpa/library/
├── LibraryApplication.java
├── config/
│ ├── SecurityConfig.java
│ ├── PublicEndpointRateLimitFilter.java
│ ├── RateLimitConfiguration.java
│ ├── RateLimitProperties.java
│ ├── LibraryAccessDeniedHandler.java
│ └── LayoutModelAdvice.java # модель для шапки (поиск, путь URI)
├── controllers/
├── services/ # ReaderWelcomeMailService, CatalogPasswordSetupService, …
├── support/ # TransactionCallbacks, MinuteBucketRateLimiter
├── repositories/
├── models/
└── exception/
src/main/resources/
├── application.yml
├── application-prod.yml # профиль prod: логи, health без деталей
├── application-test.yml # H2, без Flyway — для @DataJpaTest
├── db/migration/ # Flyway
└── templates/ # Thymeleaf: `fragments/layout.html`, `fragments/footer.html`, `about.html`, …
src/test/java/ # Mockito + @DataJpaTest
Учебный проект; при публикации добавьте лицензию при необходимости.