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> attributesResponse = oAuth2UserData.getData("response"); - if (attributesResponse != null) { - Map attributes = attributesResponse.get(0); - if (attributes != null) { - return (String) attributes.get(name); - } - } - return null; - } -} diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java deleted file mode 100644 index e8ea1ac1d..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -@Component("yandex") -public class YandexOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("default_email"); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..c71c19e8e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -8,10 +8,12 @@ app: max-pool-size: 100 spring: + config: + import: optional:file:.env-prod[.properties] init: mode: never jpa: - show-sql: true + show-sql: off open-in-view: false # validate db by model @@ -25,10 +27,11 @@ spring: default_batch_fetch_size: 20 # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 + datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -46,79 +49,56 @@ spring: cache-names: users caffeine.spec: maximumSize=10000,expireAfterAccess=5m + sql: + init: + mode: always + + mail: + properties: + mail: + smtp: + starttls: + enable: true + auth: true + host: ${MAIL_HOST} + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + port: ${MAIL_PORT} + + thymeleaf.check-template-location: false + + mvc.throw-exception-if-no-handler-found: true + web.resources.add-mappings: false + security: oauth2: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${OAUTH2_GITHUB_CLIENT_ID} + client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET} scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${OAUTH2_GOOGLE_CLIENT_ID} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET} scope: - email - profile - vk: - client-id: 51562377 - client-secret: jNM1YHQy1362Mqs49wUN - client-name: Vkontakte - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - scope: email - yandex: - client-id: 2f3395214ba84075956b76a34b231985 - client-secret: ed236c501e444a609b0f419e5e88f1e1 - client-name: Yandex - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${OAUTH2_GITLAB_CLIENT_ID} + client-secret: ${OAUTH2_GITLAB_CLIENT_SECRET} client-name: GitLab redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code scope: read_user provider: - vk: - authorization-uri: https://oauth.vk.com/authorize - token-uri: https://oauth.vk.com/access_token - user-info-uri: https://api.vk.com/method/users.get?v=8.1 - user-name-attribute: response - yandex: - authorization-uri: https://oauth.yandex.ru/authorize - token-uri: https://oauth.yandex.ru/token - user-info-uri: https://login.yandex.ru/info - user-name-attribute: login gitlab: authorization-uri: https://gitlab.com/oauth/authorize token-uri: https://gitlab.com/oauth/token user-info-uri: https://gitlab.com/api/v4/user user-name-attribute: email - sql: - init: - mode: always - - mail: - properties: - mail: - smtp: - starttls: - enable: true - auth: true - host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj - port: 587 - thymeleaf.check-template-location: false - - mvc.throw-exception-if-no-handler-found: true - web.resources.add-mappings: false - logging: level: root: WARN @@ -134,4 +114,4 @@ server: charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly enabled: true # Enable http encoding support force: true -springdoc.swagger-ui.path: /doc +springdoc.swagger-ui.path: /doc \ No newline at end of file diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index a7d43cbad..e87683ab8 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -54,8 +54,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); delete from ATTACHMENT; @@ -315,6 +314,10 @@ values ('Add role manager and filters in security', 'task', 'done', 1, 1, 1, alter sequence TASK_ID_SEQ restart with 1000; +INSERT INTO task_tag(task_id, tag) +values (1, 'foo'), + (1, 'bar'); + ---task 1------ INSERT INTO ACTIVITY(AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, PRIORITY_CODE) diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..c971b5389 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -329,3 +329,7 @@ values ('todo', 'ToDo', 3, 'in_progress,canceled|'), drop index UK_USER_BELONG; create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE) where ENDPOINT is null; + +--changeset timofej:delete_vk + +delete from reference where code = 'vk' \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..ff37bf8e2 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -9,7 +9,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications -@Sql(scripts = {"classpath:db/changelog.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) +@Sql(scripts = {"classpath:db/schema-test.sql", "classpath:db/data.sql"}, config = @SqlConfig(encoding = "UTF-8")) @AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment public abstract class AbstractControllerTest extends BaseTests { diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java index a6fd5e3bf..e6fb95f6c 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java @@ -1,8 +1,175 @@ package com.javarush.jira.profile.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.common.util.JsonUtil; +import com.javarush.jira.profile.ProfileTo; +import com.javarush.jira.profile.internal.ProfileMapper; +import com.javarush.jira.profile.internal.ProfileRepository; +import com.javarush.jira.profile.internal.model.Profile; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static com.javarush.jira.login.internal.web.UserTestData.*; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class ProfileRestControllerTest extends AbstractControllerTest { +@Autowired +private ProfileRepository repository; +@Autowired +ProfileMapper mapper; +@Test +void get_WhenUnauthorized_ReturnUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andExpect(status().isUnauthorized()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void get_WhenAuthorizedAsUser_ReturnUserProfile() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(USER_PROFILE_TO)); +} + +@Test +@WithUserDetails(value = ADMIN_MAIL) +public void get_WhenAuthorizedAsAdmin_ReturnAdminProfile() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(ADMIN_PROFILE_TO)); +} + +@Test +@WithUserDetails(value = GUEST_MAIL) +public void get_WhenAuthorizedAsGuest_ReturnEmptyProfile() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(GUEST_PROFILE_EMPTY_TO)); +} + +@Test +void update_WhenUnauthorized_ReturnUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenExistingValidProfile_SaveChangesToDb() throws Exception { + ProfileTo updatedProfileTo = getUpdatedTo(USER_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + // load updated profile from db + Profile afterUpdateProfile = repository.getExisted(USER_ID); + ProfileTo afterUpdateProfileTo = mapper.toTo(afterUpdateProfile); + + assertAll( + () -> assertEquals(afterUpdateProfileTo.id(), updatedProfileTo.id()), + () -> assertThat(afterUpdateProfileTo.getContacts()).hasSameElementsAs(updatedProfileTo.getContacts()), + () -> assertThat(afterUpdateProfileTo.getMailNotifications()).hasSameElementsAs(updatedProfileTo.getMailNotifications()) + ); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenInconsistentId_ReturnUnprocessable() throws Exception { + ProfileTo updatedProfileTo = getUpdatedTo(ADMIN_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenNullId__SaveChangesToDbForAuthorizedUserProfile() throws Exception { + ProfileTo updatedProfileTo = getUpdatedTo(null); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + // load updated profile from db + Profile afterUpdateProfile = repository.getExisted(USER_ID); + ProfileTo afterUpdateProfileTo = mapper.toTo(afterUpdateProfile); + updatedProfileTo.setId(USER_ID); + + assertAll( + () -> assertEquals(afterUpdateProfileTo.id(), updatedProfileTo.id()), + () -> assertThat(afterUpdateProfileTo.getContacts()).hasSameElementsAs(updatedProfileTo.getContacts()), + () -> assertThat(afterUpdateProfileTo.getMailNotifications()).hasSameElementsAs(updatedProfileTo.getMailNotifications()) + ); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenUnknownContact_ReturnUnprocessable() throws Exception { + ProfileTo updatedProfileTo = getWithUnknownContactTo(USER_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenInvalidProfile_ReturnUnprocessable() throws Exception { + ProfileTo updatedProfileTo = getInvalidTo(USER_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenUnknownNotifications_ReturnUnprocessable() throws Exception { + ProfileTo updatedProfileTo = getWithUnknownNotificationTo(USER_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); +} + +@Test +@WithUserDetails(value = USER_MAIL) +public void update_WhenContactHtmlUnsafe_ReturnUnprocessable() throws Exception { + ProfileTo updatedProfileTo = getWithContactHtmlUnsafeTo(USER_ID); + + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updatedProfileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); +} } \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java index fb4407268..e9a6ca47e 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java @@ -3,85 +3,68 @@ import com.javarush.jira.MatcherFactory; import com.javarush.jira.profile.ContactTo; import com.javarush.jira.profile.ProfileTo; -import com.javarush.jira.profile.internal.model.Contact; +import com.javarush.jira.profile.internal.ProfileUtil; import com.javarush.jira.profile.internal.model.Profile; +import static com.javarush.jira.login.internal.web.UserTestData.*; import java.util.Collections; import java.util.Set; public class ProfileTestData { - public static MatcherFactory.Matcher PROFILE_MATCHER = - MatcherFactory.usingIgnoringFieldsComparator(Profile.class, "user"); + public static MatcherFactory.Matcher PROFILE_TO_MATCHER = + MatcherFactory.usingIgnoringFieldsComparator(ProfileTo.class); - public static ProfileTo USER_PROFILE_TO = new ProfileTo(null, + public static final ProfileTo ADMIN_PROFILE_TO = new ProfileTo( + ADMIN_ID, + ProfileUtil.maskToNotifications(14), + Set.of( + new ContactTo("github", "adminGitHub"), + new ContactTo("tg", "adminTg"), + new ContactTo("vk", "adminVk") + )); + + public static ProfileTo USER_PROFILE_TO = new ProfileTo( + USER_ID, Set.of("assigned", "overdue", "deadline"), Set.of(new ContactTo("skype", "userSkype"), new ContactTo("mobile", "+01234567890"), new ContactTo("website", "user.com"))); - public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(null, + public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo( + GUEST_ID, Set.of(), Set.of()); - public static ProfileTo getNewTo() { - return new ProfileTo(null, - Set.of("three_days_before_deadline", "two_days_before_deadline", "one_day_before_deadline"), - Set.of(new ContactTo("tg", "guestTg"))); - } - - public static Profile getNew(long id) { - Profile profile = new Profile(); - profile.setId(id); - profile.setMailNotifications(14); - profile.setContacts(Set.of(new Contact(id, "tg", "guestTg"))); - return profile; - } - - public static ProfileTo getUpdatedTo() { - return new ProfileTo(null, + public static ProfileTo getUpdatedTo(Long id) { + return new ProfileTo(id, Set.of("assigned", "three_days_before_deadline", "two_days_before_deadline", "one_day_before_deadline", "deadline", "overdue"), Set.of(new ContactTo("skype", "newSkype"), new ContactTo("mobile", "+380987654321"), new ContactTo("website", "new.com"), new ContactTo("github", "newGitHub"), new ContactTo("tg", "newTg"), - new ContactTo("vk", "newVk"), new ContactTo("linkedin", "newLinkedin"))); } - public static Profile getUpdated(long id) { - Profile profile = new Profile(); - profile.setId(id); - profile.setMailNotifications(63); - profile.setContacts(Set.of(new Contact(id, "skype", "newSkype"), - new Contact(id, "mobile", "+380987654321"), - new Contact(id, "website", "new.com"), - new Contact(id, "github", "newGitHub"), - new Contact(id, "tg", "newTg"), - new Contact(id, "vk", "newVk"), - new Contact(id, "linkedin", "newLinkedin"))); - return profile; - } - - public static ProfileTo getInvalidTo() { - return new ProfileTo(null, + public static ProfileTo getInvalidTo(Long id) { + return new ProfileTo(id, Set.of(""), Set.of(new ContactTo("skype", ""))); } - public static ProfileTo getWithUnknownNotificationTo() { - return new ProfileTo(null, + public static ProfileTo getWithUnknownNotificationTo(long id) { + return new ProfileTo(id, Set.of("WrongNotification"), Collections.emptySet()); } - public static ProfileTo getWithUnknownContactTo() { - return new ProfileTo(null, + public static ProfileTo getWithUnknownContactTo(Long id) { + return new ProfileTo(id, Collections.emptySet(), Set.of(new ContactTo("WrongContactCode", "contact"))); } - public static ProfileTo getWithContactHtmlUnsafeTo() { - return new ProfileTo(null, + public static ProfileTo getWithContactHtmlUnsafeTo(Long id) { + return new ProfileTo(id, Collections.emptySet(), Set.of(new ContactTo("tg", ""))); } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..63651c5e3 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,3 @@ -spring.cache.type: none spring: - init: - mode: always - datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + cache.type: none + liquibase.enabled: false \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/db/data.sql similarity index 88% rename from src/test/resources/data.sql rename to src/test/resources/db/data.sql index 5087dbddc..8fda54489 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/db/data.sql @@ -1,33 +1,3 @@ ---------- users ---------------------- -delete -from USER_ROLE; -delete -from CONTACT; -delete -from PROFILE; - -delete -from ACTIVITY; -alter -sequence ACTIVITY_ID_SEQ restart with 1; -delete -from TASK; -alter -sequence TASK_ID_SEQ restart with 1; -delete -from SPRINT; -alter -sequence SPRINT_ID_SEQ restart with 1; -delete -from PROJECT; -alter -sequence PROJECT_ID_SEQ restart with 1; - -delete -from USERS; -alter -sequence USERS_ID_SEQ restart with 1; - insert into USERS (EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) values ('user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), ('admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), diff --git a/src/test/resources/db/schema-test.sql b/src/test/resources/db/schema-test.sql new file mode 100644 index 000000000..10249bdb8 --- /dev/null +++ b/src/test/resources/db/schema-test.sql @@ -0,0 +1,269 @@ +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP TABLE IF EXISTS ACTIVITY; +DROP TABLE IF EXISTS TASK; +DROP TABLE IF EXISTS SPRINT; +DROP TABLE IF EXISTS PROJECT; +DROP TABLE IF EXISTS REFERENCE; +DROP TABLE IF EXISTS ATTACHMENT; +DROP TABLE IF EXISTS USERS; + +create table PROJECT +( + ID bigserial primary key, + CODE varchar(32) not null + constraint UK_PROJECT_CODE unique, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + PARENT_ID bigint, + constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade +); + +create table MAIL_CASE +( + ID bigserial primary key, + EMAIL varchar(255) not null, + NAME varchar(255) not null, + DATE_TIME timestamp not null, + RESULT varchar(255) not null, + TEMPLATE varchar(255) not null +); + +create table SPRINT +( + ID bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial primary key, + CODE varchar(32) not null, + REF_TYPE smallint not null, + ENDPOINT timestamp, + STARTPOINT timestamp, + TITLE varchar(1024) not null, + AUX varchar, + constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) +); + +create table USERS +( + ID bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); + +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'); + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT +alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK +DROP COLUMN DESCRIPTION; +ALTER TABLE TASK +DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK +DROP COLUMN ESTIMATE; +ALTER TABLE TASK +DROP COLUMN UPDATED; + +alter table ACTIVITY +drop constraint FK_ACTIVITY_USERS; +alter table ACTIVITY + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG +drop constraint FK_USER_BELONG; +alter table USER_BELONG + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT +drop constraint FK_ATTACHMENT; +alter table ATTACHMENT + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); \ No newline at end of file