diff --git a/.env-prod b/.env-prod
new file mode 100644
index 000000000..f7856fdb0
--- /dev/null
+++ b/.env-prod
@@ -0,0 +1,13 @@
+DATASOURCE_URL=jdbc:postgresql://localhost:5433/jira
+DATASOURCE_USERNAME=jira
+DATASOURCE_PASSWORD=JiraRush
+MAIL_HOST=smtp.gmail.com
+MAIL_USERNAME=jira4jr@gmail.com
+MAIL_PASSWORD=zdfzsrqvgimldzyj
+MAIL_PORT=587
+OAUTH2_GITHUB_CLIENT_ID=3d0d8738e65881fff266
+OAUTH2_GITHUB_CLIENT_SECRET=0f97031ce6178b7dfb67a6af587f37e222a16120
+OAUTH2_GOOGLE_CLIENT_ID=329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com
+OAUTH2_GOOGLE_CLIENT_SECRET=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
+OAUTH2_GITLAB_CLIENT_ID=b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494
+OAUTH2_GITLAB_CLIENT_SECRET=e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..88f764fbc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM maven:3.9.6-amazoncorretto-17-debian
+LABEL maintainer="Timofej"
+LABEL description="JiraRush application"
+LABEL version="1.0"
+WORKDIR /opt/jirarush
+COPY pom.xml .
+COPY src ./src
+COPY resources ./resources
+COPY lombok.config ./lombok.config
+COPY config/_application-prod.yaml ./src/main/resources/application-prod.yaml
+RUN mvn clean install -DskipTests
+RUN mv ./target/*.jar ./jirarush.jar
+RUN rm -rf ./target
+RUN rm -rf ./src
+RUN rm lombok.config
+RUN rm pom.xml
+EXPOSE 8080
+ENTRYPOINT [ "java", "-Dspring.profiles.active=prod", "-jar", "jirarush.jar" ]
\ No newline at end of file
diff --git a/JiraApi.postman_collection.json b/JiraApi.postman_collection.json
new file mode 100644
index 000000000..321aeb016
--- /dev/null
+++ b/JiraApi.postman_collection.json
@@ -0,0 +1,186 @@
+{
+ "info": {
+ "_postman_id": "74e5f89d-f7f2-4559-b01a-44696cef0cd2",
+ "name": "JiraApi",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "29978865"
+ },
+ "item": [
+ {
+ "name": "postTags",
+ "request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "admin",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "admin@gmail.com",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "[\"baz\", \"qux\"]",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/api/tasks/1/tags",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "tasks",
+ "1",
+ "tags"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "getTags",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "admin",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "admin@gmail.com",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/api/tasks/1/tags",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "tasks",
+ "1",
+ "tags"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "clearTags",
+ "request": {
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "admin",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "admin@gmail.com",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/api/tasks/1/tags",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "tasks",
+ "1",
+ "tags"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "auth": {
+ "type": "basic",
+ "basic": [
+ {
+ "key": "password",
+ "value": "admin",
+ "type": "string"
+ },
+ {
+ "key": "username",
+ "value": "admin@gmail.com",
+ "type": "string"
+ }
+ ]
+ },
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 719b268f5..f29ea4aa8 100644
--- a/README.md
+++ b/README.md
@@ -26,5 +26,31 @@
- https://habr.com/ru/articles/259055/
+‼️Програма запускається черрез файл docker-compose.yml, або через команду docker compose up‼️
+
+
Список выполненных задач:
-...
\ No newline at end of file
+
+1.Розібратися зі структурою проєкту (onboarding)✅
+
+2.Видалити соціальні мережі: vk, yandex✅
+
+3.Винести чутливу інформацію до окремого проперті файлу✅
+
+4.Переробити тести так, щоб під час тестів використовувалася in memory БД (H2), а не PostgreSQL✅
+
+5.Написати тести для всіх публічних методів контролера ProfileRestController✅
+
+6.Зробити рефакторинг методу com.javarush.jira.bugtracking.attachment.FileUtil#upload✅
+
+7.Додати новий функціонал: додавання тегів до завдання (REST API + реалізація на сервісі)✅
+
+8.Додати підрахунок часу: скільки завдання перебувало у роботі та тестуванні❌
+
+9.Написати Dockerfile для основного сервера✅
+
+10.Написати docker-compose файл для запуску контейнера сервера разом з БД та nginx✅
+
+11.Додати локалізацію мінімум двома мовами❌
+
+12.Переробити механізм розпізнавання «свій-чужий» між фронтом і беком з JSESSIONID на JWT❌
diff --git a/config/_application-prod.yaml b/config/_application-prod.yaml
index 67fd8b7c2..4211706df 100644
--- a/config/_application-prod.yaml
+++ b/config/_application-prod.yaml
@@ -3,7 +3,7 @@ app:
host-url: http://localhost
spring:
datasource:
- url: jdbc:postgresql://localhost:5432/jira
+ url: jdbc:postgresql://jirarush-db:5432/jira
username: jira
password: JiraRush
diff --git a/config/nginx.conf b/config/nginx.conf
index 82b9e234d..fb98dfbd5 100644
--- a/config/nginx.conf
+++ b/config/nginx.conf
@@ -1,40 +1,64 @@
-# 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;
- }
-}
\ No newline at end of file
+events {
+ worker_connections 1024;
+}
+
+http {
+ include mime.types;
+ sendfile on;
+
+ server {
+ listen 8080;
+ location / {
+ return 301 http://localhost$request_uri;
+ }
+ }
+
+ server {
+ listen 80;
+ 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;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ root /opt/jirarush/resources;
+
+ if ($request_uri ~ ';') { return 404; }
+
+ location /static/ {
+ expires 30d;
+ access_log off;
+ }
+
+ location /robots.txt {
+ access_log off;
+ }
+
+ location ~ (/$|/view/|/ui/|/oauth2/|/backlog) {
+ expires 0m;
+ proxy_connect_timeout 30s;
+ proxy_read_timeout 30s;
+ proxy_pass http://jirarush-app:8080;
+
+ location ~ /oauth2/authorization/ {
+ proxy_set_header Host $host:$proxy_port;
+ proxy_pass http://jirarush-app:8080;
+ }
+ }
+
+ location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) {
+ proxy_connect_timeout 150s;
+ proxy_read_timeout 150s;
+ proxy_pass http://jirarush-app:8080;
+ }
+
+ location / {
+ try_files /view/404.html =404;
+ }
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..aa0bf04a3
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,56 @@
+version: "3.9"
+services:
+ jirarush-nginx:
+ container_name: jirarush-nginx
+ image: nginx:1.25.4
+ ports:
+ - "80:80"
+ - "8080:8080"
+ volumes:
+ - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./resources/static:/opt/jirarush/resources/static:ro
+ depends_on:
+ - jirarush-app
+ restart: always
+
+ jirarush-app:
+ container_name: jirarush-app
+ image: jirarush-app:1.0.0
+ build: ./
+ environment:
+ SPRING_PROFILES_ACTIVE: prod
+ DATASOURCE_URL: jdbc:postgresql://localhost:5433/jira
+ DATASOURCE_USERNAME: jira
+ DATASOURCE_PASSWORD: JiraRush
+ MAIL_HOST: smtp.gmail.com
+ MAIL_PORT: 587
+ MAIL_USERNAME: jira4jr@gmail.com
+ MAIL_PASSWORD: zdfzsrqvgimldzyj
+ OAUTH2_GITHUB_CLIENT_ID: 3d0d8738e65881fff266
+ OAUTH2_GITHUB_CLIENT_SECRET: 0f97031ce6178b7dfb67a6af587f37e222a16120
+ OAUTH2_GOOGLE_CLIENT_ID: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com
+ OAUTH2_GOOGLE_CLIENT_SECRET: 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
+ OAUTH2_GITLAB_CLIENT_ID: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494
+ OAUTH2_GITLAB_CLIENT_SECRET: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004
+ expose:
+ - "8080"
+ depends_on:
+ - jirarush-db
+ restart: always
+
+ jirarush-db:
+ container_name: jirarush-db
+ image: postgres:latest
+ environment:
+ POSTGRES_DB: jira
+ POSTGRES_USER: jira
+ POSTGRES_PASSWORD: JiraRush
+ healthcheck:
+ test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+ restart: always
+ ports:
+ - "5433:5432"
diff --git a/pom.xml b/pom.xml
index f6c152c68..0bd8c012f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -136,6 +136,12 @@
spring-security-test
test
+
+
+ com.h2database
+ h2
+ test
+
org.junit.platform
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..fe7fd45ad 100644
--- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
+++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
@@ -25,14 +25,11 @@ 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 {
+ Files.createDirectories(Paths.get(directoryPath));
+ Files.copy(multipartFile.getInputStream(), Paths.get(directoryPath + fileName));
+ } catch (IOException ex) {
+ throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename());
}
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
index b53f7ff37..1ed7c4340 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
@@ -23,6 +23,7 @@
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import static com.javarush.jira.common.BaseHandler.createdResponse;
@@ -40,7 +41,6 @@ public class TaskController {
private final Handlers.ActivityHandler activityHandler;
private final UserBelongRepository userBelongRepository;
-
@GetMapping("/{id}")
public TaskToFull get(@PathVariable long id) {
log.info("get task by id={}", id);
@@ -156,4 +156,21 @@ public TaskTreeNode(TaskTo taskTo) {
this(taskTo, new LinkedList<>());
}
}
-}
+ @PostMapping(path = "/{id}/tags", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void addTags(@PathVariable("id") Long taskId, @RequestBody String[] tags) {
+ taskService.addTags(taskId, tags);
+ }
+
+ @GetMapping(path = "/{id}/tags")
+ public String[] getTags(@PathVariable("id") Long taskId) {
+ return taskService.getTags(taskId);
+ }
+
+ @DeleteMapping(path = "/{id}/tags")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void clearTags(@PathVariable("id") Long taskId) {
+ taskService.clearTags(taskId);
+ }
+ }
+
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..07c7fc9e0 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java
@@ -21,6 +21,7 @@
import java.time.LocalDateTime;
import java.util.List;
+import java.util.Set;
import static com.javarush.jira.bugtracking.ObjectType.TASK;
import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields;
@@ -33,6 +34,7 @@ public class TaskService {
static final String CANNOT_ASSIGN = "Cannot assign as %s to task with status=%s";
static final String CANNOT_UN_ASSIGN = "Cannot unassign as %s from task with status=%s";
+ private final TaskRepository taskRepo;
private final Handlers.TaskExtHandler handler;
private final Handlers.ActivityHandler activityHandler;
private final TaskFullMapper fullMapper;
@@ -140,4 +142,23 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass
throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode()));
}
}
+ @Transactional
+ public void addTags(Long taskId, String[] tags) {
+ Task task = taskRepo.getExisted(taskId);
+ task.getTags().addAll(Set.of(tags));
+ taskRepo.save(task);
+ }
+
+ @Transactional(readOnly = true)
+ public String[] getTags(Long taskId) {
+ Task task = taskRepo.getExisted(taskId);
+ return task.getTags().toArray(String[]::new);
+ }
+
+ @Transactional
+ public void clearTags(Long taskId) {
+ Task task = taskRepo.getExisted(taskId);
+ task.getTags().clear();
+ taskRepo.save(task);
+ }
}
diff --git a/src/main/java/com/javarush/jira/common/internal/config/AppConfig.java b/src/main/java/com/javarush/jira/common/internal/config/AppConfig.java
index fbc6ef804..253de4c8b 100644
--- a/src/main/java/com/javarush/jira/common/internal/config/AppConfig.java
+++ b/src/main/java/com/javarush/jira/common/internal/config/AppConfig.java
@@ -1,5 +1,4 @@
package com.javarush.jira.common.internal.config;
-
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -11,28 +10,29 @@
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.http.ProblemDetail;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.Executor;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
-
@Configuration
@Slf4j
@EnableCaching
@RequiredArgsConstructor
@EnableScheduling
public class AppConfig {
-
private final AppProperties appProperties;
private final Environment env;
-
@Bean("mailExecutor")
Executor getAsyncExecutor() {
return new ThreadPoolTaskExecutor() {
@@ -43,15 +43,12 @@ Executor getAsyncExecutor() {
}
};
}
-
public boolean isProd() {
return env.acceptsProfiles(Profiles.of("prod"));
}
-
public boolean isTest() {
return env.acceptsProfiles(Profiles.of("test"));
}
-
@Autowired
void configureAndStoreObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(new Hibernate5JakartaModule());
@@ -59,11 +56,23 @@ void configureAndStoreObjectMapper(ObjectMapper objectMapper) {
objectMapper.addMixIn(ProblemDetail.class, MixIn.class);
JsonUtil.setMapper(objectMapper);
}
-
// https://stackoverflow.com/a/74630129/548473
@JsonAutoDetect(fieldVisibility = NONE, getterVisibility = ANY)
interface MixIn {
@JsonAnyGetter
Map getProperties();
}
+
+ @Profile("test")
+ @Bean(name = "dataSource")
+ public DataSource dataSourceForTest() {
+ return
+ new EmbeddedDatabaseBuilder()
+ .setType(EmbeddedDatabaseType.H2)
+ .setName("testDB;MODE=PostgreSQL;NON_KEYWORDS=VALUE")
+ .setScriptEncoding("UTF-8")
+ .addScript("classpath:db/schema-test.sql")
+ .addScript("classpath:db/data.sql")
+ .build();
+ }
}
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