diff --git a/.gitignore b/.gitignore index 67c71c5..75cc394 100644 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,12 @@ build/ .air/ .config-repo/ +.config/ +.properties/ .env application-*.properties application-*.yml application-*.yaml +docker-compose.dev.yaml diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleDetailQueryHandler.java b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleDetailQueryHandler.java new file mode 100644 index 0000000..851d863 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleDetailQueryHandler.java @@ -0,0 +1,38 @@ +package io.theurl.bundle.persistence.handler; + +import com.neroyun.mediator.Handler; +import io.theurl.bundle.persistence.model.BundleDetailModel; +import io.theurl.bundle.persistence.query.BundleDetailQuery; +import io.theurl.bundle.persistence.repository.JpaBundleRepository; +import io.theurl.framework.core.BeanScope; +import jakarta.persistence.EntityNotFoundException; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +@Scope(BeanScope.PROTOTYPE) +public class BundleDetailQueryHandler implements Handler { + + private final JpaBundleRepository repository; + private final ModelMapper mapper; + + public BundleDetailQueryHandler(JpaBundleRepository repository, ModelMapper mapper) { + this.repository = repository; + this.mapper = mapper; + } + + @Override + public CompletableFuture handleAsync(BundleDetailQuery message) { + var entity = repository.findByVanity(message.vanity()) + .orElse(null); + if (entity == null || entity.isDeleted()) { + throw new EntityNotFoundException("Bundle not found with vanity: " + message.vanity()); + } + + var detail = mapper.map(entity, BundleDetailModel.class); + return CompletableFuture.completedFuture(detail); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleDetailModel.java b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleDetailModel.java new file mode 100644 index 0000000..b2efa73 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleDetailModel.java @@ -0,0 +1,23 @@ +package io.theurl.bundle.persistence.model; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class BundleDetailModel { + private Long id; + private String vanity; + private String type; + private String name; + private String description; + private Long ownerId; + private String ownerName; + private int itemCount; + private int commentCount; + private int favoriteCount; + private int visitCount; + private LocalDateTime lastVisitedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java new file mode 100644 index 0000000..09f222e --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java @@ -0,0 +1,4 @@ +package io.theurl.bundle.persistence.model; + +public class BundleItemModel { +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleDetailQuery.java b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleDetailQuery.java new file mode 100644 index 0000000..64b8675 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleDetailQuery.java @@ -0,0 +1,7 @@ +package io.theurl.bundle.persistence.query; + +import com.neroyun.mediator.Query; +import io.theurl.bundle.persistence.model.BundleDetailModel; + +public record BundleDetailQuery(String vanity) implements Query { +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/repository/BundleRepositoryImpl.java b/bundle/src/main/java/io/theurl/bundle/persistence/repository/BundleRepositoryImpl.java new file mode 100644 index 0000000..c14c778 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/repository/BundleRepositoryImpl.java @@ -0,0 +1,57 @@ +package io.theurl.bundle.persistence.repository; + +import io.theurl.bundle.domain.aggregate.Bundle; +import io.theurl.bundle.domain.repository.BundleRepository; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public class BundleRepositoryImpl implements BundleRepository { + private final JpaBundleRepository repository; + private final ModelMapper mapper; + + public BundleRepositoryImpl(JpaBundleRepository repository, ModelMapper mapper) { + this.repository = repository; + this.mapper = mapper; + } + + @Override + public void save(Bundle bundle, long operatorId) { + var entity = repository.findById(bundle.getId()) + .orElse(null); + if (entity == null) { + entity = mapper.map(bundle, io.theurl.bundle.persistence.entity.Bundle.class); + entity.setCreatedBy(operatorId); + } else if (bundle.isDeleted()) { + entity.setDeleted(true); + entity.setDeletedBy(operatorId); + entity.setDeletedAt(LocalDateTime.now()); + } else { + mapper.map(bundle, entity); + entity.setUpdatedBy(operatorId); + } + + repository.save(entity); + } + + @Override + public Bundle findById(Long id) { + var entity = repository.findById(id).orElse(null); + if (entity == null) { + return null; + } + return mapper.map(entity, Bundle.class); + } + + @Override + public Bundle findByVanity(String vanity) { + var entity = repository.findByVanity(vanity) + .orElse(null); + if (entity == null || entity.isDeleted()) { + return null; + } + return mapper.map(entity, Bundle.class); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/repository/JpaBundleRepository.java b/bundle/src/main/java/io/theurl/bundle/persistence/repository/JpaBundleRepository.java new file mode 100644 index 0000000..4103321 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/repository/JpaBundleRepository.java @@ -0,0 +1,10 @@ +package io.theurl.bundle.persistence.repository; + +import io.theurl.bundle.persistence.entity.Bundle; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface JpaBundleRepository extends CrudRepository { + Optional findByVanity(String vanity); +} diff --git a/config/pom.xml b/config/pom.xml index c33064e..290789c 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -37,6 +37,11 @@ org.springframework.boot spring-boot-starter-security + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.3 + diff --git a/config/src/main/java/io/theurl/config/ConfigController.java b/config/src/main/java/io/theurl/config/ConfigController.java new file mode 100644 index 0000000..ebd6757 --- /dev/null +++ b/config/src/main/java/io/theurl/config/ConfigController.java @@ -0,0 +1,54 @@ +package io.theurl.config; + +import org.bson.Document; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("api") +public class ConfigController { + private final MongoTemplate mongo; + private final String collectionName; + + public ConfigController(MongoTemplate mongo, @Value("${spring.cloud.config.server.mongodb.collection:properties}") String collectionName) { + this.mongo = mongo; + this.collectionName = collectionName; + } + + @PostMapping("{application}/{profile}") + public void update(@PathVariable String application, @PathVariable String profile, @RequestBody Document payload) { + + if (application == null || application.isEmpty()) { + throw new IllegalArgumentException("application cannot be null or empty"); + } + + if (profile == null || profile.isEmpty()) { + throw new IllegalArgumentException("profile cannot be null or empty"); + } + + var query = new Query(); + query.addCriteria(Criteria.where("application").is(application) + .and("profile").is(profile)); + var update = new Update(); + update.set("version", System.currentTimeMillis()); + update.set("properties", payload); + mongo.upsert(query, update, collectionName); + } + + @GetMapping("{application}/{profile}") + public Object query(@PathVariable String application, @PathVariable String profile) { + var query = new Query(); + query.addCriteria(Criteria.where("application").is(application).and("profile").is(profile)); + var document = mongo.find(query, Document.class, collectionName).stream().findFirst().orElse(null); + if (document == null) { + return null; + } + + return document.get("properties"); + } +} diff --git a/config/src/main/java/io/theurl/config/LoggingRequestFilter.java b/config/src/main/java/io/theurl/config/LoggingRequestFilter.java new file mode 100644 index 0000000..407db3c --- /dev/null +++ b/config/src/main/java/io/theurl/config/LoggingRequestFilter.java @@ -0,0 +1,50 @@ +package io.theurl.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class LoggingRequestFilter extends OncePerRequestFilter { + + private static final String[] ipHeaders = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + var ip = getClientIp(request); + String method = request.getMethod(); + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + String fullUri = uri + (queryString != null ? "?" + queryString : ""); + System.out.println(request.getSession().getId() + " " + ip + " : " + method + " " + fullUri); + filterChain.doFilter(request, response); + System.out.println(request.getSession().getId() + " " + ip + " : " + response.getStatus()); + } + + public static String getClientIp(HttpServletRequest request) { + for (String header : ipHeaders) { + if (request.getHeader(header) != null) { + return request.getHeader(header); + } + } + + var remoteAddr = request.getRemoteAddr(); + return switch (remoteAddr) { + case null -> "127.0.0.1"; + case "0:0:0:0:0:0:0:1", "::1" -> "127.0.0.1"; + default -> remoteAddr; + }; + } +} diff --git a/config/src/main/java/io/theurl/config/RequestFilterConfiguration.java b/config/src/main/java/io/theurl/config/RequestFilterConfiguration.java new file mode 100644 index 0000000..8d8ce8b --- /dev/null +++ b/config/src/main/java/io/theurl/config/RequestFilterConfiguration.java @@ -0,0 +1,17 @@ +package io.theurl.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RequestFilterConfiguration { + + @Bean + public FilterRegistrationBean loggingFilterRegistration() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new LoggingRequestFilter()); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } +} diff --git a/config/src/main/java/io/theurl/config/SecurityConfiguration.java b/config/src/main/java/io/theurl/config/SecurityConfiguration.java index 308ee49..730f51d 100644 --- a/config/src/main/java/io/theurl/config/SecurityConfiguration.java +++ b/config/src/main/java/io/theurl/config/SecurityConfiguration.java @@ -11,7 +11,7 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { http.authorizeHttpRequests(auth -> { - auth.requestMatchers("/api/**").authenticated(); + //auth.requestMatchers("/api/**").authenticated(); auth.anyRequest().permitAll(); }) .csrf(AbstractHttpConfigurer::disable) diff --git a/config/src/main/resources/application.yaml b/config/src/main/resources/application.yaml index 190fd0a..dd92afa 100644 --- a/config/src/main/resources/application.yaml +++ b/config/src/main/resources/application.yaml @@ -11,13 +11,14 @@ spring: name: ${CONFIG_SERVER_USERNAME:config-user} password: ${CONFIG_SERVER_PASSWORD:config-password} mongodb: - uri: ${MONGO_URI:mongodb://host.docker.internal:27017/linkyou?authSource=admin} + uri: ${MONGO_URI:mongodb://mongoadmin:nerosoft.8888@localhost:27017/linkyou?authSource=admin} data: redis: - url: ${REDIS_URL:redis://localhost:6379} + url: ${REDIS_URL:redis://host.docker.internal:6379} cloud: config: server: + prefix: env native: search-locations: ${CONFIG_SEARCH_LOCATIONS:file:./config} mongodb: diff --git a/docker-compose.yaml b/docker-compose.yaml index c52b171..5b65b5c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,8 @@ services: - CONFIG_SERVER_USERNAME=theurl - CONFIG_SERVER_PASSWORD=Qwer.1234 - CONFIG_SEARCH_LOCATIONS=file:./config + - MONGO_URI=mongodb://theurl-mongo:27017/linkyou?authSource=admin + - REDIS_URL=redis://theurl-redis:6379 volumes: - theurl-config-data:/app/config @@ -18,9 +20,7 @@ services: ports: - "8901:8901" environment: - - CONFIG_SERVER_URI=http://theurl-config-server:8900 - - CONFIG_SERVER_USERNAME=theurl - - CONFIG_SERVER_PASSWORD=Qwer.1234 + - CONFIG_SERVER_URI=http://theurl-config-server:8900/env - DB_URL=jdbc:postgresql://theurl-db:5432/linkyou?currentSchema=public - DB_USERNAME=postgres - DB_PASSWORD=postgres diff --git a/identity/Dockerfile b/identity/Dockerfile index c7978dc..f4afcb2 100644 --- a/identity/Dockerfile +++ b/identity/Dockerfile @@ -22,10 +22,8 @@ WORKDIR /app ENV JAVA_OPTS="-Xms512m -Xmx1024m" ENV SPRING_PROFILES_ACTIVE=prod -ENV CONFIG_SERVER_URI=http://theurl-config-server:8900 -ENV CONFIG_SERVER_USERNAME=theurl -ENV CONFIG_SERVER_PASSWORD=Qwer.1234 -ENV DB_URL=jdbc:postgresql://theurl-db:5432/linkyou?currentSchema=public +ENV CONFIG_SERVER_URI=http://theurl-config-server:8900/env +ENV DB_URL="jdbc:postgresql://theurl-db:5432/linkyou?currentSchema=public" ENV DB_USERNAME=postgres ENV DB_PASSWORD=postgres ENV DB_DRIVER=org.postgresql.Driver diff --git a/identity/src/main/resources/application.yaml b/identity/src/main/resources/application.yaml index c2bb7be..edf855f 100644 --- a/identity/src/main/resources/application.yaml +++ b/identity/src/main/resources/application.yaml @@ -7,13 +7,7 @@ spring: application: name: identity config: - import: optional:file:.env[.properties] - cloud: - config: - enabled: true - uri: ${CONFIG_SERVER_URI:http://localhost:8900} - password: ${CONFIG_SERVER_PASSWORD:Qwer.1234} - username: ${CONFIG_SERVER_USERNAME:theurl} + import: optional:configserver:${CONFIG_SERVER_URI:http://localhost:8900/env} datasource: url: ${DB_URL:jdbc:postgresql://localhost:5432/linkyou?currentSchema=public} username: ${DB_USERNAME:postgres}