diff --git a/.env b/.env
new file mode 100644
index 000000000..13a97d096
--- /dev/null
+++ b/.env
@@ -0,0 +1,20 @@
+# Database
+DB_URL=jdbc:postgresql://localhost:5432/jira
+DB_USER_NAME=jira
+DB_USER_PWD=JiraRush
+
+# OAuth2 - GitHub
+GITHUB_CLIENT_ID=3d0d8738e65881fff266
+GITHUB_CLIENT_SECRET=0f97031ce6178bdf7b67a6af587f37e222a16120
+
+# OAuth2 - Google
+GOOGLE_CLIENT_ID=329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com
+GOOGLE_CLIENT_SECRET=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
+
+# OAuth2 - GitLab
+GITLAB_CLIENT_ID=b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494
+GITLAB_CLIENT_SECRET=e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004
+
+# Mail
+MAIL_USER_NAME=jira4jr@gmail.com
+MAIL_PASSWORD=zdfzsrqvgimldzyj
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index cd38e2e7b..ffc994e15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@ target
logs
attachments
*.patch
-
-
+#.env
+pgdata/
+pgdata-test/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..5b09d9e8c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+# Этап сборки
+FROM maven:3.9.6-eclipse-temurin-17-alpine AS build
+
+WORKDIR /app
+
+COPY pom.xml .
+RUN mvn dependency:go-offline
+
+COPY src ./src
+COPY resources ./resources
+RUN mvn clean package -DskipTests
+
+# Этап запуска
+FROM eclipse-temurin:17-jre-alpine
+
+WORKDIR /app
+
+# Копируем JAR из этапа сборки
+COPY --from=build /app/target/*.jar app.jar
+COPY --from=build /app/resources ./resources
+
+EXPOSE 8080
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 719b268f5..0671d4311 100644
--- a/README.md
+++ b/README.md
@@ -27,4 +27,76 @@
- https://habr.com/ru/articles/259055/
Список выполненных задач:
+## Команды загрузки
+```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 $(pwd)/pgdata:/var/lib/postgresql/data -d postgres
+```
+```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 $(pwd)/pgdata-test:/var/lib/postgresql/data -d postgres
+```
+
+1. Разобраться со структурой проекта (onboarding).✅
+
+2. Удалить социальные сети: vk, yandex. `Easy task`✅
+
+3. Вынести чувствительную информацию в отдельный проперти файл: ✅
+ - логин
+ - пароль БД
+ - идентификаторы для OAuth регистрации/авторизации
+ - настройки почты
+
+ Значения этих проперти должны считываться при старте сервера из переменных окружения машины. `Easy task`
+ - ```
+ добавлен .env файл в проект
+ написан тест
+ проект учебный .env не добавлен в gitignore
+ ```
+
+4. Переделать тесты так, чтоб во время тестов использовалась **in memory БД (H2)**, а не PostgreSQL. Для этого нужно определить 2 бина, и выборка какой из них использовать должно определяться активным профилем Spring. H2 не поддерживает все фичи, которые есть у PostgreSQL, поэтому тебе прийдется немного упростить скрипты с тестовыми данными.
+
+5. Написать тесты для всех публичных методов контроллера `ProfileRestController`. Хоть методов только 2, но тестовых методов должно быть больше, т.к. нужно проверить success and unsuccess path. ✅
+
+6. Сделать рефакторинг метода `com.javarush.jira.bugtracking.attachment.FileUtil#upload` чтоб он использовал современный подход для работы с файловой системмой. `Easy task` ✅
+
+7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе). Фронт делать необязательно. Таблица `task_tag` уже создана.
+
+8. Добавить подсчет времени сколько задача находилась в работе и тестировании. Написать 2 метода на уровне сервиса, которые параметром принимают задачу и возвращают затраченное время: ✅
+ - Сколько задача находилась в работе (ready_for_review минус in_progress).
+ - Сколько задача находилась на тестировании (done минус ready_for_review).
+
+ Для написания этого задания, нужно добавить в конец скрипта инициализации базы данных `changelog.sql` **3 записи** в таблицу `ACTIVITY`:
+ ```
+ insert into ACTIVITY ( ID, AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE ) values ...
+ ```
+Со статусами:
+- время начала работы над задачей – **in_progress**
+- время окончания разработки - **ready_for_review**
+- время конца тестирования - **done**
+
+9. Написать `Dockerfile` для основного сервера ✅
+```
+добавлен Dockerfile и docker-compose.yml
+docker build --no-cache --progress=plain -t project-final-app .
+
+```
+```bash
+docker build --no-cache --progress=plain -t project-final-app .
+```
+```bash
+docker-compose up -d
+```
+
+10. Написать `docker-compose` файл для запуска контейнера сервера вместе с БД и [nginx](https://nginx.org/ru/docs/). Для nginx используй конфиг-файл `config/nginx.conf`. При необходимости файл конфига можно редактировать. `Hard task` ✅
+```bash
+docker-compose down -v
+```
+```bash
+docker-compose up -d --build
+```
+```bash
+
+```
+11. Добавить локализацию минимум на двух языках для шаблонов писем (mails) и стартовой страницы `index.html`.
+
+12. Переделать механизм распознавания «свой-чужой» между фронтом и беком с `JSESSIONID` на `JWT`. Из сложностей – тебе придётся переделать отправку форм с фронта, чтоб добавлять хедер аутентификации. `Extra-hard task`
...
\ No newline at end of file
diff --git a/config/nginx.conf b/config/nginx.conf
index 82b9e234d..0f6ac4ae1 100644
--- a/config/nginx.conf
+++ b/config/nginx.conf
@@ -2,39 +2,57 @@
# 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;
- }
+ listen 80;
+
+ 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; }
+
+ location /static/ {
+ expires 30d;
+ access_log off;
+ }
+
+ location /robots.txt {
+ access_log off;
+ }
+
+ # API endpoints - должны идти первыми
+ location /api/ {
+ proxy_pass http://app:8080/api/;
+ proxy_connect_timeout 150s;
+ 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 /actuator/ {
+ proxy_pass http://app:8080/actuator/;
+ proxy_connect_timeout 150s;
+ 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;
+ }
+
+ # UI и основные страницы
+ location ~ (/$|/view/|/ui/|/oauth2/) {
+ expires 0m;
+ proxy_pass http://app:8080;
+ proxy_connect_timeout 30s;
+ 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 / {
+ try_files /view/404.html = 404;
+ }
}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..14d0479a2
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,55 @@
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:15-alpine
+ container_name: jira_postgres
+ environment:
+ POSTGRES_DB: jira
+ POSTGRES_USER: ${DB_USER_NAME}
+ POSTGRES_PASSWORD: ${DB_USER_PWD}
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ networks:
+ - jira_network
+ restart: unless-stopped
+
+ app:
+ build: .
+ container_name: jira_app
+ env_file:
+ - .env
+ environment:
+ DB_URL: jdbc:postgresql://postgres:5432/jira
+ depends_on:
+ - postgres
+ networks:
+ - jira_network
+ restart: unless-stopped
+
+ nginx:
+ image: nginx:alpine
+ container_name: jira_nginx
+ ports:
+ - "80:80"
+ volumes:
+ # Монтируем наш конфиг в sites-available
+ - ./config/nginx.conf:/etc/nginx/sites-available/jirarush.conf:ro
+ # Создаём симлинк в sites-enabled
+ - ./config/nginx.conf:/etc/nginx/sites-enabled/jirarush.conf:ro
+ # Монтируем статику
+ - ./resources:/opt/jirarush/resources:ro
+ depends_on:
+ - app
+ networks:
+ - jira_network
+ restart: unless-stopped
+
+volumes:
+ postgres_data:
+
+networks:
+ jira_network:
+ driver: bridge
diff --git a/pom.xml b/pom.xml
index f6c152c68..1b6de20ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -142,6 +142,12 @@
junit-platform-launcher
test
+
+
+ me.paulschwarz
+ spring-dotenv
+ 4.0.0
+
diff --git a/resources/view/login.html b/resources/view/login.html
index 8765ca8ff..d49ce5691 100644
--- a/resources/view/login.html
+++ b/resources/view/login.html
@@ -48,14 +48,6 @@ Sign in
type="button">
-
-
-
-
-
-
diff --git a/resources/view/unauth/register.html b/resources/view/unauth/register.html
index 2ba955045..52a892bd3 100644
--- a/resources/view/unauth/register.html
+++ b/resources/view/unauth/register.html
@@ -77,14 +77,6 @@ Registration
type="button">
-
-
-
-
-
-
diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
index 6cffbe175..efd19255f 100644
--- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
+++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
@@ -7,10 +7,8 @@
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;
@@ -25,14 +23,15 @@ 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());
+ try {
+ Path dir = Paths.get(directoryPath);
+ if (!Files.exists(dir)) {
+ Files.createDirectories(dir);
}
+ Path file = dir.resolve(fileName);
+ Files.write(file, multipartFile.getBytes());
+ } catch (IOException e) {
+ throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
}
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
index e6f385548..d5af54998 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
@@ -19,6 +19,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
+import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
@@ -140,4 +141,62 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass
throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode()));
}
}
+
+ private Long checkTaskTimeInWork(Task task) {
+ if(task == null) {
+ return 0L;
+ }
+ List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.getId());
+ if(activities == null || activities.isEmpty()) {
+ return 0L;
+ }
+ LocalDateTime readyForReviewTime = null;
+ LocalDateTime inProgressTime = null;
+ for (Activity activity : activities) {
+ String statusCode = activity.getStatusCode();
+ LocalDateTime updated = activity.getUpdated();
+
+ if ("in_progress".equals(statusCode)) {
+ inProgressTime = updated;
+ } else if ("ready_for_review".equals(statusCode)) {
+ readyForReviewTime = updated;
+ break;
+ }
+ }
+
+ if (inProgressTime != null && readyForReviewTime != null) {
+ return Duration.between(inProgressTime, readyForReviewTime).toSeconds();
+ }
+ return 0L;
+ }
+
+ private Long checkTaskTimeInTesting(Task task) {
+ if(task == null) {
+ return 0L;
+ }
+ List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.getId());
+ if(activities == null || activities.isEmpty()) {
+ return 0L;
+ }
+
+ LocalDateTime readyForReviewTime = null;
+ LocalDateTime doneTime = null;
+
+ for (Activity activity : activities) {
+ String statusCode = activity.getStatusCode();
+ LocalDateTime updated = activity.getUpdated();
+
+ if ("ready_for_review".equals(statusCode)) {
+ readyForReviewTime = updated;
+ } else if ("done".equals(statusCode)) {
+ doneTime = updated;
+ break;
+ }
+ }
+
+ if (readyForReviewTime != null && doneTime != null) {
+ return Duration.between(readyForReviewTime, doneTime).toSeconds();
+ }
+ return 0L;
+ }
}
diff --git a/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java b/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java
index 8a434a807..b9184421d 100644
--- a/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java
+++ b/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java
@@ -12,6 +12,7 @@
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.filter.ForwardedHeaderFilter;
+import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -24,6 +25,9 @@
import java.time.Duration;
import java.util.Properties;
+import org.springframework.context.annotation.Primary;
+import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
+
//@EnableWebMvc : http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
@EnableAutoConfiguration
@Configuration
diff --git a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java
index 85a134319..d0e9b390c 100644
--- a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java
+++ b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java
@@ -12,18 +12,24 @@
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import java.util.List;
+
@Component
@AllArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
- @Qualifier("handlerExceptionResolver")
- private final HandlerExceptionResolver resolver;
+ private final List resolvers;
private final RequestMappingHandlerMapping mapping;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException {
try {
HandlerExecutionChain handler = mapping.getHandler(request);
- resolver.resolveException(request, response, handler == null ? null : handler.getHandler(), authException);
+ // Пробуем каждый резолвер, пока один не обработает исключение
+ for (HandlerExceptionResolver resolver : resolvers) {
+ if (resolver.resolveException(request, response, handler == null ? null : handler.getHandler(), authException) != null) {
+ break;
+ }
+ }
} catch (Exception e) {
throw new ServletException(e);
}
diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java
deleted file mode 100644
index e8e05be05..000000000
--- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.javarush.jira.login.internal.sociallogin.handler;
-
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-import java.util.Map;
-
-@Component("vk")
-public class VkOAuth2UserDataHandler implements OAuth2UserDataHandler {
- @Override
- public String getFirstName(OAuth2UserData oAuth2UserData) {
- return getAttribute(oAuth2UserData, "first_name");
- }
-
- @Override
- public String getLastName(OAuth2UserData oAuth2UserData) {
- return getAttribute(oAuth2UserData, "last_name");
- }
-
- @Override
- public String getEmail(OAuth2UserData oAuth2UserData) {
- return oAuth2UserData.getData("email");
- }
-
- private String getAttribute(OAuth2UserData oAuth2UserData, String name) {
- List