diff --git a/351004/Halukha/discussion/pom.xml b/351004/Halukha/discussion/pom.xml
new file mode 100644
index 000000000..d20b2ce09
--- /dev/null
+++ b/351004/Halukha/discussion/pom.xml
@@ -0,0 +1,55 @@
+
+ 4.0.0
+
+ com.example
+ lab-parent
+ 0.0.1-SNAPSHOT
+
+
+ discussion
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-cassandra
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.mapstruct
+ mapstruct
+ 1.5.5.Final
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+ provided
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ io.rest-assured
+ json-path
+ test
+
+
+
\ No newline at end of file
diff --git a/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/LabDiscussionApplication.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/LabDiscussionApplication.java
new file mode 100644
index 000000000..63643bc46
--- /dev/null
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/LabDiscussionApplication.java
@@ -0,0 +1,15 @@
+package com.example.lab.discussion;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class LabDiscussionApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LabDiscussionApplication.class, args);
+ }
+}
+
+// netstat -ano | findstr :
+// taskkill /PID /F
diff --git a/351004/Halukha/src/main/java/com/example/lab/controller/PostController.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/controller/PostController.java
similarity index 89%
rename from 351004/Halukha/src/main/java/com/example/lab/controller/PostController.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/controller/PostController.java
index be9f472fe..56613a581 100644
--- a/351004/Halukha/src/main/java/com/example/lab/controller/PostController.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/controller/PostController.java
@@ -1,7 +1,6 @@
-package com.example.lab.controller;
+package com.example.lab.discussion.controller;
import java.util.List;
-
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -13,9 +12,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import com.example.lab.dto.PostRequestTo;
-import com.example.lab.dto.PostResponseTo;
-import com.example.lab.service.PostService;
+import com.example.lab.discussion.dto.PostRequestTo;
+import com.example.lab.discussion.dto.PostResponseTo;
+import com.example.lab.discussion.service.PostService;
import jakarta.validation.Valid;
@@ -54,4 +53,4 @@ public ResponseEntity deletePost(@PathVariable Long id) {
postService.deletePost(id);
return ResponseEntity.noContent().build();
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/PostRequestTo.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostRequestTo.java
similarity index 89%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/PostRequestTo.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostRequestTo.java
index 6f3aeb79c..633657d13 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/PostRequestTo.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostRequestTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.discussion.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -13,6 +13,8 @@ public class PostRequestTo {
@Size(min = 2, max = 2048)
private String content;
+ public PostRequestTo() {}
+
public PostRequestTo(
@JsonProperty("newsId") Long newsId,
@JsonProperty("content") String content) {
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/PostResponseTo.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostResponseTo.java
similarity index 93%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/PostResponseTo.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostResponseTo.java
index 76c3fde2f..2a20f4d7d 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/PostResponseTo.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/dto/PostResponseTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.discussion.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
diff --git a/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/EntityNotFoundException.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/EntityNotFoundException.java
new file mode 100644
index 000000000..f3c8b412b
--- /dev/null
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/EntityNotFoundException.java
@@ -0,0 +1,12 @@
+package com.example.lab.discussion.exception;
+
+public class EntityNotFoundException extends RuntimeException {
+ private final int errorCode;
+
+ public EntityNotFoundException(String message, int errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public int getErrorCode() { return errorCode; }
+}
\ No newline at end of file
diff --git a/351004/Halukha/src/main/java/com/example/lab/exception/GlobalExceptionHandler.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/GlobalExceptionHandler.java
similarity index 97%
rename from 351004/Halukha/src/main/java/com/example/lab/exception/GlobalExceptionHandler.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/GlobalExceptionHandler.java
index b3e14f2d6..5246cfaf0 100644
--- a/351004/Halukha/src/main/java/com/example/lab/exception/GlobalExceptionHandler.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/exception/GlobalExceptionHandler.java
@@ -1,4 +1,4 @@
-package com.example.lab.exception;
+package com.example.lab.discussion.exception;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
@@ -50,4 +50,4 @@ public ErrorResponse(int errorCode, String errorMessage) {
public int getErrorCode() { return errorCode; }
public String getErrorMessage() { return errorMessage; }
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/src/main/java/com/example/lab/mapper/PostMapper.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/mapper/PostMapper.java
similarity index 75%
rename from 351004/Halukha/src/main/java/com/example/lab/mapper/PostMapper.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/mapper/PostMapper.java
index 98241b181..d3b96dc74 100644
--- a/351004/Halukha/src/main/java/com/example/lab/mapper/PostMapper.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/mapper/PostMapper.java
@@ -1,13 +1,13 @@
-package com.example.lab.mapper;
+package com.example.lab.discussion.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
-import com.example.lab.dto.PostRequestTo;
-import com.example.lab.dto.PostResponseTo;
-import com.example.lab.model.Post;
+import com.example.lab.discussion.dto.PostRequestTo;
+import com.example.lab.discussion.dto.PostResponseTo;
+import com.example.lab.discussion.model.Post;
@Mapper
public interface PostMapper {
diff --git a/351004/Halukha/src/main/java/com/example/lab/model/Post.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/model/Post.java
similarity index 65%
rename from 351004/Halukha/src/main/java/com/example/lab/model/Post.java
rename to 351004/Halukha/discussion/src/main/java/com/example/lab/discussion/model/Post.java
index 4b9eb3fe1..e0f5977c2 100644
--- a/351004/Halukha/src/main/java/com/example/lab/model/Post.java
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/model/Post.java
@@ -1,29 +1,24 @@
-package com.example.lab.model;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+package com.example.lab.discussion.model;
+
+import org.springframework.data.cassandra.core.mapping.PrimaryKey;
+import org.springframework.data.cassandra.core.mapping.Table;
+import org.springframework.data.cassandra.core.mapping.Column;
+
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
-@Entity
-@Table(name = "tbl_post")
+@Table("tbl_post")
public class Post {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "id")
+ @PrimaryKey
private Long id;
- @Column(name = "newsId")
+ @Column("newsId")
private Long newsId;
@NotBlank
@Size(min = 2, max = 2048)
- @Column(name = "content")
+ @Column("content")
private String content;
public Post() {
@@ -39,23 +34,23 @@ public Long getId() {
return id;
}
- public Long getNewsId() {
- return newsId;
- }
-
- public String getContent() {
- return content;
- }
-
public void setId(Long id) {
this.id = id;
}
+ public Long getNewsId() {
+ return newsId;
+ }
+
public void setNewsId(Long newsId) {
this.newsId = newsId;
}
+ public String getContent() {
+ return content;
+ }
+
public void setContent(String content) {
this.content = content;
}
-}
+}
\ No newline at end of file
diff --git a/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/repository/PostRepository.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/repository/PostRepository.java
new file mode 100644
index 000000000..f87c35c27
--- /dev/null
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/repository/PostRepository.java
@@ -0,0 +1,7 @@
+package com.example.lab.discussion.repository;
+
+import org.springframework.data.cassandra.repository.CassandraRepository;
+import com.example.lab.discussion.model.Post;
+
+public interface PostRepository extends CassandraRepository {
+}
diff --git a/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/service/PostService.java b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/service/PostService.java
new file mode 100644
index 000000000..b1148950e
--- /dev/null
+++ b/351004/Halukha/discussion/src/main/java/com/example/lab/discussion/service/PostService.java
@@ -0,0 +1,80 @@
+package com.example.lab.discussion.service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import com.example.lab.discussion.dto.PostRequestTo;
+import com.example.lab.discussion.dto.PostResponseTo;
+import com.example.lab.discussion.exception.EntityNotFoundException;
+import com.example.lab.discussion.mapper.PostMapper;
+import com.example.lab.discussion.model.Post;
+import com.example.lab.discussion.repository.PostRepository;
+
+@Service
+public class PostService {
+
+ private final PostRepository postRepository;
+ private final PostMapper mapper = PostMapper.INSTANCE;
+
+ private final WebClient webClient;
+
+ public PostService(PostRepository postRepository, WebClient.Builder webClientBuilder) {
+ this.postRepository = postRepository;
+ this.webClient = webClientBuilder.baseUrl("http://localhost:24110").build();
+ }
+
+ public List getAllPost() {
+ return postRepository.findAll().stream()
+ .map(mapper::toDto)
+ .collect(Collectors.toList());
+ }
+
+ public PostResponseTo getPostById(Long id) {
+ return postRepository.findById(id)
+ .map(mapper::toDto)
+ .orElseThrow(() -> new EntityNotFoundException("Post not found", 40401));
+ }
+
+ public PostResponseTo createPost(PostRequestTo request) {
+ try {
+ Boolean exists = webClient.get()
+ .uri("/api/v1.0/news/{id}", request.getNewsId())
+ .retrieve()
+ .toBodilessEntity()
+ .map(response -> response.getStatusCode().is2xxSuccessful())
+ .block();
+
+ if (exists == null || !exists) {
+ throw new EntityNotFoundException("News not found", 40401);
+ }
+ } catch (Exception e) {
+ throw new EntityNotFoundException("News not found or service unavailable", 40401);
+ }
+
+ Post post = new Post();
+ post.setId(System.currentTimeMillis());
+ post.setNewsId(request.getNewsId());
+ post.setContent(request.getContent());
+
+ return mapper.toDto(postRepository.save(post));
+ }
+
+ public PostResponseTo updatePost(Long id, PostRequestTo request) {
+ Post existing = postRepository.findById(id)
+ .orElseThrow(() -> new EntityNotFoundException("Post not found", 40401));
+ Post updated = mapper.updateEntity(request, existing);
+ updated.setId(id);
+ Post saved = postRepository.save(updated);
+ return mapper.toDto(saved);
+ }
+
+ public void deletePost(Long id) {
+ if (!postRepository.existsById(id)) {
+ throw new EntityNotFoundException("Post not found", 40401);
+ }
+ postRepository.deleteById(id);
+ }
+}
diff --git a/351004/Halukha/discussion/src/main/resources/application.yaml b/351004/Halukha/discussion/src/main/resources/application.yaml
new file mode 100644
index 000000000..80510f087
--- /dev/null
+++ b/351004/Halukha/discussion/src/main/resources/application.yaml
@@ -0,0 +1,14 @@
+server:
+ port: 24130
+
+spring:
+ application:
+ name: discussion-service
+
+ cassandra:
+ contact-points: localhost
+ port: 9042
+ keyspace-name: distcomp
+ local-datacenter: datacenter1 # Исправлено здесь
+ schema-action: create_if_not_exists
+
\ No newline at end of file
diff --git a/351004/Halukha/src/test/java/com/example/lab/LabApplicationTests.java b/351004/Halukha/discussion/src/test/java/com/example/lab/LabApplicationTests.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/LabApplicationTests.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/LabApplicationTests.java
diff --git a/351004/Halukha/src/test/java/com/example/lab/test/BaseIntegrationTest.java b/351004/Halukha/discussion/src/test/java/com/example/lab/test/BaseIntegrationTest.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/test/BaseIntegrationTest.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/test/BaseIntegrationTest.java
diff --git a/351004/Halukha/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java b/351004/Halukha/discussion/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java
diff --git a/351004/Halukha/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java b/351004/Halukha/discussion/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java
diff --git a/351004/Halukha/src/test/java/com/example/lab/test/PostControllerIntegrationTest.java b/351004/Halukha/discussion/src/test/java/com/example/lab/test/PostControllerIntegrationTest.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/test/PostControllerIntegrationTest.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/test/PostControllerIntegrationTest.java
diff --git a/351004/Halukha/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java b/351004/Halukha/discussion/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java
similarity index 100%
rename from 351004/Halukha/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java
rename to 351004/Halukha/discussion/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java
diff --git a/351004/Halukha/pom.xml b/351004/Halukha/pom.xml
index ed4ef4f75..705ba106b 100644
--- a/351004/Halukha/pom.xml
+++ b/351004/Halukha/pom.xml
@@ -1,124 +1,32 @@
-
+
-
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- com.example
- lab
- 0.0.1-SNAPSHOT
- jar
-
-
+
org.springframework.boot
spring-boot-starter-parent
- 3.4.0
+ 3.2.0
+ com.example
+ lab-parent
+ 0.0.1-SNAPSHOT
+ pom
+ publisher
+ discussion
+
+
- 18
+ 17
-
-
-
- org.mapstruct
- mapstruct
- 1.5.5.Final
-
-
-
org.projectlombok
lombok
- provided
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
-
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
-
-
-
- org.springframework.boot
- spring-boot-starter-validation
+ true
-
-
-
- com.fasterxml.jackson.core
- jackson-databind
-
-
-
-
- org.postgresql
- postgresql
- runtime
-
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
-
- io.rest-assured
- rest-assured
- test
-
-
-
- io.rest-assured
- json-path
- test
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.11.0
-
-
-
-
- org.mapstruct
- mapstruct-processor
- 1.5.5.Final
-
-
-
- org.projectlombok
- lombok
- 1.18.34
-
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
-
-
+
\ No newline at end of file
diff --git a/351004/Halukha/publisher/pom.xml b/351004/Halukha/publisher/pom.xml
new file mode 100644
index 000000000..141da605a
--- /dev/null
+++ b/351004/Halukha/publisher/pom.xml
@@ -0,0 +1,94 @@
+
+ 4.0.0
+
+ com.example
+ lab-parent
+ 0.0.1-SNAPSHOT
+
+
+ publisher
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.mapstruct
+ mapstruct
+ 1.5.5.Final
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+ provided
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ io.rest-assured
+ json-path
+ test
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.5
+ runtime
+
+
+
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/LabPublisherApplication.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/LabPublisherApplication.java
new file mode 100644
index 000000000..d2cd3a4f2
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/LabPublisherApplication.java
@@ -0,0 +1,17 @@
+package com.example.lab.publisher;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+
+@SpringBootApplication
+@EnableCaching
+public class LabPublisherApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LabPublisherApplication.class, args);
+ }
+}
+
+// docker exec -it cassandra cqlsh
+// CREATE KEYSPACE distcomp WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/config/RedisCacheConfig.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/config/RedisCacheConfig.java
new file mode 100644
index 000000000..e91b6b7f7
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/config/RedisCacheConfig.java
@@ -0,0 +1,77 @@
+package com.example.lab.publisher.config;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cache.Cache;
+import org.springframework.cache.interceptor.CacheErrorHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.RedisConnectionFailureException;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+@Configuration
+public class RedisCacheConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(RedisCacheConfig.class);
+
+ @Bean
+ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
+ .serializeValuesWith(RedisSerializationContext.SerializationPair
+ .fromSerializer(new GenericJackson2JsonRedisSerializer()))
+ .entryTtl(Duration.ofMinutes(5));
+
+ Map perCache = new HashMap<>();
+ perCache.put("users", defaults.entryTtl(Duration.ofMinutes(10)));
+ perCache.put("news", defaults.entryTtl(Duration.ofMinutes(5)));
+ perCache.put("markers", defaults.entryTtl(Duration.ofMinutes(30)));
+ perCache.put("newsUser", defaults.entryTtl(Duration.ofMinutes(5)));
+
+ return RedisCacheManager.builder(connectionFactory)
+ .cacheDefaults(defaults)
+ .withInitialCacheConfigurations(perCache)
+ .build();
+ }
+
+ @Bean
+ public CacheErrorHandler cacheErrorHandler() {
+ return new CacheErrorHandler() {
+ @Override
+ public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
+ swallowIfRedisDown(exception, cache, key);
+ }
+
+ @Override
+ public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
+ swallowIfRedisDown(exception, cache, key);
+ }
+
+ @Override
+ public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
+ swallowIfRedisDown(exception, cache, key);
+ }
+
+ @Override
+ public void handleCacheClearError(RuntimeException exception, Cache cache) {
+ swallowIfRedisDown(exception, cache, null);
+ }
+
+ private void swallowIfRedisDown(RuntimeException exception, Cache cache, Object key) {
+ if (exception instanceof RedisConnectionFailureException) {
+ log.warn("Redis unavailable; skipping cache operation (cache={}, key={})", cache.getName(), key);
+ return;
+ }
+ throw exception;
+ }
+ };
+ }
+}
+
diff --git a/351004/Halukha/src/main/java/com/example/lab/controller/MarkerController.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/MarkerController.java
similarity index 86%
rename from 351004/Halukha/src/main/java/com/example/lab/controller/MarkerController.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/MarkerController.java
index f3647c1d3..26b9de245 100644
--- a/351004/Halukha/src/main/java/com/example/lab/controller/MarkerController.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/MarkerController.java
@@ -1,4 +1,4 @@
-package com.example.lab.controller;
+package com.example.lab.publisher.controller;
import java.util.List;
@@ -13,9 +13,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import com.example.lab.dto.MarkerRequestTo;
-import com.example.lab.dto.MarkerResponseTo;
-import com.example.lab.service.MarkerService;
+import com.example.lab.publisher.dto.MarkerRequestTo;
+import com.example.lab.publisher.dto.MarkerResponseTo;
+import com.example.lab.publisher.service.MarkerService;
import jakarta.validation.Valid;
@@ -45,7 +45,8 @@ public ResponseEntity createMarker(@Valid @RequestBody MarkerR
}
@PutMapping("/{id}")
- public ResponseEntity updateMarker(@PathVariable Long id, @Valid @RequestBody MarkerRequestTo marker) {
+ public ResponseEntity updateMarker(@PathVariable Long id,
+ @Valid @RequestBody MarkerRequestTo marker) {
return ResponseEntity.ok(markerService.updateMarker(id, marker));
}
@@ -54,4 +55,4 @@ public ResponseEntity deleteMarker(@PathVariable Long id) {
markerService.deleteMarker(id);
return ResponseEntity.noContent().build();
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/src/main/java/com/example/lab/controller/NewsController.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/NewsController.java
similarity index 88%
rename from 351004/Halukha/src/main/java/com/example/lab/controller/NewsController.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/NewsController.java
index 25a75c049..d835f368c 100644
--- a/351004/Halukha/src/main/java/com/example/lab/controller/NewsController.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/NewsController.java
@@ -1,4 +1,4 @@
-package com.example.lab.controller;
+package com.example.lab.publisher.controller;
import java.util.List;
@@ -13,10 +13,10 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import com.example.lab.dto.NewsRequestTo;
-import com.example.lab.dto.NewsResponseTo;
-import com.example.lab.dto.UserResponseTo;
-import com.example.lab.service.NewsService;
+import com.example.lab.publisher.dto.NewsRequestTo;
+import com.example.lab.publisher.dto.NewsResponseTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.service.NewsService;
import jakarta.validation.Valid;
@@ -60,4 +60,4 @@ public ResponseEntity deleteNews(@PathVariable Long id) {
public ResponseEntity getUserByNewsId(@PathVariable Long id) {
return ResponseEntity.ok(newsService.getUserByNewsId(id));
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/PostProxyController.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/PostProxyController.java
new file mode 100644
index 000000000..beace4d09
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/PostProxyController.java
@@ -0,0 +1,94 @@
+package com.example.lab.publisher.controller;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+
+import com.example.lab.publisher.service.PostRedisCacheService;
+
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/api/v1.0/posts")
+public class PostProxyController {
+
+ private final WebClient webClient;
+ private final PostRedisCacheService postCache;
+
+ public PostProxyController(WebClient.Builder webClientBuilder, PostRedisCacheService postCache) {
+ this.webClient = webClientBuilder.baseUrl("http://localhost:24130").build();
+ this.postCache = postCache;
+ }
+
+ @GetMapping
+ public Mono> getAllPosts() {
+ return webClient.get()
+ .uri("/api/v1.0/posts")
+ .exchangeToMono(response -> response.toEntity(Object.class));
+ }
+
+ @GetMapping("/{id}")
+ public Mono> getPostById(@PathVariable Long id) {
+ Object cached = postCache.get(id);
+ if (cached != null) {
+ return Mono.just(ResponseEntity.ok(cached));
+ }
+ return webClient.get()
+ .uri("/api/v1.0/posts/{id}", id)
+ .exchangeToMono(response -> response.toEntity(Object.class))
+ .doOnNext(entity -> {
+ if (entity.getStatusCode().is2xxSuccessful() && entity.getBody() != null) {
+ postCache.put(id, entity.getBody());
+ }
+ });
+ }
+
+ @PostMapping
+ public Mono> createPost(@RequestBody Object post) {
+ return webClient.post()
+ .uri("/api/v1.0/posts")
+ .bodyValue(post)
+ .retrieve()
+ .toEntity(Object.class)
+ .doOnNext(entity -> {
+ if (entity.getStatusCode().is2xxSuccessful() && entity.getBody() instanceof java.util.Map, ?> map) {
+ Object idValue = map.get("id");
+ if (idValue instanceof Number n) {
+ postCache.put(n.longValue(), entity.getBody());
+ }
+ }
+ })
+ .onErrorResume(WebClientResponseException.class, e -> {
+ Object errorBody = e.getResponseBodyAs(Object.class);
+ return Mono.just(ResponseEntity
+ .status(e.getStatusCode())
+ .body(errorBody));
+ });
+ }
+
+ @PutMapping("/{id}")
+ public Mono> updatePost(@PathVariable Long id, @RequestBody Object post) {
+ return webClient.put()
+ .uri("/api/v1.0/posts/{id}", id)
+ .bodyValue(post)
+ .exchangeToMono(response -> response.toEntity(Object.class))
+ .doOnNext(entity -> {
+ if (entity.getStatusCode().is2xxSuccessful() && entity.getBody() != null) {
+ postCache.put(id, entity.getBody());
+ }
+ });
+ }
+
+ @DeleteMapping("/{id}")
+ public Mono> deletePost(@PathVariable Long id) {
+ return webClient.delete()
+ .uri("/api/v1.0/posts/{id}", id)
+ .exchangeToMono(response -> response.toBodilessEntity())
+ .doOnNext(entity -> {
+ if (entity.getStatusCode().is2xxSuccessful()) {
+ postCache.evict(id);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/src/main/java/com/example/lab/controller/UserController.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/UserController.java
similarity index 89%
rename from 351004/Halukha/src/main/java/com/example/lab/controller/UserController.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/UserController.java
index 384c94b5a..9b179363d 100644
--- a/351004/Halukha/src/main/java/com/example/lab/controller/UserController.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/UserController.java
@@ -1,4 +1,4 @@
-package com.example.lab.controller;
+package com.example.lab.publisher.controller;
import java.util.List;
@@ -13,9 +13,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import com.example.lab.dto.UserRequestTo;
-import com.example.lab.dto.UserResponseTo;
-import com.example.lab.service.UserService;
+import com.example.lab.publisher.dto.UserRequestTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.service.UserService;
import jakarta.validation.Valid;
@@ -54,4 +54,4 @@ public ResponseEntity deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/MarkerControllerV2.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/MarkerControllerV2.java
new file mode 100644
index 000000000..7488f3482
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/MarkerControllerV2.java
@@ -0,0 +1,62 @@
+package com.example.lab.publisher.controller.v2;
+
+import java.util.List;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.lab.publisher.dto.MarkerRequestTo;
+import com.example.lab.publisher.dto.MarkerResponseTo;
+import com.example.lab.publisher.service.MarkerService;
+
+import jakarta.validation.Valid;
+
+@RestController
+@RequestMapping("/api/v2.0/markers")
+public class MarkerControllerV2 {
+
+ private final MarkerService markerService;
+
+ public MarkerControllerV2(MarkerService markerService) {
+ this.markerService = markerService;
+ }
+
+ @GetMapping
+ public ResponseEntity> getAllMarker() {
+ return ResponseEntity.ok(markerService.getAllMarker());
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getMarker(@PathVariable Long id) {
+ return ResponseEntity.ok(markerService.getMarkerById(id));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN')")
+ public ResponseEntity createMarker(@Valid @RequestBody MarkerRequestTo marker) {
+ return ResponseEntity.status(HttpStatus.CREATED).body(markerService.createMarker(marker));
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ResponseEntity updateMarker(@PathVariable Long id, @Valid @RequestBody MarkerRequestTo marker) {
+ return ResponseEntity.ok(markerService.updateMarker(id, marker));
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN')")
+ public ResponseEntity deleteMarker(@PathVariable Long id) {
+ markerService.deleteMarker(id);
+ return ResponseEntity.noContent().build();
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/NewsControllerV2.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/NewsControllerV2.java
new file mode 100644
index 000000000..2875f818b
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/NewsControllerV2.java
@@ -0,0 +1,83 @@
+package com.example.lab.publisher.controller.v2;
+
+import java.util.List;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.lab.publisher.dto.NewsRequestTo;
+import com.example.lab.publisher.dto.NewsResponseTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.security.OwnershipService;
+import com.example.lab.publisher.service.NewsService;
+
+import jakarta.validation.Valid;
+
+@RestController
+@RequestMapping("/api/v2.0/news")
+public class NewsControllerV2 {
+
+ private final NewsService newsService;
+ private final OwnershipService ownership;
+
+ public NewsControllerV2(NewsService newsService, OwnershipService ownership) {
+ this.newsService = newsService;
+ this.ownership = ownership;
+ }
+
+ @GetMapping
+ public ResponseEntity> getAllNews() {
+ return ResponseEntity.ok(newsService.getAllNews());
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getNews(@PathVariable Long id) {
+ return ResponseEntity.ok(newsService.getNewsById(id));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN') or hasRole('CUSTOMER')")
+ public ResponseEntity createNews(@Valid @RequestBody NewsRequestTo news) {
+ Long me = ownership.currentUserId();
+ NewsRequestTo adjusted = news;
+ if (me != null) {
+ adjusted = new NewsRequestTo(me, news.getTitle(), news.getContent(), news.getCreated(), news.getModified(),
+ news.getMarkers());
+ }
+ return ResponseEntity.status(HttpStatus.CREATED).body(newsService.createNews(adjusted));
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or @ownership.canModifyNews(#id)")
+ public ResponseEntity updateNews(@PathVariable Long id, @Valid @RequestBody NewsRequestTo news) {
+ Long me = ownership.currentUserId();
+ NewsRequestTo adjusted = news;
+ if (me != null) {
+ adjusted = new NewsRequestTo(me, news.getTitle(), news.getContent(), news.getCreated(), news.getModified(),
+ news.getMarkers());
+ }
+ return ResponseEntity.ok(newsService.updateNews(id, adjusted));
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or @ownership.canModifyNews(#id)")
+ public ResponseEntity deleteNews(@PathVariable Long id) {
+ newsService.deleteNews(id);
+ return ResponseEntity.noContent().build();
+ }
+
+ @GetMapping("/user/{id}")
+ public ResponseEntity getUserByNewsId(@PathVariable Long id) {
+ return ResponseEntity.ok(newsService.getUserByNewsId(id));
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/PostProxyControllerV2.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/PostProxyControllerV2.java
new file mode 100644
index 000000000..beb9e2ff0
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/PostProxyControllerV2.java
@@ -0,0 +1,127 @@
+package com.example.lab.publisher.controller.v2;
+
+import java.util.Map;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+
+import com.example.lab.publisher.exception.GlobalExceptionHandler.ErrorResponse;
+import com.example.lab.publisher.security.OwnershipService;
+
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/api/v2.0/posts")
+public class PostProxyControllerV2 {
+
+ private final WebClient webClient;
+ private final OwnershipService ownership;
+
+ public PostProxyControllerV2(WebClient.Builder webClientBuilder, OwnershipService ownership) {
+ this.webClient = webClientBuilder.baseUrl("http://localhost:24130").build();
+ this.ownership = ownership;
+ }
+
+ @GetMapping
+ public Mono> getAllPosts() {
+ return webClient.get()
+ .uri("/api/v1.0/posts")
+ .exchangeToMono(response -> response.toEntity(Object.class));
+ }
+
+ @GetMapping("/{id}")
+ public Mono> getPostById(@PathVariable Long id) {
+ return webClient.get()
+ .uri("/api/v1.0/posts/{id}", id)
+ .exchangeToMono(response -> response.toEntity(Object.class));
+ }
+
+ @PostMapping
+ @PreAuthorize("hasRole('ADMIN') or hasRole('CUSTOMER')")
+ public Mono> createPost(@RequestBody Object post) {
+ Long newsId = extractNewsId(post);
+ if (!canWrite(newsId)) {
+ return Mono.just(ResponseEntity.status(403).body(new ErrorResponse(40301, "Forbidden")));
+ }
+ return webClient.post()
+ .uri("/api/v1.0/posts")
+ .bodyValue(post)
+ .retrieve()
+ .toEntity(Object.class)
+ .onErrorResume(WebClientResponseException.class, e -> {
+ Object errorBody = e.getResponseBodyAs(Object.class);
+ return Mono.just(ResponseEntity.status(e.getStatusCode()).body(errorBody));
+ });
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or hasRole('CUSTOMER')")
+ public Mono> updatePost(@PathVariable Long id, @RequestBody Object post) {
+ Long newsId = extractNewsId(post);
+ if (!canWrite(newsId)) {
+ return Mono.just(ResponseEntity.status(403).body(new ErrorResponse(40301, "Forbidden")));
+ }
+ return webClient.put()
+ .uri("/api/v1.0/posts/{id}", id)
+ .bodyValue(post)
+ .exchangeToMono(response -> response.toEntity(Object.class));
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or hasRole('CUSTOMER')")
+ public Mono> deletePost(@PathVariable Long id) {
+ return webClient.get()
+ .uri("/api/v1.0/posts/{id}", id)
+ .exchangeToMono(response -> response.toEntity(Object.class))
+ .flatMap(entity -> {
+ Long newsId = extractNewsId(entity.getBody());
+ if (!canWrite(newsId)) {
+ return Mono.just(ResponseEntity.status(403).build());
+ }
+ return webClient.delete()
+ .uri("/api/v1.0/posts/{id}", id)
+ .exchangeToMono(resp -> resp.toBodilessEntity());
+ });
+ }
+
+ private boolean canWrite(Long newsId) {
+ if (newsId == null) {
+ return false;
+ }
+ if (isAdmin()) {
+ return true;
+ }
+ return ownership.canModifyNews(newsId);
+ }
+
+ private boolean isAdmin() {
+ Authentication a = SecurityContextHolder.getContext().getAuthentication();
+ if (a == null) {
+ return false;
+ }
+ return a.getAuthorities().stream().anyMatch(ga -> "ROLE_ADMIN".equals(ga.getAuthority()));
+ }
+
+ private Long extractNewsId(Object body) {
+ if (body instanceof Map, ?> map) {
+ Object v = map.get("newsId");
+ if (v instanceof Number n) {
+ return n.longValue();
+ }
+ }
+ return null;
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/UserControllerV2.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/UserControllerV2.java
new file mode 100644
index 000000000..b07d0e258
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/controller/v2/UserControllerV2.java
@@ -0,0 +1,80 @@
+package com.example.lab.publisher.controller.v2;
+
+import java.util.List;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.lab.publisher.dto.UserRequestTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.exception.EntityNotFoundException;
+import com.example.lab.publisher.mapper.UserMapper;
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.UserRepository;
+
+import jakarta.validation.Valid;
+
+@RestController
+@RequestMapping("/api/v2.0/users")
+public class UserControllerV2 {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final UserMapper mapper = UserMapper.INSTANCE;
+
+ public UserControllerV2(UserRepository userRepository, PasswordEncoder passwordEncoder) {
+ this.userRepository = userRepository;
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ @GetMapping
+ public ResponseEntity> getAllUsers() {
+ List users = userRepository.findAll().stream().map(mapper::toDto).toList();
+ return ResponseEntity.ok(users);
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getUser(@PathVariable Long id) {
+ return ResponseEntity.ok(userRepository.findById(id).map(mapper::toDto)
+ .orElseThrow(() -> new EntityNotFoundException("User not found", 40401)));
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or @ownership.canAccessUser(#id)")
+ public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody UserRequestTo req) {
+ User existing = userRepository.findById(id)
+ .orElseThrow(() -> new EntityNotFoundException("User not found", 40401));
+
+ User updated = mapper.updateEntity(req, existing);
+ updated.setId(id);
+ updated.setPassword(passwordEncoder.encode(req.getPassword()));
+ User saved = userRepository.save(updated);
+ return ResponseEntity.ok(mapper.toDto(saved));
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN') or @ownership.canAccessUser(#id)")
+ public ResponseEntity deleteUser(@PathVariable Long id) {
+ if (!userRepository.existsById(id)) {
+ throw new EntityNotFoundException("User not found", 40401);
+ }
+ userRepository.deleteById(id);
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ }
+
+ static Authentication auth() {
+ return SecurityContextHolder.getContext().getAuthentication();
+ }
+}
+
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/MarkerRequestTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerRequestTo.java
similarity index 91%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/MarkerRequestTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerRequestTo.java
index 9000686d0..49e4d18b7 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/MarkerRequestTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerRequestTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/MarkerResponseTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerResponseTo.java
similarity index 91%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/MarkerResponseTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerResponseTo.java
index 5554df9f3..70909ec84 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/MarkerResponseTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/MarkerResponseTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/NewsRequestTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsRequestTo.java
similarity index 90%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/NewsRequestTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsRequestTo.java
index fdf1aceef..8d9f8a04f 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/NewsRequestTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsRequestTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import java.time.LocalDateTime;
import java.util.List;
@@ -23,27 +23,27 @@ public class NewsRequestTo {
@Size(min = 4, max = 2048)
private String content;
- private List markers;
-
@DateTimeFormat(iso = ISO.DATE_TIME)
private LocalDateTime created;
@DateTimeFormat(iso = ISO.DATE_TIME)
private LocalDateTime modified;
+ private List markers;
+
public NewsRequestTo(
@JsonProperty("userId") Long userId,
@JsonProperty("title") String title,
@JsonProperty("content") String content,
- @JsonProperty("markers") List markers,
@JsonProperty("created") LocalDateTime created,
- @JsonProperty("modified") LocalDateTime modified) {
+ @JsonProperty("modified") LocalDateTime modified,
+ @JsonProperty("markers") List markers) {
this.userId = userId;
this.title = title;
this.content = content;
- this.markers = markers;
this.created = created;
this.modified = modified;
+ this.markers = markers;
}
public Long getUserId() {
@@ -58,10 +58,6 @@ public String getContent() {
return content;
}
- public List getMarkers() {
- return markers;
- }
-
public LocalDateTime getCreated() {
return created;
}
@@ -69,4 +65,8 @@ public LocalDateTime getCreated() {
public LocalDateTime getModified() {
return modified;
}
+
+ public List getMarkers() {
+ return markers;
+ }
}
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/NewsResponseTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsResponseTo.java
similarity index 96%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/NewsResponseTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsResponseTo.java
index ccfd206d7..f154ff0a1 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/NewsResponseTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/NewsResponseTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import java.time.LocalDateTime;
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/UserRequestTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserRequestTo.java
similarity index 66%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/UserRequestTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserRequestTo.java
index 664fdd348..8cd480a87 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/UserRequestTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserRequestTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -18,22 +18,22 @@ public class UserRequestTo {
@NotBlank
@Size(min = 2, max = 64)
- private final String firstName;
+ private final String firstname;
@NotBlank
@Size(min = 2, max = 64)
- private final String lastName;
+ private final String lastname;
@JsonCreator
public UserRequestTo(
@JsonProperty("login") String login,
@JsonProperty("password") String password,
- @JsonProperty("firstname") String firstName,
- @JsonProperty("lastname") String lastName) {
+ @JsonProperty("firstname") String firstname,
+ @JsonProperty("lastname") String lastname) {
this.login = login;
this.password = password;
- this.firstName = firstName;
- this.lastName = lastName;
+ this.firstname = firstname;
+ this.lastname = lastname;
}
public String getLogin() {
@@ -44,11 +44,11 @@ public String getPassword() {
return password;
}
- public String getFirstName() {
- return firstName;
+ public String getFirstname() {
+ return firstname;
}
- public String getLastName() {
- return lastName;
+ public String getLastname() {
+ return lastname;
}
}
\ No newline at end of file
diff --git a/351004/Halukha/src/main/java/com/example/lab/dto/UserResponseTo.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserResponseTo.java
similarity index 63%
rename from 351004/Halukha/src/main/java/com/example/lab/dto/UserResponseTo.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserResponseTo.java
index b6673e7df..f95282bca 100644
--- a/351004/Halukha/src/main/java/com/example/lab/dto/UserResponseTo.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/dto/UserResponseTo.java
@@ -1,4 +1,4 @@
-package com.example.lab.dto;
+package com.example.lab.publisher.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -8,21 +8,21 @@ public class UserResponseTo {
private Long id;
private String login;
private String password;
- private String firstName;
- private String lastName;
+ private String firstname;
+ private String lastname;
@JsonCreator
public UserResponseTo(
@JsonProperty("id") Long id,
@JsonProperty("login") String login,
@JsonProperty("password") String password,
- @JsonProperty("firstname") String firstName,
- @JsonProperty("lastname") String lastName) {
+ @JsonProperty("firstname") String firstname,
+ @JsonProperty("lastname") String lastname) {
this.id = id;
this.login = login;
this.password = password;
- this.firstName = firstName;
- this.lastName = lastName;
+ this.firstname = firstname;
+ this.lastname = lastname;
}
public Long getId() {
@@ -37,11 +37,11 @@ public String getPassword() {
return password;
}
- public String getFirstName() {
- return firstName;
+ public String getFirstname() {
+ return firstname;
}
- public String getLastName() {
- return lastName;
+ public String getLastname() {
+ return lastname;
}
}
diff --git a/351004/Halukha/src/main/java/com/example/lab/exception/EntityNotFoundException.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/EntityNotFoundException.java
similarity index 86%
rename from 351004/Halukha/src/main/java/com/example/lab/exception/EntityNotFoundException.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/EntityNotFoundException.java
index d33557ae5..7207a03ab 100644
--- a/351004/Halukha/src/main/java/com/example/lab/exception/EntityNotFoundException.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/EntityNotFoundException.java
@@ -1,4 +1,4 @@
-package com.example.lab.exception;
+package com.example.lab.publisher.exception;
public class EntityNotFoundException extends RuntimeException {
private final int errorCode;
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/GlobalExceptionHandler.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/GlobalExceptionHandler.java
new file mode 100644
index 000000000..cf6112837
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/exception/GlobalExceptionHandler.java
@@ -0,0 +1,74 @@
+package com.example.lab.publisher.exception;
+
+import java.nio.file.AccessDeniedException;
+
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(EntityNotFoundException.class)
+ public ResponseEntity handleEntityNotFound(EntityNotFoundException ex) {
+ ErrorResponse error = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
+ }
+
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ public ResponseEntity handleDuplicate(DataIntegrityViolationException ex) {
+ ErrorResponse error = new ErrorResponse(40301, "Duplicate entry");
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidation(MethodArgumentNotValidException ex) {
+ String msg = ex.getBindingResult().getFieldErrors().stream()
+ .map(e -> e.getField() + ": " + e.getDefaultMessage())
+ .findFirst()
+ .orElse("Validation failed");
+ ErrorResponse error = new ErrorResponse(40001, msg);
+ return ResponseEntity.badRequest().body(error);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleGeneric(Exception ex) {
+ ErrorResponse error = new ErrorResponse(50001, "Internal server error");
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
+ }
+
+ @ExceptionHandler(AuthenticationException.class)
+ public ResponseEntity handleAuthenticationException(AuthenticationException e) {
+ return ResponseEntity
+ .status(HttpStatus.UNAUTHORIZED)
+ .body(new ErrorResponse(40101, "Invalid login or password"));
+ }
+
+ @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
+ public ResponseEntity handleAccessDenied(AccessDeniedException ex) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(new ErrorResponse(40301, "У вас нет прав на это действие"));
+ }
+
+ public static class ErrorResponse {
+ private final int errorCode;
+ private final String errorMessage;
+
+ public ErrorResponse(int errorCode, String errorMessage) {
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ }
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaMessage.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaMessage.java
new file mode 100644
index 000000000..8388577f5
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaMessage.java
@@ -0,0 +1,37 @@
+package com.example.lab.publisher.kafka;
+
+import java.time.LocalDateTime;
+
+public class KafkaMessage {
+ private String eventType; // CREATE, UPDATE, DELETE
+ private String entityType; // User, News, Marker
+ private T data;
+ private LocalDateTime timestamp;
+ private String operationId;
+
+ public KafkaMessage() {}
+
+ public KafkaMessage(String eventType, String entityType, T data) {
+ this.eventType = eventType;
+ this.entityType = entityType;
+ this.data = data;
+ this.timestamp = LocalDateTime.now();
+ this.operationId = java.util.UUID.randomUUID().toString();
+ }
+
+ // Getters and Setters
+ public String getEventType() { return eventType; }
+ public void setEventType(String eventType) { this.eventType = eventType; }
+
+ public String getEntityType() { return entityType; }
+ public void setEntityType(String entityType) { this.entityType = entityType; }
+
+ public T getData() { return data; }
+ public void setData(T data) { this.data = data; }
+
+ public LocalDateTime getTimestamp() { return timestamp; }
+ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
+
+ public String getOperationId() { return operationId; }
+ public void setOperationId(String operationId) { this.operationId = operationId; }
+}
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaProducerService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaProducerService.java
new file mode 100644
index 000000000..f64222e3c
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/kafka/KafkaProducerService.java
@@ -0,0 +1,126 @@
+package com.example.lab.publisher.kafka;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.support.SendResult;
+import org.springframework.stereotype.Service;
+
+import com.example.lab.publisher.dto.MarkerResponseTo;
+import com.example.lab.publisher.dto.NewsResponseTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.dto.kafka.KafkaMessage;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import java.util.concurrent.CompletableFuture;
+
+@Service
+public class KafkaProducerService {
+
+ private static final Logger logger = LoggerFactory.getLogger(KafkaProducerService.class);
+
+ @Autowired
+ private KafkaTemplate kafkaTemplate;
+
+ @Value("${app.kafka.topics.user:user-topic}")
+ private String userTopic;
+
+ @Value("${app.kafka.topics.news:news-topic}")
+ private String newsTopic;
+
+ @Value("${app.kafka.topics.marker:marker-topic}")
+ private String markerTopic;
+
+ private final ObjectMapper objectMapper = new ObjectMapper()
+ .registerModule(new JavaTimeModule());
+
+ // ==================== USER EVENTS ====================
+
+ public void sendUserCreated(UserResponseTo user) {
+ sendMessage(userTopic, "CREATE", "User", user);
+ }
+
+ public void sendUserUpdated(UserResponseTo user) {
+ sendMessage(userTopic, "UPDATE", "User", user);
+ }
+
+ public void sendUserDeleted(Long userId) {
+ UserResponseTo deletedUser = new UserResponseTo();
+ deletedUser.setId(userId);
+ sendMessage(userTopic, "DELETE", "User", deletedUser);
+ }
+
+ // ==================== NEWS EVENTS ====================
+
+ public void sendNewsCreated(NewsResponseTo news) {
+ sendMessage(newsTopic, "CREATE", "News", news);
+ }
+
+ public void sendNewsUpdated(NewsResponseTo news) {
+ sendMessage(newsTopic, "UPDATE", "News", news);
+ }
+
+ public void sendNewsDeleted(Long newsId) {
+ NewsResponseTo deletedNews = new NewsResponseTo();
+ deletedNews.setId(newsId);
+ sendMessage(newsTopic, "DELETE", "News", deletedNews);
+ }
+
+ // ==================== MARKER EVENTS ====================
+
+ public void sendMarkerCreated(MarkerResponseTo marker) {
+ sendMessage(markerTopic, "CREATE", "Marker", marker);
+ }
+
+ public void sendMarkerUpdated(MarkerResponseTo marker) {
+ sendMessage(markerTopic, "UPDATE", "Marker", marker);
+ }
+
+ public void sendMarkerDeleted(Long markerId) {
+ MarkerResponseTo deletedMarker = new MarkerResponseTo();
+ deletedMarker.setId(markerId);
+ sendMessage(markerTopic, "DELETE", "Marker", deletedMarker);
+ }
+
+ // ==================== PRIVATE METHODS ====================
+
+ private void sendMessage(String topic, String eventType, String entityType, T data) {
+ try {
+ KafkaMessage message = new KafkaMessage<>(eventType, entityType, data);
+ String jsonMessage = objectMapper.writeValueAsString(message);
+ String key = entityType + "_" + getEntityId(data);
+
+ CompletableFuture> future =
+ kafkaTemplate.send(topic, key, jsonMessage);
+
+ future.whenComplete((result, ex) -> {
+ if (ex == null) {
+ logger.info("Sent message to topic {}: event={}, entity={}, id={}, offset={}",
+ topic, eventType, entityType, getEntityId(data),
+ result.getRecordMetadata().offset());
+ } else {
+ logger.error("Failed to send message to topic {}: event={}, entity={}, id={}",
+ topic, eventType, entityType, getEntityId(data), ex);
+ }
+ });
+
+ } catch (JsonProcessingException e) {
+ logger.error("Failed to serialize message for topic: {}", topic, e);
+ }
+ }
+
+ private Object getEntityId(T data) {
+ if (data instanceof UserResponseTo) {
+ return ((UserResponseTo) data).getId();
+ } else if (data instanceof NewsResponseTo) {
+ return ((NewsResponseTo) data).getId();
+ } else if (data instanceof MarkerResponseTo) {
+ return ((MarkerResponseTo) data).getId();
+ }
+ return "unknown";
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/src/main/java/com/example/lab/mapper/MarkerMapper.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/MarkerMapper.java
similarity index 74%
rename from 351004/Halukha/src/main/java/com/example/lab/mapper/MarkerMapper.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/MarkerMapper.java
index 087293397..316bd249c 100644
--- a/351004/Halukha/src/main/java/com/example/lab/mapper/MarkerMapper.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/MarkerMapper.java
@@ -1,13 +1,13 @@
-package com.example.lab.mapper;
+package com.example.lab.publisher.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
-import com.example.lab.dto.MarkerRequestTo;
-import com.example.lab.dto.MarkerResponseTo;
-import com.example.lab.model.Marker;
+import com.example.lab.publisher.dto.MarkerRequestTo;
+import com.example.lab.publisher.dto.MarkerResponseTo;
+import com.example.lab.publisher.model.Marker;
@Mapper
public interface MarkerMapper {
diff --git a/351004/Halukha/src/main/java/com/example/lab/mapper/NewsMapper.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/NewsMapper.java
similarity index 82%
rename from 351004/Halukha/src/main/java/com/example/lab/mapper/NewsMapper.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/NewsMapper.java
index 1264ab64f..7be54fa3b 100644
--- a/351004/Halukha/src/main/java/com/example/lab/mapper/NewsMapper.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/NewsMapper.java
@@ -1,13 +1,13 @@
-package com.example.lab.mapper;
+package com.example.lab.publisher.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
-import com.example.lab.dto.NewsRequestTo;
-import com.example.lab.dto.NewsResponseTo;
-import com.example.lab.model.News;
+import com.example.lab.publisher.dto.NewsRequestTo;
+import com.example.lab.publisher.dto.NewsResponseTo;
+import com.example.lab.publisher.model.News;
@Mapper
public interface NewsMapper {
@@ -25,9 +25,9 @@ public interface NewsMapper {
@Mapping(target = "userId", source = "dto.userId"),
@Mapping(target = "title", source = "dto.title"),
@Mapping(target = "content", source = "dto.content"),
- @Mapping(target = "markers", ignore = true),
@Mapping(target = "created", source = "dto.created"),
@Mapping(target = "modified", source = "dto.modified"),
+ @Mapping(target = "markers", ignore = true),
})
News updateEntity(NewsRequestTo dto, News existing);
}
diff --git a/351004/Halukha/src/main/java/com/example/lab/mapper/UserMapper.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/UserMapper.java
similarity index 66%
rename from 351004/Halukha/src/main/java/com/example/lab/mapper/UserMapper.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/UserMapper.java
index fe218483f..6f1884157 100644
--- a/351004/Halukha/src/main/java/com/example/lab/mapper/UserMapper.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/mapper/UserMapper.java
@@ -1,13 +1,13 @@
-package com.example.lab.mapper;
+package com.example.lab.publisher.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
-import com.example.lab.dto.UserRequestTo;
-import com.example.lab.dto.UserResponseTo;
-import com.example.lab.model.User;
+import com.example.lab.publisher.dto.UserRequestTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.model.User;
@Mapper
public interface UserMapper {
@@ -22,8 +22,8 @@ public interface UserMapper {
@Mappings({
@Mapping(target = "login", source = "dto.login"),
@Mapping(target = "password", source = "dto.password"),
- @Mapping(target = "firstName", source = "dto.firstName"),
- @Mapping(target = "lastName", source = "dto.lastName"),
+ @Mapping(target = "firstname", source = "dto.firstname"),
+ @Mapping(target = "lastname", source = "dto.lastname"),
@Mapping(target = "id", ignore = true)
})
User updateEntity(UserRequestTo dto, User existing);
diff --git a/351004/Halukha/src/main/java/com/example/lab/model/Marker.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/Marker.java
similarity index 95%
rename from 351004/Halukha/src/main/java/com/example/lab/model/Marker.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/Marker.java
index 6e7b5383d..a5f48a8cf 100644
--- a/351004/Halukha/src/main/java/com/example/lab/model/Marker.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/Marker.java
@@ -1,4 +1,4 @@
-package com.example.lab.model;
+package com.example.lab.publisher.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
diff --git a/351004/Halukha/src/main/java/com/example/lab/model/News.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/News.java
similarity index 91%
rename from 351004/Halukha/src/main/java/com/example/lab/model/News.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/News.java
index e994a2d17..5e56cff83 100644
--- a/351004/Halukha/src/main/java/com/example/lab/model/News.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/News.java
@@ -1,4 +1,4 @@
-package com.example.lab.model;
+package com.example.lab.publisher.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -32,7 +32,7 @@ public class News {
@NotBlank
@Size(min = 2, max = 64)
- @Column(name = "title", nullable = false, unique = true)
+ @Column(name = "title", unique = true)
private String title;
@NotBlank
@@ -40,9 +40,6 @@ public class News {
@Column(name = "content")
private String content;
- @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
- private List markers = new ArrayList<>();
-
@DateTimeFormat(iso = ISO.DATE_TIME)
@Column(name = "created")
private LocalDateTime created;
@@ -51,17 +48,22 @@ public class News {
@Column(name = "modified")
private LocalDateTime modified;
+ // Выражение связи many-to-many с таблицей tbl_markers
+ @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
+ private List markers = new ArrayList<>();
+
public News() {
}
- public News(Long id, Long userId, String title, String content, List markers, LocalDateTime created, LocalDateTime modified) {
+ public News(Long id, Long userId, String title, String content, LocalDateTime created, LocalDateTime modified,
+ List markers) {
this.id = id;
this.userId = userId;
this.title = title;
this.content = content;
- this.markers = markers;
this.created = created;
this.modified = modified;
+ this.markers = markers;
}
public Long getId() {
diff --git a/351004/Halukha/src/main/java/com/example/lab/model/User.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/User.java
similarity index 57%
rename from 351004/Halukha/src/main/java/com/example/lab/model/User.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/User.java
index 35e863c2a..171810e95 100644
--- a/351004/Halukha/src/main/java/com/example/lab/model/User.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/model/User.java
@@ -1,7 +1,9 @@
-package com.example.lab.model;
+package com.example.lab.publisher.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@@ -9,6 +11,8 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
+import com.example.lab.publisher.security.Role;
+
@Entity
@Table(name = "tbl_user")
public class User {
@@ -20,7 +24,7 @@ public class User {
@NotBlank
@Size(min = 2, max = 64)
- @Column(name = "login", nullable = false, unique = true)
+ @Column(name = "login", unique = true)
private String login;
@NotBlank
@@ -31,21 +35,25 @@ public class User {
@NotBlank
@Size(min = 2, max = 64)
@Column(name = "firstname")
- private String firstName;
+ private String firstname;
@NotBlank
@Size(min = 2, max = 64)
@Column(name = "lastname")
- private String lastName;
+ private String lastname;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "role")
+ private Role role = Role.CUSTOMER;
public User() {
}
- public User(String login, String password, String firstName, String lastName) {
+ public User(String login, String password, String firstname, String lastname) {
this.login = login;
this.password = password;
- this.firstName = firstName;
- this.lastName = lastName;
+ this.firstname = firstname;
+ this.lastname = lastname;
}
public Long getId() {
@@ -60,12 +68,16 @@ public String getPassword() {
return password;
}
- public String getFirstName() {
- return firstName;
+ public String getFirstname() {
+ return firstname;
+ }
+
+ public String getLastname() {
+ return lastname;
}
- public String getLastName() {
- return lastName;
+ public Role getRole() {
+ return role == null ? Role.CUSTOMER : role;
}
public void setId(Long id) {
@@ -80,11 +92,15 @@ public void setPassword(String password) {
this.password = password;
}
- public void setFirstName(String firstName) {
- this.firstName = firstName;
+ public void setFirstname(String firstname) {
+ this.firstname = firstname;
+ }
+
+ public void setLastname(String lastname) {
+ this.lastname = lastname;
}
- public void setLastName(String lastName) {
- this.lastName = lastName;
+ public void setRole(Role role) {
+ this.role = role == null ? Role.CUSTOMER : role;
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/src/main/java/com/example/lab/repository/MarkerRepository.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/MarkerRepository.java
similarity index 59%
rename from 351004/Halukha/src/main/java/com/example/lab/repository/MarkerRepository.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/MarkerRepository.java
index 3f25a72a8..1d08d31df 100644
--- a/351004/Halukha/src/main/java/com/example/lab/repository/MarkerRepository.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/MarkerRepository.java
@@ -1,8 +1,8 @@
-package com.example.lab.repository;
+package com.example.lab.publisher.repository;
import org.springframework.data.jpa.repository.JpaRepository;
-import com.example.lab.model.Marker;
+import com.example.lab.publisher.model.Marker;
public interface MarkerRepository extends JpaRepository {
}
diff --git a/351004/Halukha/src/main/java/com/example/lab/repository/NewsRepository.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/NewsRepository.java
similarity index 59%
rename from 351004/Halukha/src/main/java/com/example/lab/repository/NewsRepository.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/NewsRepository.java
index 3401ea168..3a0d45fa7 100644
--- a/351004/Halukha/src/main/java/com/example/lab/repository/NewsRepository.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/NewsRepository.java
@@ -1,8 +1,8 @@
-package com.example.lab.repository;
+package com.example.lab.publisher.repository;
import org.springframework.data.jpa.repository.JpaRepository;
-import com.example.lab.model.News;
+import com.example.lab.publisher.model.News;
public interface NewsRepository extends JpaRepository {
}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/UserRepository.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/UserRepository.java
new file mode 100644
index 000000000..8bef695ad
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/repository/UserRepository.java
@@ -0,0 +1,11 @@
+package com.example.lab.publisher.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.example.lab.publisher.model.User;
+
+public interface UserRepository extends JpaRepository {
+ Optional findByLogin(String login);
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthController.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthController.java
new file mode 100644
index 000000000..33391f704
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthController.java
@@ -0,0 +1,81 @@
+package com.example.lab.publisher.security;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.lab.publisher.exception.GlobalExceptionHandler.ErrorResponse;
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.UserRepository;
+import com.example.lab.publisher.security.AuthDtos.CurrentUserResponse;
+import com.example.lab.publisher.security.AuthDtos.LoginRequest;
+import com.example.lab.publisher.security.AuthDtos.RegisterRequest;
+import com.example.lab.publisher.security.AuthDtos.TokenResponse;
+
+import jakarta.validation.Valid;
+
+@RestController
+@RequestMapping("/api/v2.0")
+public class AuthController {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final AuthenticationManager authenticationManager;
+ private final JwtService jwtService;
+
+ public AuthController(UserRepository userRepository, PasswordEncoder passwordEncoder,
+ AuthenticationManager authenticationManager, JwtService jwtService) {
+ this.userRepository = userRepository;
+ this.passwordEncoder = passwordEncoder;
+ this.authenticationManager = authenticationManager;
+ this.jwtService = jwtService;
+ }
+
+ @PostMapping("/users")
+ public ResponseEntity> register(@Valid @RequestBody RegisterRequest req) {
+ if (userRepository.findByLogin(req.getLogin()).isPresent()) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(40301, "Duplicate entry"));
+ }
+
+ User user = new User();
+ user.setLogin(req.getLogin());
+ user.setPassword(passwordEncoder.encode(req.getPassword()));
+ user.setFirstname(req.getFirstname());
+ user.setLastname(req.getLastname());
+ user.setRole(req.getRole());
+
+ User saved = userRepository.save(user);
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(new CurrentUserResponse(saved.getId(), saved.getLogin(), saved.getFirstname(),
+ saved.getLastname(), saved.getRole()));
+ }
+
+ @PostMapping("/login")
+ public ResponseEntity login(@Valid @RequestBody LoginRequest req) {
+ Authentication auth = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(req.getLogin(), req.getPassword()));
+ SecurityContextHolder.getContext().setAuthentication(auth);
+
+ User user = userRepository.findByLogin(req.getLogin()).orElseThrow();
+ String token = jwtService.issueToken(user.getLogin(), user.getRole() == null ? Role.CUSTOMER : user.getRole());
+ return ResponseEntity.ok(new TokenResponse(token));
+ }
+
+ @GetMapping("/users/me")
+ public ResponseEntity me() {
+ String login = SecurityContextHolder.getContext().getAuthentication().getName();
+ User user = userRepository.findByLogin(login).orElseThrow();
+ return ResponseEntity
+ .ok(new CurrentUserResponse(user.getId(), user.getLogin(), user.getFirstname(), user.getLastname(),
+ user.getRole() == null ? Role.CUSTOMER : user.getRole()));
+ }
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthDtos.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthDtos.java
new file mode 100644
index 000000000..a89e26b48
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/AuthDtos.java
@@ -0,0 +1,145 @@
+package com.example.lab.publisher.security;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+public final class AuthDtos {
+ private AuthDtos() {
+ }
+
+ public static class RegisterRequest {
+ @NotBlank
+ @Size(min = 2, max = 64)
+ private final String login;
+
+ @NotBlank
+ @Size(min = 8, max = 128)
+ private final String password;
+
+ @NotBlank
+ @Size(min = 2, max = 64)
+ private final String firstname;
+
+ @NotBlank
+ @Size(min = 2, max = 64)
+ private final String lastname;
+
+ private final Role role;
+
+ @JsonCreator
+ public RegisterRequest(
+ @JsonProperty("login") String login,
+ @JsonProperty("password") String password,
+ @JsonProperty("firstname") String firstname,
+ @JsonProperty("lastname") String lastname,
+ @JsonProperty("role") Role role) {
+ this.login = login;
+ this.password = password;
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.role = role == null ? Role.CUSTOMER : role;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getFirstname() {
+ return firstname;
+ }
+
+ public String getLastname() {
+ return lastname;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+ }
+
+ public static class LoginRequest {
+ @NotBlank
+ private final String login;
+
+ @NotBlank
+ private final String password;
+
+ @JsonCreator
+ public LoginRequest(
+ @JsonProperty("login") String login,
+ @JsonProperty("password") String password) {
+ this.login = login;
+ this.password = password;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+ }
+
+ public static class TokenResponse {
+ private final String access_token;
+ private final String token_type;
+
+ public TokenResponse(String accessToken) {
+ this.access_token = accessToken;
+ this.token_type = "Bearer";
+ }
+
+ public String getAccess_token() {
+ return access_token;
+ }
+
+ public String getToken_type() {
+ return token_type;
+ }
+ }
+
+ public static class CurrentUserResponse {
+ private final Long id;
+ private final String login;
+ private final String firstname;
+ private final String lastname;
+ private final Role role;
+
+ public CurrentUserResponse(Long id, String login, String firstname, String lastname, Role role) {
+ this.id = id;
+ this.login = login;
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.role = role;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getFirstname() {
+ return firstname;
+ }
+
+ public String getLastname() {
+ return lastname;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtAuthFilter.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtAuthFilter.java
new file mode 100644
index 000000000..225aa396a
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtAuthFilter.java
@@ -0,0 +1,67 @@
+package com.example.lab.publisher.security;
+
+import java.io.IOException;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@Component
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtService jwtService;
+ private final UserDetailsService userDetailsService;
+
+ public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
+ this.jwtService = jwtService;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
+ if (auth == null || !auth.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String token = auth.substring("Bearer ".length()).trim();
+ if (token.isEmpty() || SecurityContextHolder.getContext().getAuthentication() != null) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ try {
+ Jws jws = jwtService.parse(token);
+ String login = jws.getPayload().getSubject();
+ if (login == null || login.isBlank()) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ UserDetails userDetails = userDetailsService.loadUserByUsername(login);
+ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.getAuthorities());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } catch (Exception e) {
+ // invalid token -> leave unauthenticated, entrypoint will handle if endpoint requires auth
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtService.java
new file mode 100644
index 000000000..9b858142d
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/JwtService.java
@@ -0,0 +1,50 @@
+package com.example.lab.publisher.security;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+
+import javax.crypto.SecretKey;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+
+@Service
+public class JwtService {
+
+ private final SecretKey key;
+ private final long ttlSeconds;
+
+ public JwtService(
+ @Value("${app.jwt.secret}") String secret,
+ @Value("${app.jwt.ttlSeconds:3600}") long ttlSeconds) {
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ this.ttlSeconds = ttlSeconds;
+ }
+
+ public String issueToken(String login, Role role) {
+ Instant now = Instant.now();
+ Instant exp = now.plusSeconds(ttlSeconds);
+
+ return Jwts.builder()
+ .subject(login)
+ .issuedAt(Date.from(now))
+ .expiration(Date.from(exp))
+ .claim("role", role.name())
+ .signWith(key)
+ .compact();
+ }
+
+ public Jws parse(String token) {
+ return Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token);
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/OwnershipService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/OwnershipService.java
new file mode 100644
index 000000000..c1c4327a9
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/OwnershipService.java
@@ -0,0 +1,46 @@
+package com.example.lab.publisher.security;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+import com.example.lab.publisher.model.News;
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.NewsRepository;
+import com.example.lab.publisher.repository.UserRepository;
+
+@Service("ownership")
+public class OwnershipService {
+
+ private final UserRepository userRepository;
+ private final NewsRepository newsRepository;
+
+ public OwnershipService(UserRepository userRepository, NewsRepository newsRepository) {
+ this.userRepository = userRepository;
+ this.newsRepository = newsRepository;
+ }
+
+ public boolean canAccessUser(Long id) {
+ var auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null)
+ return false;
+
+ return userRepository.findByLogin(auth.getName())
+ .map(user -> user.getId().equals(id))
+ .orElse(false);
+ }
+
+ public boolean canModifyNews(Long newsId) {
+ String login = SecurityContextHolder.getContext().getAuthentication().getName();
+ User user = userRepository.findByLogin(login).orElse(null);
+ if (user == null || user.getId() == null) {
+ return false;
+ }
+ News news = newsRepository.findById(newsId).orElse(null);
+ return news != null && user.getId().equals(news.getUserId());
+ }
+
+ public Long currentUserId() {
+ String login = SecurityContextHolder.getContext().getAuthentication().getName();
+ return userRepository.findByLogin(login).map(User::getId).orElse(null);
+ }
+}
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/Role.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/Role.java
new file mode 100644
index 000000000..1e0f68962
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/Role.java
@@ -0,0 +1,7 @@
+package com.example.lab.publisher.security;
+
+public enum Role {
+ ADMIN,
+ CUSTOMER
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityConfig.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityConfig.java
new file mode 100644
index 000000000..ccfc52493
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityConfig.java
@@ -0,0 +1,115 @@
+package com.example.lab.publisher.security;
+
+import java.io.IOException;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpMethod;
+
+@Configuration
+@EnableMethodSecurity
+public class SecurityConfig {
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
+ return new PasswordEncoder() {
+ @Override
+ public String encode(CharSequence rawPassword) {
+ return bcrypt.encode(rawPassword);
+ }
+
+ @Override
+ public boolean matches(CharSequence rawPassword, String encodedPassword) {
+ if (encodedPassword == null) {
+ return false;
+ }
+ // backward compatibility: allow plain-text passwords created via v1
+ if (!encodedPassword.startsWith("$2a$") && !encodedPassword.startsWith("$2b$") && !encodedPassword.startsWith("$2y$")) {
+ return rawPassword != null && encodedPassword.contentEquals(rawPassword);
+ }
+ return bcrypt.matches(rawPassword, encodedPassword);
+ }
+ };
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder encoder) {
+ DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+ provider.setUserDetailsService(userDetailsService);
+ provider.setPasswordEncoder(encoder);
+ return new ProviderManager(provider);
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter, ObjectMapper objectMapper)
+ throws Exception {
+
+ http.csrf(csrf -> csrf.disable());
+ http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ http.exceptionHandling(eh -> eh
+ .authenticationEntryPoint((request, response, authException) -> writeError(response, objectMapper, 40101,
+ "Unauthorized"))
+ .accessDeniedHandler(accessDeniedHandler(objectMapper)));
+
+ http.authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/v1.0/**").permitAll()
+ .requestMatchers(HttpMethod.POST, "/api/v2.0/login").permitAll()
+ .requestMatchers(HttpMethod.POST, "/api/v2.0/users").permitAll()
+ .requestMatchers("/api/v2.0/**").authenticated()
+ .anyRequest().permitAll());
+
+ http.httpBasic(basic -> basic.disable());
+ http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ private AccessDeniedHandler accessDeniedHandler(ObjectMapper objectMapper) {
+ return (request, response, accessDeniedException) -> writeError(response, objectMapper, 40301, "Forbidden");
+ }
+
+ private void writeError(HttpServletResponse response, ObjectMapper objectMapper, int code, String message)
+ throws IOException {
+ response.setStatus(code / 100);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ objectMapper.writeValue(response.getOutputStream(), new ErrorBody(code, message));
+ }
+
+ public static class ErrorBody {
+ private final int errorCode;
+ private final String errorMessage;
+
+ public ErrorBody(int errorCode, String errorMessage) {
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityUserDetailsService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityUserDetailsService.java
new file mode 100644
index 000000000..c12f49b6d
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/security/SecurityUserDetailsService.java
@@ -0,0 +1,36 @@
+package com.example.lab.publisher.security;
+
+import java.util.List;
+
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.UserRepository;
+
+@Service
+public class SecurityUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ public SecurityUserDetailsService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User user = userRepository.findByLogin(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+
+ String role = user.getRole() == null ? Role.CUSTOMER.name() : user.getRole().name();
+ return org.springframework.security.core.userdetails.User
+ .withUsername(user.getLogin())
+ .password(user.getPassword())
+ .authorities(List.of(new SimpleGrantedAuthority("ROLE_" + role)))
+ .build();
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/MarkerService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/MarkerService.java
new file mode 100644
index 000000000..d8b313464
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/MarkerService.java
@@ -0,0 +1,76 @@
+package com.example.lab.publisher.service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
+import org.springframework.stereotype.Service;
+
+import com.example.lab.publisher.dto.MarkerRequestTo;
+import com.example.lab.publisher.dto.MarkerResponseTo;
+import com.example.lab.publisher.exception.EntityNotFoundException;
+import com.example.lab.publisher.mapper.MarkerMapper;
+import com.example.lab.publisher.model.Marker;
+import com.example.lab.publisher.repository.MarkerRepository;
+
+@Service
+public class MarkerService {
+
+ private final MarkerRepository markerRepository;
+ private final MarkerMapper mapper = MarkerMapper.INSTANCE;
+
+ public MarkerService(MarkerRepository newsRepository) {
+ this.markerRepository = newsRepository;
+ }
+
+ @Cacheable(cacheNames = "markers", key = "'all'")
+ public List getAllMarker() {
+ return markerRepository.findAll().stream()
+ .map(mapper::toDto)
+ .collect(Collectors.toList());
+ }
+
+ @Cacheable(cacheNames = "markers", key = "#id")
+ public MarkerResponseTo getMarkerById(Long id) {
+ return markerRepository.findById(id)
+ .map(mapper::toDto)
+ .orElseThrow(() -> new EntityNotFoundException("Marker not found", 40401));
+ }
+
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "markers", key = "'all'")
+ })
+ public MarkerResponseTo createMarker(MarkerRequestTo request) {
+ Marker news = mapper.toEntity(request);
+ Marker saved = markerRepository.save(news);
+ return mapper.toDto(saved);
+ }
+
+ @Caching(put = {
+ @CachePut(cacheNames = "markers", key = "#id")
+ }, evict = {
+ @CacheEvict(cacheNames = "markers", key = "'all'")
+ })
+ public MarkerResponseTo updateMarker(Long id, MarkerRequestTo request) {
+ Marker existing = markerRepository.findById(id)
+ .orElseThrow(() -> new EntityNotFoundException("Marker not found", 40401));
+ Marker updated = mapper.updateEntity(request, existing);
+ updated.setId(id);
+ Marker saved = markerRepository.save(updated);
+ return mapper.toDto(saved);
+ }
+
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "markers", key = "#id"),
+ @CacheEvict(cacheNames = "markers", key = "'all'")
+ })
+ public void deleteMarker(Long id) {
+ if (!markerRepository.existsById(id)) {
+ throw new EntityNotFoundException("Marker not found", 40401);
+ }
+ markerRepository.deleteById(id);
+ }
+}
diff --git a/351004/Halukha/src/main/java/com/example/lab/service/NewsService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/NewsService.java
similarity index 62%
rename from 351004/Halukha/src/main/java/com/example/lab/service/NewsService.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/NewsService.java
index 7bd7e419a..9e19ba1c7 100644
--- a/351004/Halukha/src/main/java/com/example/lab/service/NewsService.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/NewsService.java
@@ -1,23 +1,27 @@
-package com.example.lab.service;
+package com.example.lab.publisher.service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
-import com.example.lab.dto.NewsRequestTo;
-import com.example.lab.dto.NewsResponseTo;
-import com.example.lab.dto.UserResponseTo;
-import com.example.lab.exception.EntityNotFoundException;
-import com.example.lab.mapper.NewsMapper;
-import com.example.lab.mapper.UserMapper;
-import com.example.lab.model.Marker;
-import com.example.lab.model.News;
-import com.example.lab.model.User;
-import com.example.lab.repository.MarkerRepository;
-import com.example.lab.repository.NewsRepository;
-import com.example.lab.repository.UserRepository;
+import com.example.lab.publisher.dto.NewsRequestTo;
+import com.example.lab.publisher.dto.NewsResponseTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.exception.EntityNotFoundException;
+import com.example.lab.publisher.mapper.NewsMapper;
+import com.example.lab.publisher.mapper.UserMapper;
+import com.example.lab.publisher.model.Marker;
+import com.example.lab.publisher.model.News;
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.MarkerRepository;
+import com.example.lab.publisher.repository.NewsRepository;
+import com.example.lab.publisher.repository.UserRepository;
@Service
public class NewsService {
@@ -35,18 +39,24 @@ public NewsService(MarkerRepository markerRepository, NewsRepository newsReposit
this.userRepository = userRepository;
}
+ @Cacheable(cacheNames = "news", key = "'all'")
public List getAllNews() {
return newsRepository.findAll().stream()
.map(mapper::toDto)
.collect(Collectors.toList());
}
+ @Cacheable(cacheNames = "news", key = "#id")
public NewsResponseTo getNewsById(Long id) {
return newsRepository.findById(id)
.map(mapper::toDto)
.orElseThrow(() -> new EntityNotFoundException("News not found", 40401));
}
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "news", key = "'all'"),
+ @CacheEvict(cacheNames = "markers", key = "'all'", condition = "#request.markers != null && !#request.markers.isEmpty()")
+ })
public NewsResponseTo createNews(NewsRequestTo request) {
if (!userRepository.existsById(request.getUserId())) {
throw new EntityNotFoundException("User not found", 40401);
@@ -60,6 +70,13 @@ public NewsResponseTo createNews(NewsRequestTo request) {
return mapper.toDto(saved);
}
+ @Caching(put = {
+ @CachePut(cacheNames = "news", key = "#id")
+ }, evict = {
+ @CacheEvict(cacheNames = "news", key = "'all'"),
+ @CacheEvict(cacheNames = "newsUser", key = "#id"),
+ @CacheEvict(cacheNames = "markers", key = "'all'", condition = "#request.markers != null && !#request.markers.isEmpty()")
+ })
public NewsResponseTo updateNews(Long id, NewsRequestTo request) {
News existing = newsRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("News not found", 40401));
@@ -85,6 +102,12 @@ private List resolveMarkers(List markerNames) {
}).collect(Collectors.toList());
}
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "news", key = "#id"),
+ @CacheEvict(cacheNames = "news", key = "'all'"),
+ @CacheEvict(cacheNames = "newsUser", key = "#id"),
+ @CacheEvict(cacheNames = "markers", key = "'all'")
+ })
public void deleteNews(Long id) {
if (!newsRepository.existsById(id)) {
throw new EntityNotFoundException("News not found", 40401);
@@ -97,6 +120,7 @@ public void deleteNews(Long id) {
newsRepository.deleteById(id);
}
+ @Cacheable(cacheNames = "newsUser", key = "#newsId")
public UserResponseTo getUserByNewsId(Long newsId) {
News news = newsRepository.findById(newsId)
.orElseThrow(() -> new EntityNotFoundException("News not found", 40401));
diff --git a/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/PostRedisCacheService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/PostRedisCacheService.java
new file mode 100644
index 000000000..d0140c2e2
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/PostRedisCacheService.java
@@ -0,0 +1,63 @@
+package com.example.lab.publisher.service;
+
+import java.time.Duration;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.RedisConnectionFailureException;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Service
+public class PostRedisCacheService {
+
+ private static final Duration POST_TTL = Duration.ofMinutes(5);
+
+ private final StringRedisTemplate redis;
+ private final ObjectMapper objectMapper;
+
+ public PostRedisCacheService(StringRedisTemplate redis, ObjectMapper objectMapper) {
+ this.redis = redis;
+ this.objectMapper = objectMapper;
+ }
+
+ private String keyById(Long id) {
+ return "posts:" + id;
+ }
+
+ public Object get(Long id) {
+ try {
+ String json = redis.opsForValue().get(keyById(id));
+ if (json == null) {
+ return null;
+ }
+ return objectMapper.readValue(json, Object.class);
+ } catch (DataAccessException e) {
+ return null;
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ }
+
+ public void put(Long id, Object value) {
+ try {
+ String json = objectMapper.writeValueAsString(value);
+ redis.opsForValue().set(keyById(id), json, POST_TTL);
+ } catch (DataAccessException e) {
+ // skip cache on redis issues
+ } catch (JsonProcessingException e) {
+ // skip cache on serialization issues
+ }
+ }
+
+ public void evict(Long id) {
+ try {
+ redis.delete(keyById(id));
+ } catch (DataAccessException e) {
+ // skip
+ }
+ }
+}
+
diff --git a/351004/Halukha/src/main/java/com/example/lab/service/UserService.java b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/UserService.java
similarity index 59%
rename from 351004/Halukha/src/main/java/com/example/lab/service/UserService.java
rename to 351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/UserService.java
index e6bc79526..52c2823c0 100644
--- a/351004/Halukha/src/main/java/com/example/lab/service/UserService.java
+++ b/351004/Halukha/publisher/src/main/java/com/example/lab/publisher/service/UserService.java
@@ -1,16 +1,20 @@
-package com.example.lab.service;
+package com.example.lab.publisher.service;
import java.util.List;
import java.util.stream.Collectors;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
-import com.example.lab.dto.UserRequestTo;
-import com.example.lab.dto.UserResponseTo;
-import com.example.lab.exception.EntityNotFoundException;
-import com.example.lab.mapper.UserMapper;
-import com.example.lab.model.User;
-import com.example.lab.repository.UserRepository;
+import com.example.lab.publisher.dto.UserRequestTo;
+import com.example.lab.publisher.dto.UserResponseTo;
+import com.example.lab.publisher.exception.EntityNotFoundException;
+import com.example.lab.publisher.mapper.UserMapper;
+import com.example.lab.publisher.model.User;
+import com.example.lab.publisher.repository.UserRepository;
@Service
public class UserService {
@@ -22,24 +26,34 @@ public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
+ @Cacheable(cacheNames = "users", key = "'all'")
public List getAllUsers() {
return userRepository.findAll().stream()
.map(mapper::toDto)
.collect(Collectors.toList());
}
+ @Cacheable(cacheNames = "users", key = "#id")
public UserResponseTo getUserById(Long id) {
return userRepository.findById(id)
.map(mapper::toDto)
.orElseThrow(() -> new EntityNotFoundException("User not found", 40401));
}
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "users", key = "'all'")
+ })
public UserResponseTo createUser(UserRequestTo request) {
User user = mapper.toEntity(request);
User saved = userRepository.save(user);
return mapper.toDto(saved);
}
+ @Caching(put = {
+ @CachePut(cacheNames = "users", key = "#id")
+ }, evict = {
+ @CacheEvict(cacheNames = "users", key = "'all'")
+ })
public UserResponseTo updateUser(Long id, UserRequestTo request) {
User existing = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found", 40401));
@@ -49,10 +63,14 @@ public UserResponseTo updateUser(Long id, UserRequestTo request) {
return mapper.toDto(saved);
}
+ @Caching(evict = {
+ @CacheEvict(cacheNames = "users", key = "#id"),
+ @CacheEvict(cacheNames = "users", key = "'all'")
+ })
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new EntityNotFoundException("User not found", 40401);
}
userRepository.deleteById(id);
}
-}
\ No newline at end of file
+}
diff --git a/351004/Halukha/publisher/src/main/resources/application.yaml b/351004/Halukha/publisher/src/main/resources/application.yaml
new file mode 100644
index 000000000..ca7419e9b
--- /dev/null
+++ b/351004/Halukha/publisher/src/main/resources/application.yaml
@@ -0,0 +1,50 @@
+server:
+ port: 24110
+
+spring:
+ application:
+ name: publisher-service
+
+ datasource:
+ url: jdbc:postgresql://localhost:5432/distcomp
+ username: postgres
+ password: postgres
+ driver-class-name: org.postgresql.Driver
+
+ data:
+ redis:
+ host: localhost
+ port: 6379
+
+ kafka:
+ bootstrap-servers: localhost:9092
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ properties:
+ spring.json.type.mapping: message:com.example.lab.publisher.dto.KafkaMessage
+ consumer:
+ group-id: publisher-group
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+ properties:
+ spring.json.trusted.packages: "*"
+ spring.json.type.mapping: message:com.example.lab.publisher.dto.KafkaMessage
+ template:
+ default-topic: news-topic
+
+app:
+ jwt:
+ secret: "change-me-change-me-change-me-change-me"
+ ttlSeconds: 3600
+
+ jpa:
+ database: postgresql
+ database-platform: org.hibernate.dialect.PostgreSQLDialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/LabApplicationTests.java b/351004/Halukha/publisher/src/test/java/com/example/lab/LabApplicationTests.java
new file mode 100644
index 000000000..74eb19fa9
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/LabApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.lab;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class LabApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/test/BaseIntegrationTest.java b/351004/Halukha/publisher/src/test/java/com/example/lab/test/BaseIntegrationTest.java
new file mode 100644
index 000000000..430104d27
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/test/BaseIntegrationTest.java
@@ -0,0 +1,29 @@
+package com.example.lab.test;
+
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.BeforeAll;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public abstract class BaseIntegrationTest {
+
+ @LocalServerPort
+ protected int port;
+
+ @BeforeAll
+ static void setUpRestAssured() {
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ }
+
+ @DynamicPropertySource
+ static void configureBaseUrl(DynamicPropertyRegistry registry) {
+ registry.add("server.port", () -> 0);
+ }
+
+ protected String getBaseUrl() {
+ return "http://localhost:" + port;
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java b/351004/Halukha/publisher/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java
new file mode 100644
index 000000000..1e34ed716
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/test/MarkerControllerIntegrationTest.java
@@ -0,0 +1,149 @@
+package com.example.lab.test;
+
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.*;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class MarkerControllerIntegrationTest extends BaseIntegrationTest {
+
+ private static Long markerId;
+
+ @Test
+ @Order(1)
+ @DisplayName("STEP 1: CREATE MARKER - Should return 201 Created")
+ void createMarker() {
+ String payload = """
+ {
+ "name": "Test Marker"
+ }
+ """;
+
+ markerId = given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .post("/api/v1.0/markers")
+ .then()
+ .statusCode(HttpStatus.CREATED.value())
+ .contentType(ContentType.JSON)
+ .body("id", notNullValue())
+ .body("name", equalTo("Test Marker"))
+ .extract()
+ .jsonPath()
+ .getLong("id");
+
+ Assertions.assertNotNull(markerId);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("STEP 2: GET MARKER BY ID - Should return 200 OK")
+ void getMarkerById() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/markers/{id}", markerId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(markerId.intValue()))
+ .body("name", equalTo("Test Marker"));
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("STEP 3: UPDATE MARKER - Should return 200 OK")
+ void updateMarker() {
+ String payload = """
+ {
+ "id": %d,
+ "name": "Updated Marker"
+ }
+ """.formatted(markerId);
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .put("/api/v1.0/markers/{id}", markerId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(markerId.intValue()))
+ .body("name", equalTo("Updated Marker"));
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("STEP 4: DELETE MARKER - Should return 204 No Content")
+ void deleteMarker() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .delete("/api/v1.0/markers/{id}", markerId)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT.value());
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("STEP 5: GET DELETED MARKER - Should return 404 Not Found")
+ void getDeletedMarker() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/markers/{id}", markerId)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .contentType(ContentType.JSON)
+ .body("errorCode", equalTo(40401));
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("NEGATIVE: CREATE MARKER WITH INVALID DATA - Should return 400 Bad Request")
+ void createMarkerWithInvalidData() {
+ String payload = """
+ {
+ "name": ""
+ }
+ """;
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .post("/api/v1.0/markers")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST.value());
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("NEGATIVE: UPDATE NON-EXISTENT MARKER - Should return 404 Not Found")
+ void updateNonExistentMarker() {
+ String payload = """
+ {
+ "id": 999999,
+ "name": "Non-existent"
+ }
+ """;
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .put("/api/v1.0/markers/{id}", 999999)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .body("errorCode", equalTo(40401));
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java b/351004/Halukha/publisher/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java
new file mode 100644
index 000000000..3b7ae5d1d
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/test/NewsControllerIntegrationTest.java
@@ -0,0 +1,176 @@
+package com.example.lab.test;
+
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.*;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class NewsControllerIntegrationTest extends BaseIntegrationTest {
+
+ private static Long newsId;
+ private static Long userId;
+
+ @BeforeEach
+ void createTestUser() {
+ // Создаем пользователя для новости
+ String userPayload = """
+ {
+ "login": "news_author",
+ "password": "password123",
+ "firstname": "Author",
+ "lastname": "Name"
+ }
+ """;
+
+ userId = given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(userPayload)
+ .when()
+ .post("/api/v1.0/users")
+ .then()
+ .statusCode(HttpStatus.CREATED.value())
+ .extract()
+ .jsonPath()
+ .getLong("id");
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("STEP 1: CREATE NEWS - Should return 201 Created")
+ void createNews() {
+ String payload = """
+ {
+ "title": "Breaking News",
+ "content": "This is important news!",
+ "userId": %d
+ }
+ """.formatted(userId);
+
+ newsId = given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .post("/api/v1.0/news")
+ .then()
+ .statusCode(HttpStatus.CREATED.value())
+ .contentType(ContentType.JSON)
+ .body("id", notNullValue())
+ .body("title", equalTo("Breaking News"))
+ .body("content", equalTo("This is important news!"))
+ .extract()
+ .jsonPath()
+ .getLong("id");
+
+ Assertions.assertNotNull(newsId);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("STEP 2: GET NEWS BY ID - Should return 200 OK")
+ void getNewsById() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/news/{id}", newsId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(newsId.intValue()))
+ .body("title", equalTo("Breaking News"))
+ .body("content", equalTo("This is important news!"));
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("STEP 4: UPDATE NEWS - Should return 200 OK")
+ void updateNews() {
+ String payload = """
+ {
+ "id": %d,
+ "title": "Updated News",
+ "content": "Updated content!",
+ "userId": %d
+ }
+ """.formatted(newsId, userId);
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .put("/api/v1.0/news/{id}", newsId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(newsId.intValue()))
+ .body("title", equalTo("Updated News"))
+ .body("content", equalTo("Updated content!"));
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("STEP 5: DELETE NEWS - Should return 204 No Content")
+ void deleteNews() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .delete("/api/v1.0/news/{id}", newsId)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT.value());
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("STEP 6: GET DELETED NEWS - Should return 404 Not Found")
+ void getDeletedNews() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/news/{id}", newsId)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .contentType(ContentType.JSON)
+ .body("errorCode", equalTo(40401));
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("STEP 7: GET USER BY DELETED NEWS ID - Should return 404 Not Found")
+ void getUserByDeletedNewsId() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/news/user/{id}", newsId)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .body("errorCode", equalTo(40401));
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("NEGATIVE: CREATE NEWS WITH INVALID USER ID - Should return 404 Not Found")
+ void createNewsWithInvalidUserId() {
+ String payload = """
+ {
+ "title": "Invalid News",
+ "content": "This should fail",
+ "userId": 999999
+ }
+ """;
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .post("/api/v1.0/news")
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .body("errorCode", equalTo(40401));
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/test/SecurityV2IntegrationTest.java b/351004/Halukha/publisher/src/test/java/com/example/lab/test/SecurityV2IntegrationTest.java
new file mode 100644
index 000000000..b3a382493
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/test/SecurityV2IntegrationTest.java
@@ -0,0 +1,86 @@
+package com.example.lab.test;
+
+import static org.hamcrest.Matchers.notNullValue;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import static io.restassured.RestAssured.given;
+import io.restassured.http.ContentType;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class SecurityV2IntegrationTest {
+
+ @LocalServerPort
+ int port;
+
+ @Test
+ void v1_stays_public() {
+ given()
+ .port(port)
+ .when()
+ .get("/api/v1.0/users")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void v2_requires_auth() {
+ given()
+ .port(port)
+ .when()
+ .get("/api/v2.0/news")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ void registration_and_login_issue_jwt() {
+ String login = "sec_user_" + System.currentTimeMillis();
+
+ given()
+ .port(port)
+ .contentType(ContentType.JSON)
+ .body("""
+ {
+ "login": "%s",
+ "password": "password123",
+ "firstname": "John",
+ "lastname": "Doe",
+ "role": "CUSTOMER"
+ }
+ """.formatted(login))
+ .when()
+ .post("/api/v2.0/users")
+ .then()
+ .statusCode(201);
+
+ String token = given()
+ .port(port)
+ .contentType(ContentType.JSON)
+ .body("""
+ {
+ "login": "%s",
+ "password": "password123"
+ }
+ """.formatted(login))
+ .when()
+ .post("/api/v2.0/login")
+ .then()
+ .statusCode(200)
+ .body("access_token", notNullValue())
+ .extract()
+ .path("access_token");
+
+ given()
+ .port(port)
+ .header("Authorization", "Bearer " + token)
+ .when()
+ .get("/api/v2.0/users/me")
+ .then()
+ .statusCode(200)
+ .body("login", notNullValue())
+ .body("role", notNullValue());
+ }
+}
+
diff --git a/351004/Halukha/publisher/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java b/351004/Halukha/publisher/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java
new file mode 100644
index 000000000..cef275a4f
--- /dev/null
+++ b/351004/Halukha/publisher/src/test/java/com/example/lab/test/UserControllerIntegrationTest.java
@@ -0,0 +1,118 @@
+package com.example.lab.test;
+
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.*;
+import org.springframework.http.HttpStatus;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class UserControllerIntegrationTest extends BaseIntegrationTest {
+
+ private static Long userId;
+
+ @Test
+ @Order(1)
+ @DisplayName("STEP 1: CREATE USER - Should return 201 Created")
+ void createUser() {
+ String payload = """
+ {
+ "login": "test_user",
+ "password": "secure123",
+ "firstname": "John",
+ "lastname": "Doe"
+ }
+ """;
+
+ userId = given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .post("/api/v1.0/users")
+ .then()
+ .statusCode(HttpStatus.CREATED.value())
+ .contentType(ContentType.JSON)
+ .body("id", notNullValue())
+ .body("login", equalTo("test_user"))
+ .body("firstname", equalTo("John"))
+ .body("lastname", equalTo("Doe"))
+ .extract()
+ .jsonPath()
+ .getLong("id");
+
+ Assertions.assertNotNull(userId);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("STEP 2: GET USER BY ID - Should return 200 OK")
+ void getUserById() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/users/{id}", userId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(userId.intValue()))
+ .body("login", equalTo("test_user"))
+ .body("firstname", equalTo("John"));
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("STEP 3: UPDATE USER - Should return 200 OK")
+ void updateUser() {
+ String payload = """
+ {
+ "id": %d,
+ "login": "test_user",
+ "password": "newpassword456",
+ "firstname": "Jane",
+ "lastname": "Smith"
+ }
+ """.formatted(userId);
+
+ given()
+ .baseUri(getBaseUrl())
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .when()
+ .put("/api/v1.0/users/{id}", userId)
+ .then()
+ .statusCode(HttpStatus.OK.value())
+ .contentType(ContentType.JSON)
+ .body("id", equalTo(userId.intValue()))
+ .body("login", equalTo("test_user"))
+ .body("firstname", equalTo("Jane"))
+ .body("lastname", equalTo("Smith"));
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("STEP 4: DELETE USER - Should return 204 No Content")
+ void deleteUser() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .delete("/api/v1.0/users/{id}", userId)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT.value());
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("STEP 5: GET DELETED USER - Should return 404 Not Found")
+ void getDeletedUser() {
+ given()
+ .baseUri(getBaseUrl())
+ .when()
+ .get("/api/v1.0/users/{id}", userId)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND.value())
+ .contentType(ContentType.JSON)
+ .body("errorCode", equalTo(40401));
+ }
+}
\ No newline at end of file
diff --git a/351004/Halukha/src/main/java/com/example/lab/LabApplication.java b/351004/Halukha/src/main/java/com/example/lab/LabApplication.java
deleted file mode 100644
index 145e3f3fa..000000000
--- a/351004/Halukha/src/main/java/com/example/lab/LabApplication.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.example.lab;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class LabApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(LabApplication.class, args);
- }
-}
diff --git a/351004/Halukha/src/main/java/com/example/lab/repository/PostRepository.java b/351004/Halukha/src/main/java/com/example/lab/repository/PostRepository.java
deleted file mode 100644
index 9feaeaf26..000000000
--- a/351004/Halukha/src/main/java/com/example/lab/repository/PostRepository.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.example.lab.repository;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import com.example.lab.model.Post;
-
-public interface PostRepository extends JpaRepository {
-}
diff --git a/351004/Halukha/src/main/java/com/example/lab/repository/UserRepository.java b/351004/Halukha/src/main/java/com/example/lab/repository/UserRepository.java
deleted file mode 100644
index 0e36a4dbb..000000000
--- a/351004/Halukha/src/main/java/com/example/lab/repository/UserRepository.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.example.lab.repository;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import com.example.lab.model.User;
-
-public interface UserRepository extends JpaRepository {
-}
diff --git a/351004/Halukha/src/main/java/com/example/lab/service/MarkerService.java b/351004/Halukha/src/main/java/com/example/lab/service/MarkerService.java
deleted file mode 100644
index 342ed0baa..000000000
--- a/351004/Halukha/src/main/java/com/example/lab/service/MarkerService.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.example.lab.service;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.springframework.stereotype.Service;
-
-import com.example.lab.dto.MarkerRequestTo;
-import com.example.lab.dto.MarkerResponseTo;
-import com.example.lab.exception.EntityNotFoundException;
-import com.example.lab.mapper.MarkerMapper;
-import com.example.lab.model.Marker;
-import com.example.lab.repository.MarkerRepository;
-
-@Service
-public class MarkerService {
-
- private final MarkerRepository newsRepository;
- private final MarkerMapper mapper = MarkerMapper.INSTANCE;
-
- public MarkerService(MarkerRepository newsRepository) {
- this.newsRepository = newsRepository;
- }
-
- public List getAllMarker() {
- return newsRepository.findAll().stream()
- .map(mapper::toDto)
- .collect(Collectors.toList());
- }
-
- public MarkerResponseTo getMarkerById(Long id) {
- return newsRepository.findById(id)
- .map(mapper::toDto)
- .orElseThrow(() -> new EntityNotFoundException("Marker not found", 40401));
- }
-
- public MarkerResponseTo createMarker(MarkerRequestTo request) {
- Marker news = mapper.toEntity(request);
- Marker saved = newsRepository.save(news);
- return mapper.toDto(saved);
- }
-
- public MarkerResponseTo updateMarker(Long id, MarkerRequestTo request) {
- Marker existing = newsRepository.findById(id)
- .orElseThrow(() -> new EntityNotFoundException("Marker not found", 40401));
- Marker updated = mapper.updateEntity(request, existing);
- updated.setId(id);
- Marker saved = newsRepository.save(updated);
- return mapper.toDto(saved);
- }
-
- public void deleteMarker(Long id) {
- if (!newsRepository.existsById(id)) {
- throw new EntityNotFoundException("Marker not found", 40401);
- }
- newsRepository.deleteById(id);
- }
-}
diff --git a/351004/Halukha/src/main/java/com/example/lab/service/PostService.java b/351004/Halukha/src/main/java/com/example/lab/service/PostService.java
deleted file mode 100644
index 8833d1365..000000000
--- a/351004/Halukha/src/main/java/com/example/lab/service/PostService.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.example.lab.service;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.springframework.stereotype.Service;
-
-import com.example.lab.dto.PostRequestTo;
-import com.example.lab.dto.PostResponseTo;
-import com.example.lab.exception.EntityNotFoundException;
-import com.example.lab.mapper.PostMapper;
-import com.example.lab.model.News;
-import com.example.lab.model.Post;
-import com.example.lab.repository.NewsRepository;
-import com.example.lab.repository.PostRepository;
-
-@Service
-public class PostService {
-
- private final PostRepository postRepository;
- private final NewsRepository newsRepository;
- private final PostMapper mapper = PostMapper.INSTANCE;
-
- public PostService(PostRepository postRepository, NewsRepository newsRepository) {
- this.postRepository = postRepository;
- this.newsRepository = newsRepository;
- }
-
- public List getAllPost() {
- return postRepository.findAll().stream()
- .map(mapper::toDto)
- .collect(Collectors.toList());
- }
-
- public PostResponseTo getPostById(Long id) {
- return postRepository.findById(id)
- .map(mapper::toDto)
- .orElseThrow(() -> new EntityNotFoundException("Post not found", 40401));
- }
-
- public PostResponseTo createPost(PostRequestTo request) {
- if (!newsRepository.existsById(request.getNewsId())) {
- throw new EntityNotFoundException("News not found", 40401);
- }
- Post news = mapper.toEntity(request);
- Post saved = postRepository.save(news);
- return mapper.toDto(saved);
- }
-
- public PostResponseTo updatePost(Long id, PostRequestTo request) {
- Post existing = postRepository.findById(id)
- .orElseThrow(() -> new EntityNotFoundException("Post not found", 40401));
- Post updated = mapper.updateEntity(request, existing);
- updated.setId(id);
- Post saved = postRepository.save(updated);
- return mapper.toDto(saved);
- }
-
- public void deletePost(Long id) {
- if (!postRepository.existsById(id)) {
- throw new EntityNotFoundException("Post not found", 40401);
- }
- postRepository.deleteById(id);
- }
-
- public List getAllPostByNewsId(Long userId) {
- List news = newsRepository.findAll().stream()
- .filter(news1 -> news1.getUserId().equals(userId))
- .collect(Collectors.toList());
- return postRepository.findAll().stream()
- .filter(post -> news.stream().anyMatch(post1 -> post1.getId().equals(post.getNewsId())))
- .map(mapper::toDto)
- .collect(Collectors.toList());
- }
-}
diff --git a/351004/Halukha/src/main/resources/application.properties b/351004/Halukha/src/main/resources/application.properties
deleted file mode 100644
index eeee4ba03..000000000
--- a/351004/Halukha/src/main/resources/application.properties
+++ /dev/null
@@ -1,12 +0,0 @@
-spring.application.name=lab
-
-spring.datasource.url=jdbc:postgresql://localhost:5432/distcomp
-spring.datasource.username=postgres
-spring.datasource.password=postgres
-spring.datasource.driver-class-name=org.postgresql.Driver
-
-spring.jpa.database=postgresql
-spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
-spring.jpa.show-sql=true
-spring.jpa.properties.hibernate.format_sql=true
diff --git a/351004/Halukha/src/main/resources/application.yaml b/351004/Halukha/src/main/resources/application.yaml
deleted file mode 100644
index 84eba713e..000000000
--- a/351004/Halukha/src/main/resources/application.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-server:
- port: 24110
-
-spring:
- application:
- name: task310-rest-api
\ No newline at end of file