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
2,701 changes: 2,701 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM eclipse-temurin:17-jdk

WORKDIR /app

COPY target/jira-1.0.jar app.jar
COPY application-secrets.yaml .
COPY resources ./resources

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,35 @@
- [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/)

## Быстрый запуск (bash)

1. Убедитесь, что установлены **Docker**, **Maven** и **Git Bash** (или WSL).
2. Выполните в корне проекта:

```bash
mvn clean package -DskipTests
```
```bash
docker build -t jira-rush .
```
```bash
docker run -d -p 8080:8080 -e DB_USERNAME=jira -e DB_PASSWORD=JiraRush -e DB_URL=jdbc:postgresql://host.docker.internal:5432/jira --name jira-rush-app jira-rush
```
## Запуск БД
```
url: jdbc:postgresql://localhost:5432/jira
username: jira
password: JiraRush
```

### Prod
```bash
docker run -p 5432:5432 --name postgres-db -e POSTGRES_USER=jira -e POSTGRES_PASSWORD=JiraRush -e POSTGRES_DB=jira -e PGDATA=/var/lib/postgresql/data/pgdata -v ./pgdata:/var/lib/postgresql/data -d postgres
```
### test
```bash
docker run -p 5433:5432 --name postgres-db-test -e POSTGRES_USER=jira -e POSTGRES_PASSWORD=JiraRush -e POSTGRES_DB=jira-test -e PGDATA=/var/lib/postgresql/data/pgdata -v ./pgdata-test:/var/lib/postgresql/data -d postgres
```
- Есть 2 общие таблицы, на которых не fk
- _Reference_ - справочник. Связь делаем по _code_ (по id нельзя, тк id привязано к окружению-конкретной базе)
- _UserBelong_ - привязка юзеров с типом (owner, lead, ...) к объекту (таска, проект, спринт, ...). FK вручную будем
Expand All @@ -27,4 +50,12 @@
- https://habr.com/ru/articles/259055/

Список выполненных задач:
...
1. Удалить социальные сети: vk, yandex. 18.06.2026
2. Вынести чувствительную информацию в отдельный проперти файл. 21.06.2026
3. Переделать тесты так, чтоб во время тестов использовалась in memory БД (H2), а не PostgreSQL. 21.06.2026
4. Написать тесты для всех публичных методов контроллера ProfileRestController. 22.06.2026
5. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload. 22.06.2026
6. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе) 22.06.2026
7. Добавить подсчет времени сколько задача находилась в работе и тестировании. 23.06.2026
8. Написать Dockerfile для основного сервера. 24.06.2026
9. Написать docker-compose файл для запуска контейнера сервера вместе с БД и nginx. 24.06.2026
12 changes: 12 additions & 0 deletions application-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DB_USERNAME: jira
DB_PASSWORD: JiraRush
GITHUB_CLIENT_ID: 3d0d8738e65881fff266
GITHUB_CLIENT_SECRET: 0f97031ce6178b7dfb67a6af587f37e222a16120
GOOGLE_CLIENT_ID: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET: 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
GITLAB_CLIENT_ID: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494
GITLAB_CLIENT_SECRET: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004
MAIL_USERNAME: jira4jr@gmail.com
MAIL_PASSWORD: zdfzsrqvgimldzyj
TEST_DB_USERNAME: jira
TEST_DB_PASSWORD: JiraRush
3 changes: 1 addition & 2 deletions config/_application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ spring:
datasource:
url: jdbc:postgresql://localhost:5432/jira
username: jira
password: JiraRush

password: JiraRush
94 changes: 55 additions & 39 deletions config/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,40 +1,56 @@
# 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;

# https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration
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;

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;
}
events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 2048;

upstream jira_backend {
server app:8080;
}

server {
listen 80;
server_name localhost;

# Корень для статики – если нужно
root /opt/jirarush/resources;
index index.html;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Обработка статики
location /static/ {
expires 30d;
access_log off;
}

location /robots.txt {
access_log off;
}

# Прокси на приложение
location ~ (/$|/view/|/ui/|/oauth2/) {
proxy_pass http://jira_backend;
proxy_connect_timeout 30s;
}

location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) {
proxy_pass http://jira_backend;
proxy_connect_timeout 150s;
}

# Для всего остального – 404 или перенаправление
location / {
try_files $uri /view/404.html = 404;
}
}
}
54 changes: 54 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
services:
# PostgreSQL база данных
db:
image: postgres:15
container_name: jira-db
environment:
POSTGRES_DB: jira
POSTGRES_USER: jira
POSTGRES_PASSWORD: JiraRush
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- jira-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U jira"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"

# Spring Boot приложение
app:
build: .
container_name: jira-app
depends_on:
db:
condition: service_healthy
environment:
DB_URL: jdbc:postgresql://db:5432/jira
DB_USERNAME: jira
DB_PASSWORD: JiraRush
SPRING_PROFILES_ACTIVE: prod
networks:
- jira-network

nginx:
image: nginx:alpine
container_name: jira-nginx
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./resources:/opt/jirarush/resources:ro
ports:
- "80:80"
depends_on:
- app
networks:
- jira-network

networks:
jira-network:

volumes:
pgdata:
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<springdoc.version>2.0.2</springdoc.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down Expand Up @@ -136,6 +138,13 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<!-- https://youtrack.jetbrains.com/issue/IDEA-231927-->
<dependency>
<groupId>org.junit.platform</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 @@ -22,4 +22,7 @@ public interface UserBelongRepository extends BaseRepository<UserBelong> {

@Query("SELECT u FROM UserBelong u WHERE u.objectId =?1 AND u.objectType=?2 and u.userId=?3 and u.userTypeCode=?4 and u.endpoint IS NULL")
Optional<UserBelong> findActiveAssignment(long objectId, ObjectType objectType, long userId, String userTypeCode);

@Query("SELECT u FROM UserBelong u WHERE u.objectId = ?1 AND u.objectType = ?2 AND u.userId = ?3 AND u.userTypeCode = ?4")
Optional<UserBelong> findAssignment(long objectId, ObjectType objectType, long userId, String userTypeCode);
}
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 @@ -24,15 +21,13 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str
if (multipartFile.isEmpty()) {
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());
}
try {
Path dirPath = Paths.get(directoryPath);
Files.createDirectories(dirPath);
Path filePath = dirPath.resolve(fileName);
Files.write(filePath, 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 @@ -5,6 +5,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Transactional(readOnly = true)
public interface ActivityRepository extends BaseRepository<Activity> {
Expand All @@ -13,4 +14,6 @@ public interface ActivityRepository extends BaseRepository<Activity> {

@Query("SELECT a FROM Activity a JOIN FETCH a.author WHERE a.taskId =:taskId AND a.comment IS NOT NULL ORDER BY a.updated DESC")
List<Activity> findAllComments(long taskId);

Optional<Activity> findFirstByTaskIdAndStatusCodeOrderByUpdatedAsc(long taskId, String statusCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
import jakarta.annotation.Nullable;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import static com.javarush.jira.common.BaseHandler.createdResponse;

Expand Down Expand Up @@ -156,4 +159,28 @@ public TaskTreeNode(TaskTo taskTo) {
this(taskTo, new LinkedList<>());
}
}

@PostMapping("/{id}/tags")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void addTags(@PathVariable long id, @Valid @RequestBody Set<@Size(min=2, max=32) String> tags) {
taskService.addTags(id, tags);
}

@GetMapping("/{id}/work-time")
public ResponseEntity<Long> getWorkTime(@PathVariable long id) {
Duration duration = taskService.getWorkDuration(id);
if (duration == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(duration.toMinutes());
}

@GetMapping("/{id}/test-time")
public ResponseEntity<Long> getTestTime(@PathVariable long id) {
Duration duration = taskService.getTestDuration(id);
if (duration == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(duration.toMinutes());
}
}
Loading