Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
DB_USERNAME=
DB_PASSWORD=

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=

MAIL_USERNAME=
MAIL_PASSWORD=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ target
logs
attachments
*.patch
.env


22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM maven:3.9.6-eclipse-temurin-17 AS build

WORKDIR /app

COPY pom.xml .
COPY src ./src
COPY resources ./resources
COPY config ./config

RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jdk

WORKDIR /app

COPY --from=build /app/target/*.jar app.jar
COPY --from=build /app/resources ./resources
COPY --from=build /app/config ./config

EXPOSE 8080

CMD ["java", "-jar", "app.jar"]
132 changes: 113 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,124 @@
## [REST API](http://localhost:8080/doc)
# JiraRush

## Концепция:
Мини-проект в стиле JIRA на `Spring Boot 3`, `Java 17`, `PostgreSQL`, `Liquibase`, `Thymeleaf` и `Spring Security`.

- Spring Modulith
- [Spring Modulith: достигли ли мы зрелости модульности](https://habr.com/ru/post/701984/)
- [Introducing Spring Modulith](https://spring.io/blog/2022/10/21/introducing-spring-modulith)
- [Spring Modulith - Reference documentation](https://docs.spring.io/spring-modulith/docs/current-SNAPSHOT/reference/html/)
## REST API

- Swagger UI через `nginx`: [http://localhost/doc](http://localhost/doc)
- Прямой backend: [http://localhost:8080/doc](http://localhost:8080/doc)

## Технологии

- Java 17
- Spring Boot 3
- Spring Security
- Spring Data JPA
- PostgreSQL
- H2 для тестов
- Liquibase
- Thymeleaf
- Docker
- Docker Compose
- Nginx

## Структура проекта

- `src/main/java` — основной Java-код
- `src/main/resources` — конфиги приложения, Liquibase, SQL
- `src/test/java` — тесты
- `src/test/resources` — тестовые данные и тестовый changelog
- `resources/view` — HTML-шаблоны
- `resources/static` — CSS, JS, шрифты, картинки
- `config` — дополнительные конфиги, включая `nginx.conf`

## Переменные окружения

Чувствительные данные вынесены из `application.yaml` в environment variables.

Для локальной работы нужен файл [`.env.example`](/C:/ProjectJavaRush/.env.example) как шаблон.
Локально нужно создать [`.env`](/C:/ProjectJavaRush/.env) и заполнить его значениями.

```env
DB_USERNAME=
DB_PASSWORD=

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=

MAIL_USERNAME=
MAIL_PASSWORD=
```

## Запуск через Docker

Основной способ запуска проекта:

```powershell
docker compose up --build
```
url: jdbc:postgresql://localhost:5432/jira
username: jira
password: JiraRush

После старта доступны:

- [http://localhost](http://localhost) — вход через `nginx`
- [http://localhost:8080](http://localhost:8080) — прямой доступ к Spring Boot
- [http://localhost/doc](http://localhost/doc) — Swagger через `nginx`

Остановка контейнеров:

```powershell
docker compose down
```

- Есть 2 общие таблицы, на которых не fk
- _Reference_ - справочник. Связь делаем по _code_ (по id нельзя, тк id привязано к окружению-конкретной базе)
- _UserBelong_ - привязка юзеров с типом (owner, lead, ...) к объекту (таска, проект, спринт, ...). FK вручную будем
проверять
Если менялся `config/nginx.conf`, может понадобиться перезапуск контейнера `nginx`.

## Прогресс

### Выполнено

- Разобрался со структурой проекта.
- Удалил `vk` и `yandex` из OAuth-конфига и UI.
- Вынес чувствительную информацию в environment variables.
- Добавил `Dockerfile` для основного сервера.
- Добавил `docker-compose.yml` для запуска сервера, БД и `nginx`.
- Адаптировал `config/nginx.conf` под Docker Compose.
- Исправил запуск приложения через `RestAuthenticationEntryPoint`.
- Перевёл тесты на H2 вместо PostgreSQL.
- Отрефакторил `FileUtil#upload` на `java.nio.file`.
- Написал тесты для `ProfileRestController`.
- Реализовал теги для задач.

### Осталось сделать

- Разобраться с проблемами авторизации и регистрации.
- Проверить OAuth redirect URI для Google.
- Реализовать подсчёт времени по статусам.

## Проблемы и решения

## Аналоги
- Локальный запуск в IntelliJ падал без `DB_USERNAME` и других env-переменных; для проверки я запускал приложение через Docker и отдельно вынес чувствительные значения в `.env`.
- H2 не принимал часть PostgreSQL-специфичных скриптов; для тестов я сделал отдельный `src/test/resources/db/changelog-test.sql` и упростил `src/test/resources/data.sql`.
- В `ProfileRestController` тесты пришлось подстроить под текущую реализацию: `GET` возвращает профиль с `id`, а `PUT` валидирует ID и контакты и отдаёт `422` на некорректные данные.
- Для тегов у `Task.tags` вылезал `LazyInitializationException`; я решил это через `LEFT JOIN FETCH t.tags` в `findFullById`, чтобы full-ответ задачи сразу содержал теги.

- https://java-source.net/open-source/issue-trackers
## Выполненные пункты задания

## Тестирование
- Разобраться со структурой проекта.
- Удалить социальные сети: `vk`, `yandex`.
- Вынести чувствительную информацию в отдельные properties и читать её из переменных окружения.
- Перевести тесты на H2.
- Написать Dockerfile для основного сервера.
- Написать docker-compose для запуска сервера вместе с БД и `nginx`.
- Сделать рефакторинг `FileUtil#upload` на современный API файловой системы.
- Написать тесты для `ProfileRestController`.
- Реализовать теги для задач.

- https://habr.com/ru/articles/259055/
## Известные проблемы

Список выполненных задач:
...
- Google OAuth сейчас может падать с `redirect_uri_mismatch`.
- Блок авторизации и регистрации ещё нужно отдельно проверить после стабилизации инфраструктуры.
36 changes: 7 additions & 29 deletions config/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
# https://losst.ru/ustanovka-nginx-ubuntu-16-04
# https://pai-bx.com/wiki/nginx/2332-useful-redirects-in-nginx/#1
# sudo iptables -A INPUT ! -s 127.0.0.1 -p tcp -m tcp --dport 8080 -j DROP
server {
listen 80;
listen 80;

# https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration
gzip on;
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 2048;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /opt/jirarush/resources;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;

if ($request_uri ~ ';') {return 404;}
if ($request_uri ~ ';') { return 404; }

# proxy_cookie_flags ~ secure samesite=none;

# static
location /static/ {
expires 30d;
access_log off;
}
location /robots.txt {
access_log off;
}

location ~ (/$|/view/|/ui/|/oauth2/) {
expires 0m;
proxy_pass http://localhost:8080;
proxy_connect_timeout 30s;
}
location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) {
proxy_pass http://localhost:8080;
proxy_connect_timeout 150s;
}
location / {
try_files /view/404.html = 404;
proxy_pass http://app:8080;
proxy_connect_timeout 150s;
}
}
47 changes: 47 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
services:
db:
image: postgres:16
container_name: jirarush-db
environment:
POSTGRES_DB: jira
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data

app:
build:
context: .
dockerfile: Dockerfile
container_name: jirarush-app
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/jira
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GITLAB_CLIENT_ID: ${GITLAB_CLIENT_ID}
GITLAB_CLIENT_SECRET: ${GITLAB_CLIENT_SECRET}
MAIL_USERNAME: ${MAIL_USERNAME}
MAIL_PASSWORD: ${MAIL_PASSWORD}
ports:
- "8080:8080"

nginx:
image: nginx:stable
container_name: jirarush-nginx
depends_on:
- app
ports:
- "80:80"
volumes:
- ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro

volumes:
postgres_data:
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.liquibase</groupId>
Expand Down
8 changes: 0 additions & 8 deletions resources/view/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,6 @@ <h3 class="mb-3">Sign in</h3>
type="button">
<i class="fa-brands fa-google"></i>
</a>
<a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"
type="button">
<i class="fa-brands fa-vk"></i>
</a>
<a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"
type="button">
<i class="fa-brands fa-yandex"></i>
</a>
<a class="btn btn-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
8 changes: 0 additions & 8 deletions resources/view/unauth/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ <h3 class="mb-3">Registration</h3>
type="button">
<i class="fa-brands fa-google"></i>
</a>
<a class="btn btn-primary btn-lg me-2" href="/oauth2/authorization/vk" style="padding-left: 17px; padding-right: 17px;"
type="button">
<i class="fa-brands fa-vk"></i>
</a>
<a class="btn btn-danger btn-lg me-2" href="/oauth2/authorization/yandex" style="padding-left: 21px; padding-right: 21px;"
type="button">
<i class="fa-brands fa-yandex"></i>
</a>
<a class="btn btn-dark btn-lg me-2" href="/oauth2/authorization/github" type="button">
<i class="fa-brands fa-github"></i>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -25,14 +22,13 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str
throw new IllegalRequestDataException("Select a file to upload.");
}

File dir = new File(directoryPath);
if (dir.exists() || dir.mkdirs()) {
File file = new File(directoryPath + fileName);
try (OutputStream outStream = new FileOutputStream(file)) {
outStream.write(multipartFile.getBytes());
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
}
Path directory = Paths.get(directoryPath);
Path file = directory.resolve(fileName);
try {
Files.createDirectories(directory);
Files.write(file, multipartFile.getBytes());
} catch (IOException ex) {
throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.javarush.jira.bugtracking.task.to.TaskTo;
import com.javarush.jira.bugtracking.task.to.TaskToExt;
import com.javarush.jira.bugtracking.task.to.TaskToFull;
import com.javarush.jira.bugtracking.task.to.TagsTo;
import com.javarush.jira.bugtracking.tree.ITreeNode;
import com.javarush.jira.common.util.Util;
import com.javarush.jira.login.AuthUser;
Expand Down Expand Up @@ -107,6 +108,13 @@ public void changeTaskSprint(@PathVariable long id, @Nullable @RequestParam Long
taskService.changeSprint(id, sprintId);
}

@PatchMapping(path = "/{id}/tags", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void addTags(@PathVariable long id, @Valid @RequestBody TagsTo tagsTo) {
log.info("add tags {} to task {}", tagsTo.getTags(), id);
taskService.addTags(id, tagsTo.getTags());
}

@GetMapping("/assignments/by-sprint")
public List<UserBelong> getTaskAssignmentsBySprint(@RequestParam long sprintId) {
log.info("get task assignments for user {} for sprint {}", AuthUser.authId(), sprintId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface TaskRepository extends BaseRepository<Task> {
@Query("SELECT t FROM Task t WHERE t.projectId =:projectId ORDER BY t.startpoint DESC")
List<Task> findAllByProjectId(long projectId);

@Query("SELECT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent WHERE t.id =:id")
@Query("SELECT DISTINCT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent LEFT JOIN FETCH t.tags WHERE t.id =:id")
Optional<Task> findFullById(long id);

@Modifying
Expand Down
Loading