diff --git a/.dev/dev_arm64.yaml b/.dev/dev_arm64.yaml index 220140d3d..dc1a8726e 100644 --- a/.dev/dev_arm64.yaml +++ b/.dev/dev_arm64.yaml @@ -32,7 +32,7 @@ services: KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true' kafka0: - image: confluentinc/cp-kafka:7.6.0.arm64 + image: confluentinc/cp-kafka:7.8.0.arm64 user: "0:0" hostname: kafka0 container_name: kafka0 @@ -60,7 +60,7 @@ services: CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' schema-registry0: - image: confluentinc/cp-schema-registry:7.6.0.arm64 + image: confluentinc/cp-schema-registry:7.8.0.arm64 ports: - 8085:8085 depends_on: @@ -76,7 +76,7 @@ services: SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas kafka-connect0: - image: confluentinc/cp-kafka-connect:7.6.0.arm64 + image: confluentinc/cp-kafka-connect:7.8.0.arm64 ports: - 8083:8083 depends_on: @@ -101,7 +101,7 @@ services: CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components,/usr/local/share/kafka/plugins,/usr/share/filestream-connectors" ksqldb0: - image: confluentinc/cp-ksqldb-server:7.6.0.arm64 + image: confluentinc/cp-ksqldb-server:7.8.0.arm64 depends_on: - kafka0 - kafka-connect0 @@ -119,7 +119,7 @@ services: KSQL_CACHE_MAX_BYTES_BUFFERING: 0 kafka-init-topics: - image: confluentinc/cp-kafka:7.6.0.arm64 + image: confluentinc/cp-kafka:7.8.0.arm64 volumes: - ../documentation/compose/data/message.json:/data/message.json depends_on: diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 6755b1b61..7c4955373 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -23,7 +23,7 @@ jobs: - uses: pnpm/action-setup@v4.0.0 with: - version: 9.11.0 + version: 9.15.0 - name: Install node uses: actions/setup-node@v4.0.2 diff --git a/.gitignore b/.gitignore index efd6a9749..51efbef39 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ build/ *.tgz /docker/*.override.yaml +/e2e-tests/allure-results/ diff --git a/.java-version b/.java-version new file mode 100644 index 000000000..aabe6ec39 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/README.md b/README.md index 0f6e2fd73..d6206100a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,9 @@ Versatile, fast and lightweight web UI for managing Apache Kafka® clusters.

Documentation • - Quick Start • + Quick StartCommunity
- AWS MarketplaceProductHunt

@@ -28,7 +27,7 @@ Versatile, fast and lightweight web UI for managing Apache Kafka® clusters. #### Kafbat UI is a free, open-source web UI to monitor and manage Apache Kafka clusters. -Kafbat UI is a simple tool that makes your data flows observable, helps find and troubleshoot issues faster and deliver optimal performance. Its lightweight dashboard makes it easy to track key metrics of your Kafka clusters - Brokers, Topics, Partitions, Production, and Consumption. +[Kafbat UI](https://kafbat.io/) is a simple tool that makes your data flows observable, helps find and troubleshoot issues faster and deliver optimal performance. Its lightweight dashboard makes it easy to track key metrics of your Kafka clusters - Brokers, Topics, Partitions, Production, and Consumption. Kafbat UI, developed by Kafbat*, proudly carries forward the legacy of the UI Apache Kafka project. diff --git a/api/pom.xml b/api/pom.xml index 3f7c044d0..dc774c09a 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -50,7 +50,10 @@ org.apache.kafka kafka-clients - ${kafka-clients.version} + + ${confluent.version}-ccs org.apache.commons diff --git a/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java b/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java index 72ab7386a..474a0c159 100644 --- a/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java +++ b/api/src/main/java/io/kafbat/ui/client/RetryingKafkaConnectClient.java @@ -22,6 +22,7 @@ import java.util.Objects; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.util.unit.DataSize; import org.springframework.web.client.RestClientException; @@ -51,14 +52,36 @@ private static Retry conflictCodeRetry() { (WebClientResponseException.Conflict) signal.failure())); } - private static Mono withRetryOnConflict(Mono publisher) { - return publisher.retryWhen(conflictCodeRetry()); + private static @NotNull Retry retryOnRebalance() { + return Retry.fixedDelay(MAX_RETRIES, RETRIES_DELAY).filter(e -> { + + if (e instanceof WebClientResponseException.InternalServerError exception) { + final var errorMessage = getMessage(exception); + return StringUtils.equals(errorMessage, + // From https://github.com/apache/kafka/blob/dfc07e0e0c6e737a56a5402644265f634402b864/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/distributed/DistributedHerder.java#L2340 + "Request cannot be completed because a rebalance is expected"); + } + return false; + }); + } + + private static Mono withRetryOnConflictOrRebalance(Mono publisher) { + return publisher + .retryWhen(retryOnRebalance()) + .retryWhen(conflictCodeRetry()); + } + + private static Flux withRetryOnConflictOrRebalance(Flux publisher) { + return publisher + .retryWhen(retryOnRebalance()) + .retryWhen(conflictCodeRetry()); } - private static Flux withRetryOnConflict(Flux publisher) { - return publisher.retryWhen(conflictCodeRetry()); + private static Mono withRetryOnRebalance(Mono publisher) { + return publisher.retryWhen(retryOnRebalance()); } + private static Mono withBadRequestErrorHandling(Mono publisher) { return publisher .onErrorResume(WebClientResponseException.BadRequest.class, @@ -73,18 +96,21 @@ private record ErrorMessage(@NotNull @JsonProperty("message") String message) { } private static @NotNull Mono parseConnectErrorMessage(WebClientResponseException parseException) { + return Mono.error(new ValidationException(getMessage(parseException))); + } + + private static String getMessage(WebClientResponseException parseException) { final var errorMessage = parseException.getResponseBodyAs(ErrorMessage.class); - return Mono.error(new ValidationException( - Objects.requireNonNull(errorMessage, - // see https://github.com/apache/kafka/blob/a0a501952b6d61f6f273bdb8f842346b51e9dfce/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/rest/errors/ConnectExceptionMapper.java - "This should not happen according to the ConnectExceptionMapper") - .message())); + return Objects.requireNonNull(errorMessage, + // see https://github.com/apache/kafka/blob/a0a501952b6d61f6f273bdb8f842346b51e9dfce/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/rest/errors/ConnectExceptionMapper.java + "This should not happen according to the ConnectExceptionMapper") + .message(); } @Override public Mono createConnector(NewConnector newConnector) throws RestClientException { return withBadRequestErrorHandling( - super.createConnector(newConnector) + withRetryOnRebalance(super.createConnector(newConnector)) ); } @@ -92,178 +118,178 @@ public Mono createConnector(NewConnector newConnector) throws RestCli public Mono setConnectorConfig(String connectorName, Map requestBody) throws RestClientException { return withBadRequestErrorHandling( - super.setConnectorConfig(connectorName, requestBody) + withRetryOnRebalance(super.setConnectorConfig(connectorName, requestBody)) ); } @Override public Mono> createConnectorWithHttpInfo(NewConnector newConnector) throws WebClientResponseException { - return withRetryOnConflict(super.createConnectorWithHttpInfo(newConnector)); + return withRetryOnConflictOrRebalance(super.createConnectorWithHttpInfo(newConnector)); } @Override public Mono deleteConnector(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.deleteConnector(connectorName)); + return withRetryOnConflictOrRebalance(super.deleteConnector(connectorName)); } @Override public Mono> deleteConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.deleteConnectorWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.deleteConnectorWithHttpInfo(connectorName)); } @Override public Mono getConnector(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnector(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnector(connectorName)); } @Override public Mono> getConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorWithHttpInfo(connectorName)); } @Override public Mono> getConnectorConfig(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorConfig(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorConfig(connectorName)); } @Override public Mono>> getConnectorConfigWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorConfigWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorConfigWithHttpInfo(connectorName)); } @Override public Flux getConnectorPlugins() throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorPlugins()); + return withRetryOnConflictOrRebalance(super.getConnectorPlugins()); } @Override public Mono>> getConnectorPluginsWithHttpInfo() throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorPluginsWithHttpInfo()); + return withRetryOnConflictOrRebalance(super.getConnectorPluginsWithHttpInfo()); } @Override public Mono getConnectorStatus(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorStatus(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorStatus(connectorName)); } @Override public Mono> getConnectorStatusWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorStatusWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorStatusWithHttpInfo(connectorName)); } @Override public Mono getConnectorTaskStatus(String connectorName, Integer taskId) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTaskStatus(connectorName, taskId)); + return withRetryOnConflictOrRebalance(super.getConnectorTaskStatus(connectorName, taskId)); } @Override public Mono> getConnectorTaskStatusWithHttpInfo(String connectorName, Integer taskId) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId)); + return withRetryOnConflictOrRebalance(super.getConnectorTaskStatusWithHttpInfo(connectorName, taskId)); } @Override public Flux getConnectorTasks(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTasks(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorTasks(connectorName)); } @Override public Mono>> getConnectorTasksWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTasksWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorTasksWithHttpInfo(connectorName)); } @Override public Mono> getConnectorTopics(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTopics(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorTopics(connectorName)); } @Override public Mono>> getConnectorTopicsWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorTopicsWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.getConnectorTopicsWithHttpInfo(connectorName)); } @Override public Mono> getConnectors(String search) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectors(search)); + return withRetryOnConflictOrRebalance(super.getConnectors(search)); } @Override public Mono>> getConnectorsWithHttpInfo(String search) throws WebClientResponseException { - return withRetryOnConflict(super.getConnectorsWithHttpInfo(search)); + return withRetryOnConflictOrRebalance(super.getConnectorsWithHttpInfo(search)); } @Override public Mono pauseConnector(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.pauseConnector(connectorName)); + return withRetryOnConflictOrRebalance(super.pauseConnector(connectorName)); } @Override public Mono> pauseConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.pauseConnectorWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.pauseConnectorWithHttpInfo(connectorName)); } @Override public Mono restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed) throws WebClientResponseException { - return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed)); + return withRetryOnConflictOrRebalance(super.restartConnector(connectorName, includeTasks, onlyFailed)); } @Override public Mono> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks, Boolean onlyFailed) throws WebClientResponseException { - return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed)); + return withRetryOnConflictOrRebalance(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed)); } @Override public Mono restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException { - return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId)); + return withRetryOnConflictOrRebalance(super.restartConnectorTask(connectorName, taskId)); } @Override public Mono> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId) throws WebClientResponseException { - return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId)); + return withRetryOnConflictOrRebalance(super.restartConnectorTaskWithHttpInfo(connectorName, taskId)); } @Override public Mono resumeConnector(String connectorName) throws WebClientResponseException { - return super.resumeConnector(connectorName); + return withRetryOnRebalance(super.resumeConnector(connectorName)); } @Override public Mono> resumeConnectorWithHttpInfo(String connectorName) throws WebClientResponseException { - return withRetryOnConflict(super.resumeConnectorWithHttpInfo(connectorName)); + return withRetryOnConflictOrRebalance(super.resumeConnectorWithHttpInfo(connectorName)); } @Override public Mono> setConnectorConfigWithHttpInfo(String connectorName, Map requestBody) throws WebClientResponseException { - return withRetryOnConflict(super.setConnectorConfigWithHttpInfo(connectorName, requestBody)); + return withRetryOnConflictOrRebalance(super.setConnectorConfigWithHttpInfo(connectorName, requestBody)); } @Override public Mono validateConnectorPluginConfig(String pluginName, Map requestBody) throws WebClientResponseException { - return withRetryOnConflict(super.validateConnectorPluginConfig(pluginName, requestBody)); + return withRetryOnConflictOrRebalance(super.validateConnectorPluginConfig(pluginName, requestBody)); } @Override public Mono> validateConnectorPluginConfigWithHttpInfo( String pluginName, Map requestBody) throws WebClientResponseException { - return withRetryOnConflict(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody)); + return withRetryOnConflictOrRebalance(super.validateConnectorPluginConfigWithHttpInfo(pluginName, requestBody)); } private static class RetryingApiClient extends ApiClient { diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index e91a5bc9a..5931602b2 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -35,22 +35,31 @@ public class ClustersProperties { public static class Cluster { String name; String bootstrapServers; + + TruststoreConfig ssl; + String schemaRegistry; SchemaRegistryAuth schemaRegistryAuth; KeystoreConfig schemaRegistrySsl; + String ksqldbServer; KsqldbServerAuth ksqldbServerAuth; KeystoreConfig ksqldbServerSsl; + List kafkaConnect; - MetricsConfigData metrics; - Map properties; - boolean readOnly = false; + List serde; String defaultKeySerde; String defaultValueSerde; - List masking; + + MetricsConfigData metrics; + Map properties; + boolean readOnly = false; + Long pollingThrottleRate; - TruststoreConfig ssl; + + List masking; + AuditProperties audit; } @@ -99,6 +108,16 @@ public static class SchemaRegistryAuth { public static class TruststoreConfig { String truststoreLocation; String truststorePassword; + boolean verifySsl = true; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString(exclude = {"keystorePassword"}) + public static class KeystoreConfig { + String keystoreLocation; + String keystorePassword; } @Data @@ -118,15 +137,6 @@ public static class KsqldbServerAuth { String password; } - @Data - @NoArgsConstructor - @AllArgsConstructor - @ToString(exclude = {"keystorePassword"}) - public static class KeystoreConfig { - String keystoreLocation; - String keystorePassword; - } - @Data public static class Masking { Type type; @@ -182,6 +192,7 @@ private void flattenClusterProperties() { } } + @SuppressWarnings("unchecked") private Map flattenClusterProperties(@Nullable String prefix, @Nullable Map propertiesMap) { Map flattened = new HashMap<>(); diff --git a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java index f23a0dd2a..265bac03f 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java @@ -1,24 +1,53 @@ package io.kafbat.ui.config.auth; +import io.kafbat.ui.util.EmptyRedirectStrategy; +import java.net.URI; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; + abstract class AbstractAuthSecurityConfig { protected AbstractAuthSecurityConfig() { } + protected static final String LOGIN_URL = "/login"; + protected static final String LOGOUT_URL = "/auth?logout"; + protected static final String[] AUTH_WHITELIST = { - "/css/**", - "/js/**", - "/media/**", + /* STATIC */ + "/index.html", + "/assets/**", + "/manifest.json", + "/favicon.svg", + "/favicon/**", + + "/static/**", "/resources/**", + + /* ACTUATOR */ "/actuator/health/**", "/actuator/info", "/actuator/prometheus", - "/auth", + + /* AUTH */ "/login", "/logout", "/oauth2/**", - "/static/**" + "/api/config/authentication", + "/api/authorization" }; + protected RedirectServerAuthenticationSuccessHandler emptyRedirectSuccessHandler() { + final var authHandler = new RedirectServerAuthenticationSuccessHandler(); + authHandler.setRedirectStrategy(new EmptyRedirectStrategy()); + return authHandler; + } + + protected RedirectServerLogoutSuccessHandler redirectLogoutSuccessHandler() { + final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); + logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); + return logoutSuccessHandler; + } + } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java index 7a25fb3a7..db8ef8153 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/BasicAuthSecurityConfig.java @@ -1,6 +1,7 @@ package io.kafbat.ui.config.auth; import io.kafbat.ui.util.EmptyRedirectStrategy; +import io.kafbat.ui.util.StaticFileWebFilter; import java.net.URI; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -8,6 +9,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; @@ -20,32 +22,28 @@ @Slf4j public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig { - public static final String LOGIN_URL = "/auth"; - public static final String LOGOUT_URL = "/auth?logout"; - @Bean public SecurityWebFilterChain configure(ServerHttpSecurity http) { log.info("Configuring LOGIN_FORM authentication."); - final var authHandler = new RedirectServerAuthenticationSuccessHandler(); - authHandler.setRedirectStrategy(new EmptyRedirectStrategy()); - - final var logoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); - logoutSuccessHandler.setLogoutSuccessUrl(URI.create(LOGOUT_URL)); - - - return http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() .authenticated() ) - .formLogin(spec -> spec.loginPage(LOGIN_URL).authenticationSuccessHandler(authHandler)) + .formLogin(form -> form + .loginPage(LOGIN_URL) + .authenticationSuccessHandler(emptyRedirectSuccessHandler()) + ) .logout(spec -> spec - .logoutSuccessHandler(logoutSuccessHandler) + .logoutSuccessHandler(redirectLogoutSuccessHandler()) .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java index 90ffa17fe..9b1445507 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java @@ -3,6 +3,7 @@ import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.service.rbac.extractor.RbacActiveDirectoryAuthoritiesExtractor; import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor; +import io.kafbat.ui.util.StaticFileWebFilter; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -14,14 +15,15 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -36,6 +38,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; @Configuration @EnableWebFluxSecurity @@ -136,16 +139,24 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { log.info("Active Directory support for LDAP has been enabled."); } - return http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() .authenticated() ) - .formLogin(Customizer.withDefaults()) - .logout(Customizer.withDefaults()) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .formLogin(form -> form + .loginPage(LOGIN_URL) + .authenticationSuccessHandler(emptyRedirectSuccessHandler()) + ) + .logout(spec -> spec + .logoutSuccessHandler(redirectLogoutSuccessHandler()) + .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))) + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } private static class RbacUserDetailsMapper extends LdapUserDetailsMapper { diff --git a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java index 09c7df794..4794b83ca 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java @@ -3,6 +3,7 @@ import io.kafbat.ui.config.auth.logout.OAuthLogoutSuccessHandler; import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import io.kafbat.ui.util.StaticFileWebFilter; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -19,6 +20,7 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; @@ -50,7 +52,7 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig { public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) { log.info("Configuring OAUTH2 authentication."); - return http.authorizeExchange(spec -> spec + var builder = http.authorizeExchange(spec -> spec .pathMatchers(AUTH_WHITELIST) .permitAll() .anyExchange() @@ -58,8 +60,12 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSucc ) .oauth2Login(Customizer.withDefaults()) .logout(spec -> spec.logoutSuccessHandler(logoutHandler)) - .csrf(ServerHttpSecurity.CsrfSpec::disable) - .build(); + .csrf(ServerHttpSecurity.CsrfSpec::disable); + + + builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + + return builder.build(); } @Bean diff --git a/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java b/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java index 5d5d4ed98..e8d763545 100644 --- a/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java +++ b/api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java @@ -6,6 +6,7 @@ import io.kafbat.ui.api.ApplicationConfigApi; import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.ActionDTO; +import io.kafbat.ui.model.AppAuthenticationSettingsDTO; import io.kafbat.ui.model.ApplicationConfigDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesDTO; import io.kafbat.ui.model.ApplicationConfigValidationDTO; @@ -66,6 +67,13 @@ public Mono> getApplicationInfo(ServerWebExch return Mono.just(applicationInfoService.getApplicationInfo()).map(ResponseEntity::ok); } + @Override + public Mono> getAuthenticationSettings( + ServerWebExchange exchange) { + return Mono.just(applicationInfoService.getAuthenticationProperties()) + .map(ResponseEntity::ok); + } + @Override public Mono> getCurrentConfig(ServerWebExchange exchange) { var context = AccessContext.builder() @@ -109,7 +117,7 @@ public Mono> uploadConfigRelatedFile(Flux dynamicConfigOperations.uploadConfigRelatedFile((FilePart) file) - .map(path -> new UploadedFileInfoDTO().location(path.toString())) + .map(path -> new UploadedFileInfoDTO(path.toString())) .map(ResponseEntity::ok)) .doOnEach(sig -> audit(context, sig)); } diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthController.java b/api/src/main/java/io/kafbat/ui/controller/AuthController.java deleted file mode 100644 index e4532dda3..000000000 --- a/api/src/main/java/io/kafbat/ui/controller/AuthController.java +++ /dev/null @@ -1,99 +0,0 @@ -package io.kafbat.ui.controller; - -import java.nio.charset.Charset; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.web.server.csrf.CsrfToken; -import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; - -@RestController -@RequiredArgsConstructor -@Slf4j -public class AuthController { - - @GetMapping(value = "/auth", produces = {"text/html"}) - public Mono getAuth(ServerWebExchange exchange) { - Mono token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()); - return token - .map(AuthController::csrfToken) - .defaultIfEmpty("") - .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput)); - } - - private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) { - MultiValueMap queryParams = exchange.getRequest() - .getQueryParams(); - String contextPath = exchange.getRequest().getPath().contextPath().value(); - String page = - "\n" + "\n" + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "
\n" - + formLogin(queryParams, contextPath, csrfTokenHtmlInput) - + "
\n" - + " \n" - + ""; - - return page.getBytes(Charset.defaultCharset()); - } - - private String formLogin( - MultiValueMap queryParams, - String contextPath, String csrfTokenHtmlInput) { - - boolean isError = queryParams.containsKey("error"); - boolean isLogoutSuccess = queryParams.containsKey("logout"); - return - "
\n" - + " \n" - + createError(isError) - + createLogoutSuccess(isLogoutSuccess) - + "

\n" - + " \n" - + " \n" - + "

\n" + "

\n" - + " \n" - + " \n" - + "

\n" + csrfTokenHtmlInput - + " \n" - + "
\n"; - } - - private static String csrfToken(CsrfToken token) { - return " \n"; - } - - private static String createError(boolean isError) { - return isError - ? "
Invalid credentials
" - : ""; - } - - private static String createLogoutSuccess(boolean isLogoutSuccess) { - return isLogoutSuccess - ? "
You have been signed out
" - : ""; - } -} diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java new file mode 100644 index 000000000..c94c344c9 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/controller/AuthenticationController.java @@ -0,0 +1,22 @@ +package io.kafbat.ui.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class AuthenticationController { + + private static final String INDEX_HTML = "/static/index.html"; + + @GetMapping(value = "/login", produces = {"text/html"}) + public Mono getLoginPage() { + return Mono.just(new ClassPathResource(INDEX_HTML)); + } + +} diff --git a/api/src/main/java/io/kafbat/ui/controller/AccessController.java b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java similarity index 97% rename from api/src/main/java/io/kafbat/ui/controller/AccessController.java rename to api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java index e5b1ea438..1ac0aeb85 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AccessController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java @@ -26,7 +26,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class AccessController implements AuthorizationApi { +public class AuthorizationController implements AuthorizationApi { private final AccessControlService accessControlService; diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java b/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java index 9ccc10ccf..dbf5c456b 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java @@ -69,8 +69,10 @@ public boolean isAccessible(List userPermissions) throws AccessDenie if (name == null && permission.getCompiledValuePattern() == null) { return true; } - Preconditions.checkState(permission.getCompiledValuePattern() != null && name != null); - return permission.getCompiledValuePattern().matcher(name).matches(); + if (permission.getCompiledValuePattern() != null && name != null) { + return permission.getCompiledValuePattern().matcher(name).matches(); + } + return false; }) .flatMap(p -> p.getParsedActions().stream()) .collect(Collectors.toSet()); diff --git a/api/src/main/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerde.java b/api/src/main/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerde.java index 51c921603..2c0939c03 100644 --- a/api/src/main/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerde.java +++ b/api/src/main/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerde.java @@ -15,6 +15,7 @@ import com.google.protobuf.StructProto; import com.google.protobuf.TimestampProto; import com.google.protobuf.TypeProto; +import com.google.protobuf.TypeRegistry; import com.google.protobuf.WrappersProto; import com.google.protobuf.util.JsonFormat; import com.google.type.ColorProto; @@ -63,6 +64,7 @@ import javax.annotation.Nullable; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @Slf4j @@ -147,12 +149,18 @@ public boolean canSerialize(String topic, Serde.Target type) { @Override public Serde.Serializer serializer(String topic, Serde.Target type) { var descriptor = descriptorFor(topic, type).orElseThrow(); + TypeRegistry typeRegistry = TypeRegistry.newBuilder() + .add(descriptorPaths.keySet()) + .build(); + return new Serde.Serializer() { @SneakyThrows @Override public byte[] serialize(String input) { DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor); - JsonFormat.parser().merge(input, builder); + JsonFormat.parser() + .usingTypeRegistry(typeRegistry) + .merge(input, builder); return builder.build().toByteArray(); } }; @@ -409,7 +417,7 @@ private Map loadFilesWithLocations() { files.filter(p -> !Files.isDirectory(p) && p.toString().endsWith(".proto")) .forEach(path -> { // relative path will be used as "import" statement - String relativePath = baseLocation.relativize(path).toString(); + String relativePath = removeBackSlashes(baseLocation.relativize(path).toString()); var protoFileElement = ProtoParser.Companion.parse( Location.get(baseLocation.toString(), relativePath), readFileAsString(path) @@ -419,6 +427,27 @@ private Map loadFilesWithLocations() { } return filesByLocations; } + + /** + * Replaces backslashes in the given file path with forward slashes if the operating system is Windows. + * + *

This method is designed to standardize file paths by converting Windows-style backslashes (`\`) + * to Linux/Unix-style forward slashes (`/`) when the application is running on a Windows OS. + * On other operating systems, the input path is returned unchanged.

+ * + *

This is needed because imports in Protobuf use forward slashes (`/`) + * which causes a conflict with Windows paths. For example,`language/language.proto` + * would be converted to `language\language.proto` in Windows causing a resolution exception

+ * + * @param path the file path to standardize; must not be {@code null}. + * @return the standardized file path with forward slashes if running on Windows, or the original path otherwise. + */ + private @NotNull String removeBackSlashes(@NotNull final String path) { + if (SystemUtils.IS_OS_WINDOWS) { + return path.replace("\\", "/"); + } + return path; + } } } diff --git a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java index 675b85426..6c018ba31 100644 --- a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java +++ b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java @@ -2,7 +2,7 @@ import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.KafkaCluster; -import io.kafbat.ui.util.SslPropertiesUtil; +import io.kafbat.ui.util.KafkaClientSslPropertiesUtil; import java.io.Closeable; import java.time.Instant; import java.util.Map; @@ -42,7 +42,7 @@ public Mono get(KafkaCluster cluster) { private Mono createAdminClient(KafkaCluster cluster) { return Mono.fromSupplier(() -> { Properties properties = new Properties(); - SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); properties.putAll(cluster.getProperties()); properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); properties.putIfAbsent(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, clientTimeout); diff --git a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java index 7d380036c..7ee28b62d 100644 --- a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java +++ b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java @@ -1,16 +1,23 @@ package io.kafbat.ui.service; +import static io.kafbat.ui.api.model.AuthType.DISABLED; +import static io.kafbat.ui.api.model.AuthType.OAUTH2; import static io.kafbat.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum; import static io.kafbat.ui.util.GithubReleaseInfo.GITHUB_RELEASE_INFO_TIMEOUT; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Streams; +import io.kafbat.ui.model.AppAuthenticationSettingsDTO; import io.kafbat.ui.model.ApplicationInfoBuildDTO; import io.kafbat.ui.model.ApplicationInfoDTO; import io.kafbat.ui.model.ApplicationInfoLatestReleaseDTO; +import io.kafbat.ui.model.AuthTypeDTO; +import io.kafbat.ui.model.OAuthProviderDTO; import io.kafbat.ui.util.DynamicConfigOperations; import io.kafbat.ui.util.GithubReleaseInfo; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; @@ -18,20 +25,27 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.GitProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.stereotype.Service; @Service public class ApplicationInfoService { private final GithubReleaseInfo githubReleaseInfo; + private final ApplicationContext applicationContext; private final DynamicConfigOperations dynamicConfigOperations; private final BuildProperties buildProperties; private final GitProperties gitProperties; public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations, + ApplicationContext applicationContext, @Autowired(required = false) BuildProperties buildProperties, @Autowired(required = false) GitProperties gitProperties, @Value("${" + GITHUB_RELEASE_INFO_TIMEOUT + ":10}") int githubApiMaxWaitTime) { + this.applicationContext = applicationContext; this.dynamicConfigOperations = dynamicConfigOperations; this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties())); this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties())); @@ -70,6 +84,38 @@ private List getEnabledFeatures() { return enabledFeatures; } + public AppAuthenticationSettingsDTO getAuthenticationProperties() { + return new AppAuthenticationSettingsDTO() + .authType(AuthTypeDTO.fromValue(getAuthType())) + .oAuthProviders(getOAuthProviders()); + } + + private String getAuthType() { + return Optional.ofNullable(applicationContext.getEnvironment().getProperty("auth.type")) + .orElse(DISABLED.getValue()); + } + + @SuppressWarnings("unchecked") + private List getOAuthProviders() { + if (!getAuthType().equalsIgnoreCase(OAUTH2.getValue())) { + return Collections.emptyList(); + } + var type = ResolvableType.forClassWithGenerics(Iterable.class, ClientRegistration.class); + String[] names = this.applicationContext.getBeanNamesForType(type); + var bean = (Iterable) (names.length == 1 ? this.applicationContext.getBean(names[0]) : null); + + if (bean == null) { + return Collections.emptyList(); + } + + return Streams.stream(bean.iterator()) + .filter(r -> AuthorizationGrantType.AUTHORIZATION_CODE.equals(r.getAuthorizationGrantType())) + .map(r -> new OAuthProviderDTO() + .clientName(r.getClientName()) + .authorizationUri("/oauth2/authorization/" + r.getRegistrationId())) + .toList(); + } + // updating on startup and every hour @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}") public void updateGithubReleaseInfo() { diff --git a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java index b2d6bd20f..282bdc5b6 100644 --- a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java +++ b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java @@ -10,7 +10,7 @@ import io.kafbat.ui.model.SortOrderDTO; import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.util.ApplicationMetrics; -import io.kafbat.ui.util.SslPropertiesUtil; +import io.kafbat.ui.util.KafkaClientSslPropertiesUtil; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -264,7 +264,7 @@ public EnhancedConsumer createConsumer(KafkaCluster cluster) { public EnhancedConsumer createConsumer(KafkaCluster cluster, Map properties) { Properties props = new Properties(); - SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props); + KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), props); props.putAll(cluster.getProperties()); props.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafbat-ui-consumer-" + System.currentTimeMillis()); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); diff --git a/api/src/main/java/io/kafbat/ui/service/MessagesService.java b/api/src/main/java/io/kafbat/ui/service/MessagesService.java index 2f6192e11..c94472d56 100644 --- a/api/src/main/java/io/kafbat/ui/service/MessagesService.java +++ b/api/src/main/java/io/kafbat/ui/service/MessagesService.java @@ -23,7 +23,7 @@ import io.kafbat.ui.model.TopicMessageEventDTO; import io.kafbat.ui.serdes.ConsumerRecordDeserializer; import io.kafbat.ui.serdes.ProducerRecordCreator; -import io.kafbat.ui.util.SslPropertiesUtil; +import io.kafbat.ui.util.KafkaClientSslPropertiesUtil; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -199,7 +199,7 @@ private Mono sendMessageImpl(KafkaCluster cluster, public static KafkaProducer createProducer(KafkaCluster cluster, Map additionalProps) { Properties properties = new Properties(); - SslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties(cluster.getOriginalProperties().getSsl(), properties); properties.putAll(cluster.getProperties()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); diff --git a/api/src/main/java/io/kafbat/ui/service/ksql/KsqlApiClient.java b/api/src/main/java/io/kafbat/ui/service/ksql/KsqlApiClient.java index 3a0b46c81..90192eb2d 100644 --- a/api/src/main/java/io/kafbat/ui/service/ksql/KsqlApiClient.java +++ b/api/src/main/java/io/kafbat/ui/service/ksql/KsqlApiClient.java @@ -130,8 +130,8 @@ private Flux executeSelect(String ksql, Map s * Some version of ksqldb (?..0.24) can cut off json streaming without respect proper array ending like

* [{"header":{"queryId":"...","schema":"..."}}, ] * which will cause json parsing error and will be propagated to UI. - * This is a know issue(https://github.com/confluentinc/ksql/issues/8746), but we don't know when it will be fixed. - * To workaround this we need to check DecodingException err msg. + * This is a known issue(...), but we don't know when it will be fixed. + * To work around this we need to check DecodingException err msg. */ private boolean isUnexpectedJsonArrayEndCharException(Throwable th) { return th instanceof DecodingException diff --git a/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java b/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java new file mode 100644 index 000000000..324e2e4d0 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/util/KafkaClientSslPropertiesUtil.java @@ -0,0 +1,35 @@ +package io.kafbat.ui.util; + +import io.kafbat.ui.config.ClustersProperties; +import java.util.Properties; +import javax.annotation.Nullable; +import org.apache.kafka.common.config.SslConfigs; + +public final class KafkaClientSslPropertiesUtil { + + private KafkaClientSslPropertiesUtil() { + } + + public static void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, + Properties sink) { + if (truststoreConfig == null) { + return; + } + + if (!truststoreConfig.isVerifySsl()) { + sink.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + } + + if (truststoreConfig.getTruststoreLocation() == null) { + return; + } + + sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation()); + + if (truststoreConfig.getTruststorePassword() != null) { + sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword()); + } + + } + +} diff --git a/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java b/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java index 629d0f339..397fa3839 100644 --- a/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java +++ b/api/src/main/java/io/kafbat/ui/util/KafkaServicesValidation.java @@ -65,7 +65,7 @@ public static Mono validateClusterConnection(S @Nullable TruststoreConfig ssl) { Properties properties = new Properties(); - SslPropertiesUtil.addKafkaSslProperties(ssl, properties); + KafkaClientSslPropertiesUtil.addKafkaSslProperties(ssl, properties); properties.putAll(clusterProps); properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); // editing properties to make validation faster diff --git a/api/src/main/java/io/kafbat/ui/util/SslPropertiesUtil.java b/api/src/main/java/io/kafbat/ui/util/SslPropertiesUtil.java deleted file mode 100644 index fda959a2b..000000000 --- a/api/src/main/java/io/kafbat/ui/util/SslPropertiesUtil.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.kafbat.ui.util; - -import io.kafbat.ui.config.ClustersProperties; -import java.util.Properties; -import javax.annotation.Nullable; -import org.apache.kafka.common.config.SslConfigs; - -public final class SslPropertiesUtil { - - private SslPropertiesUtil() { - } - - public static void addKafkaSslProperties(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, - Properties sink) { - if (truststoreConfig != null && truststoreConfig.getTruststoreLocation() != null) { - sink.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreConfig.getTruststoreLocation()); - if (truststoreConfig.getTruststorePassword() != null) { - sink.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststoreConfig.getTruststorePassword()); - } - } - } - -} diff --git a/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java new file mode 100644 index 000000000..1b74bd374 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/util/StaticFileWebFilter.java @@ -0,0 +1,61 @@ +package io.kafbat.ui.util; + +import java.io.IOException; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +public class StaticFileWebFilter implements WebFilter { + + private static final String INDEX_HTML = "/static/index.html"; + + private final ServerWebExchangeMatcher matcher; + private final String contents; + + public StaticFileWebFilter() { + this("/login", new ClassPathResource(INDEX_HTML)); + } + + public StaticFileWebFilter(String path, ClassPathResource resource) { + this.matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, path); + + try { + this.contents = ResourceUtil.readAsString(resource); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public @NotNull Mono filter(@NotNull ServerWebExchange exchange, WebFilterChain chain) { + return this.matcher.matches(exchange) + .filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .flatMap((matchResult) -> this.render(exchange)); + } + + private Mono render(ServerWebExchange exchange) { + String contextPath = exchange.getRequest().getPath().contextPath().value(); + + String contentBody = contents + .replace("\"assets/", "\"" + contextPath + "/assets/") + .replace("PUBLIC-PATH-VARIABLE", contextPath); + + ServerHttpResponse result = exchange.getResponse(); + result.setStatusCode(HttpStatus.OK); + result.getHeaders().setContentType(MediaType.TEXT_HTML); + DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); + return result.writeWith(Mono.just(bufferFactory.wrap(contentBody.getBytes()))); + } + +} diff --git a/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java b/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java index 5d364f6dc..1c289f54f 100644 --- a/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java +++ b/api/src/main/java/io/kafbat/ui/util/WebClientConfigurator.java @@ -7,6 +7,7 @@ import io.kafbat.ui.exception.ValidationException; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import java.io.FileInputStream; import java.security.KeyStore; import java.util.function.Consumer; @@ -45,6 +46,10 @@ private static ObjectMapper defaultOM() { public WebClientConfigurator configureSsl(@Nullable ClustersProperties.TruststoreConfig truststoreConfig, @Nullable ClustersProperties.KeystoreConfig keystoreConfig) { + if (truststoreConfig != null && !truststoreConfig.isVerifySsl()) { + return configureNoSsl(); + } + return configureSsl( keystoreConfig != null ? keystoreConfig.getKeystoreLocation() : null, keystoreConfig != null ? keystoreConfig.getKeystorePassword() : null, @@ -97,6 +102,17 @@ private WebClientConfigurator configureSsl( return this; } + @SneakyThrows + public WebClientConfigurator configureNoSsl() { + var contextBuilder = SslContextBuilder.forClient(); + contextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + + SslContext context = contextBuilder.build(); + + httpClient = httpClient.secure(t -> t.sslContext(context)); + return this; + } + public WebClientConfigurator configureBasicAuth(@Nullable String username, @Nullable String password) { if (username != null && password != null) { builder.defaultHeaders(httpHeaders -> httpHeaders.setBasicAuth(username, password)); diff --git a/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java b/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java index 554387a1a..7224649b1 100644 --- a/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java +++ b/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java @@ -5,6 +5,7 @@ import io.kafbat.ui.container.KafkaConnectContainer; import io.kafbat.ui.container.KsqlDbContainer; import io.kafbat.ui.container.SchemaRegistryContainer; +import java.io.FileNotFoundException; import java.nio.file.Path; import java.util.List; import java.util.Properties; @@ -22,6 +23,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.util.TestSocketUtils; +import org.springframework.util.ResourceUtils; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -38,7 +40,7 @@ public abstract class AbstractIntegrationTest { private static final boolean IS_ARM = System.getProperty("os.arch").contains("arm") || System.getProperty("os.arch").contains("aarch64"); - private static final String CONFLUENT_PLATFORM_VERSION = IS_ARM ? "7.2.1.arm64" : "7.2.1"; + private static final String CONFLUENT_PLATFORM_VERSION = IS_ARM ? "7.8.0.arm64" : "7.8.0"; public static final KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka").withTag(CONFLUENT_PLATFORM_VERSION)) @@ -75,6 +77,18 @@ public static class Initializer public void initialize(@NotNull ConfigurableApplicationContext context) { System.setProperty("kafka.clusters.0.name", LOCAL); System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers()); + + // Add ProtobufFileSerde configuration + System.setProperty("kafka.clusters.0.serde.0.name", "ProtobufFile"); + System.setProperty("kafka.clusters.0.serde.0.topicValuesPattern", "masking-test-.*"); + try { + System.setProperty("kafka.clusters.0.serde.0.properties.protobufFilesDir", + ResourceUtils.getFile("classpath:protobuf-serde").getAbsolutePath()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + System.setProperty("kafka.clusters.0.serde.0.properties.protobufMessageName", "test.MessageWithAny"); + // List unavailable hosts to verify failover System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s", diff --git a/api/src/test/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerdeTest.java b/api/src/test/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerdeTest.java index 069f9ed45..becccafa6 100644 --- a/api/src/test/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerdeTest.java +++ b/api/src/test/java/io/kafbat/ui/serdes/builtin/ProtobufFileSerdeTest.java @@ -80,14 +80,15 @@ void setUp() throws Exception { void loadsAllProtoFiledFromTargetDirectory() throws Exception { var protoDir = ResourceUtils.getFile("classpath:protobuf-serde/").getPath(); List files = new ProtobufFileSerde.ProtoSchemaLoader(protoDir).load(); - assertThat(files).hasSize(4); + assertThat(files).hasSize(5); assertThat(files) .map(f -> f.getLocation().getPath()) .containsExactlyInAnyOrder( "language/language.proto", "sensor.proto", "address-book.proto", - "lang-description.proto" + "lang-description.proto", + "messagewithany.proto" ); } diff --git a/api/src/test/java/io/kafbat/ui/service/MessagesServiceTest.java b/api/src/test/java/io/kafbat/ui/service/MessagesServiceTest.java index 849daefd6..1fecae247 100644 --- a/api/src/test/java/io/kafbat/ui/service/MessagesServiceTest.java +++ b/api/src/test/java/io/kafbat/ui/service/MessagesServiceTest.java @@ -13,6 +13,7 @@ import io.kafbat.ui.model.TopicMessageDTO; import io.kafbat.ui.model.TopicMessageEventDTO; import io.kafbat.ui.producer.KafkaTestProducer; +import io.kafbat.ui.serdes.builtin.ProtobufFileSerde; import io.kafbat.ui.serdes.builtin.StringSerde; import java.util.HashSet; import java.util.List; @@ -214,4 +215,33 @@ void execSmartFilterTestReturnsErrorOnFilterCompilationError() { assertThat(result.getError()).containsIgnoringCase("Compilation error"); } + @Test + void sendMessageWithProtobufAnyType() { + String jsonContent = """ + { + "name": "testName", + "payload": { + "@type": "type.googleapis.com/test.PayloadMessage", + "id": "123" + } + } + """; + + CreateTopicMessageDTO testMessage = new CreateTopicMessageDTO() + .key(null) + .partition(0) + .keySerde(StringSerde.name()) + .content(jsonContent) + .valueSerde(ProtobufFileSerde.name()); + + String testTopic = MASKED_TOPICS_PREFIX + UUID.randomUUID(); + createTopicWithCleanup(new NewTopic(testTopic, 5, (short) 1)); + + StepVerifier.create(messagesService.sendMessage(cluster, testTopic, testMessage)) + .expectNextMatches(metadata -> metadata.topic().equals(testTopic) + && metadata.partition() == 0 + && metadata.offset() >= 0) + .verifyComplete(); + } + } diff --git a/api/src/test/resources/protobuf-serde/messagewithany.proto b/api/src/test/resources/protobuf-serde/messagewithany.proto new file mode 100644 index 000000000..5a4b0dd64 --- /dev/null +++ b/api/src/test/resources/protobuf-serde/messagewithany.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package test; + +import "google/protobuf/any.proto"; + +message MessageWithAny { + string name = 1; + google.protobuf.Any payload = 2; +} + +message PayloadMessage { + string id = 1; +} diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 04cc17514..315c4a17e 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -31,7 +31,6 @@ paths: items: $ref: '#/components/schemas/Cluster' - /api/clusters/{clusterName}/cache: post: tags: @@ -54,7 +53,6 @@ paths: 404: description: Not found - /api/clusters/{clusterName}/brokers: get: tags: @@ -432,7 +430,6 @@ paths: 404: description: Not found - /api/clusters/{clusterName}/topics/{topicName}: get: tags: @@ -2150,7 +2147,7 @@ paths: get: tags: - Authorization - summary: Get user authentication related info + summary: Get user authorization related info operationId: getUserAuthInfo responses: 200: @@ -2220,7 +2217,6 @@ paths: schema: $ref: '#/components/schemas/ApplicationConfigValidation' - /api/config/relatedfiles: post: tags: @@ -2244,6 +2240,43 @@ paths: schema: $ref: '#/components/schemas/UploadedFileInfo' + /api/config/authentication: + get: + tags: + - ApplicationConfig + summary: Get authentication methods enabled for the app and other related settings + operationId: getAuthenticationSettings + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AppAuthenticationSettings' + + /login: + post: + tags: + - Unmapped + summary: Authenticate + operationId: authenticate + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + username: + type: string + password: + type: string + responses: + '200': + description: OK + '401': + description: Unauthorized + components: schemas: TopicSerdeSuggestion: @@ -2354,6 +2387,32 @@ components: htmlUrl: type: string + AppAuthenticationSettings: + type: object + properties: + authType: + $ref: '#/components/schemas/AuthType' + oAuthProviders: + type: array + items: + $ref: '#/components/schemas/OAuthProvider' + + OAuthProvider: + type: object + properties: + clientName: + type: string + authorizationUri: + type: string + + AuthType: + type: string + enum: + - DISABLED + - OAUTH2 + - LOGIN_FORM + - LDAP + Cluster: type: object properties: @@ -4183,6 +4242,10 @@ components: type: string truststorePassword: type: string + verifySsl: + type: boolean + description: Skip SSL verification for the host. + default: true schemaRegistry: type: string schemaRegistryAuth: diff --git a/documentation/compose/e2e-tests.yaml b/documentation/compose/e2e-tests.yaml index da986cc39..e18eb7a16 100644 --- a/documentation/compose/e2e-tests.yaml +++ b/documentation/compose/e2e-tests.yaml @@ -29,7 +29,7 @@ services: KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb:8088 kafka0: - image: confluentinc/cp-kafka:7.6.0 + image: confluentinc/cp-kafka:7.8.0 user: "0:0" hostname: kafka0 container_name: kafka0 @@ -62,7 +62,7 @@ services: CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' schemaregistry0: - image: confluentinc/cp-schema-registry:7.6.0 + image: confluentinc/cp-schema-registry:7.8.0 ports: - 8085:8085 depends_on: @@ -87,7 +87,7 @@ services: build: context: ./kafka-connect args: - image: confluentinc/cp-kafka-connect:7.6.0 + image: confluentinc/cp-kafka-connect:7.8.0 ports: - 8083:8083 depends_on: @@ -121,7 +121,7 @@ services: # AWS_SECRET_ACCESS_KEY: "" kafka-init-topics: - image: confluentinc/cp-kafka:7.6.0 + image: confluentinc/cp-kafka:7.8.0 volumes: - ./data/message.json:/data/message.json depends_on: @@ -161,7 +161,7 @@ services: command: sh -c '/connectors/start.sh' ksqldb: - image: confluentinc/cp-ksqldb-server:7.6.0 + image: confluentinc/cp-ksqldb-server:7.8.0 healthcheck: test: [ "CMD", "timeout", "1", "curl", "--silent", "--fail", "http://localhost:8088/info" ] interval: 30s diff --git a/e2e-tests/pom.xml b/e2e-tests/pom.xml index 8131dd11d..33db9404d 100644 --- a/e2e-tests/pom.xml +++ b/e2e-tests/pom.xml @@ -12,7 +12,6 @@ e2e-tests - 3.8.0 ${project.version} 21 3.5.1 @@ -25,7 +24,7 @@ org.apache.kafka kafka_2.13 - ${kafka.version} + ${confluent.version}-ccs io.kafbat.ui diff --git a/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/NaviSideBar.java b/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/NaviSideBar.java index 6972f379d..b5dc2be8d 100644 --- a/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/NaviSideBar.java +++ b/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/NaviSideBar.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.openqa.selenium.By; public class NaviSideBar extends BasePage { @@ -45,7 +46,7 @@ public String getPagePath(MenuItem menuItem) { @Step public NaviSideBar openSideMenu(String clusterName, MenuItem menuItem) { WebUtil.clickByActions(expandCluster(clusterName).parent() - .$x(String.format(sideMenuOptionElementLocator, menuItem.getNaviTitle()))); + .find(By.linkText(menuItem.getNaviTitle()))); return this; } diff --git a/e2e-tests/src/main/java/io/kafbat/ui/screens/topics/TopicSettingsTab.java b/e2e-tests/src/main/java/io/kafbat/ui/screens/topics/TopicSettingsTab.java index ef32fe352..75e3fe8e7 100644 --- a/e2e-tests/src/main/java/io/kafbat/ui/screens/topics/TopicSettingsTab.java +++ b/e2e-tests/src/main/java/io/kafbat/ui/screens/topics/TopicSettingsTab.java @@ -9,10 +9,13 @@ import io.qameta.allure.Step; import java.util.ArrayList; import java.util.List; +import java.util.NoSuchElementException; public class TopicSettingsTab extends BasePage { protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']"); + protected SelenideElement nextButton = $x("//button[contains(text(), 'Next')]"); + protected SelenideElement previousButton = $x("//button[contains(text(), 'Previous')]"); @Step public TopicSettingsTab waitUntilScreenReady() { @@ -36,7 +39,25 @@ private TopicSettingsTab.SettingsGridItem getItemByKey(String key) { @Step public String getValueByKey(String key) { - return getItemByKey(key).getValue(); + while (true) { + try { + String value = getItemByKey(key).getValue(); + resetPageNavigation(); + return value; + } catch (NoSuchElementException e) { + if (nextButton.isEnabled()) { + nextButton.click(); + } else { + throw e; + } + } + } + } + + private void resetPageNavigation() { + while (previousButton.isEnabled()) { + previousButton.click(); + } } public static class SettingsGridItem extends BasePage { diff --git a/frontend/package.json b/frontend/package.json index 47e2cfa2e..249c91a8f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "ajv": "8.8.2", "ajv-formats": "2.1.1", "json-schema-faker": "0.5.6", - "jsonpath-plus": "8.1.0", + "jsonpath-plus": "10.0.7", "lossless-json": "2.0.11", "pretty-ms": "7.0.1", "react": "18.2.0", @@ -96,15 +96,15 @@ "ts-node": "10.9.2", "ts-prune": "0.10.3", "typescript": "5.3.3", - "vite": "5.2.10", + "vite": "5.2.14", "vite-plugin-checker": "0.6.4", "vite-plugin-ejs": "1.7.0", "vite-tsconfig-paths": "4.3.2", "whatwg-fetch": "3.6.20" }, "engines": { - "node": "v18.17.1", - "pnpm": "v9.11.0" + "node": "18.17.1", + "pnpm": "9.15.0" }, "pnpm": { "overrides": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4474bab46..29518a5cf 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: 0.5.6 version: 0.5.6 jsonpath-plus: - specifier: 8.1.0 - version: 8.1.0 + specifier: 10.0.7 + version: 10.0.7 lossless-json: specifier: 2.0.11 version: 2.0.11 @@ -170,7 +170,7 @@ importers: version: 6.21.0(eslint@8.57.0)(typescript@5.3.3) '@vitejs/plugin-react-swc': specifier: 3.6.0 - version: 3.6.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) + version: 3.6.0(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)) dotenv: specifier: 16.4.5 version: 16.4.5 @@ -179,10 +179,10 @@ importers: version: 8.57.0 eslint-config-airbnb: specifier: 19.0.4 - version: 19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0) + version: 19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0) eslint-config-airbnb-typescript: specifier: 18.0.0 - version: 18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + version: 18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-prettier: specifier: 9.1.0 version: 9.1.0(eslint@8.57.0) @@ -244,17 +244,17 @@ importers: specifier: 5.3.3 version: 5.3.3 vite: - specifier: 5.2.10 - version: 5.2.10(@types/node@20.11.17)(sass@1.66.1) + specifier: 5.2.14 + version: 5.2.14(@types/node@20.11.17)(sass@1.66.1) vite-plugin-checker: specifier: 0.6.4 - version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)) vite-plugin-ejs: specifier: 1.7.0 - version: 1.7.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) + version: 1.7.0(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)) vite-tsconfig-paths: specifier: 4.3.2 - version: 4.3.2(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) + version: 4.3.2(typescript@5.3.3)(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)) whatwg-fetch: specifier: 3.6.20 version: 3.6.20 @@ -667,6 +667,7 @@ packages: '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -674,6 +675,7 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -790,6 +792,18 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -868,83 +882,98 @@ packages: resolution: {integrity: sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==} engines: {node: '>=14.0.0'} - '@rollup/rollup-android-arm-eabi@4.16.1': - resolution: {integrity: sha512-92/y0TqNLRYOTXpm6Z7mnpvKAG9P7qmK7yJeRJSdzElNCUnsgbpAsGqerUboYRIQKzgfq4pWu9xVkgpWLfmNsw==} + '@rollup/rollup-android-arm-eabi@4.29.1': + resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.16.1': - resolution: {integrity: sha512-ttWB6ZCfRLuDIUiE0yiu5gcqOsYjA5F7kEV1ggHMj20FwLZ8A1FMeahZJFl/pnOmcnD2QL0z4AcDuo27utGU8A==} + '@rollup/rollup-android-arm64@4.29.1': + resolution: {integrity: sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.16.1': - resolution: {integrity: sha512-QLDvPLetbqjHojTGFw9+nuSP3YY/iz2k1cep6crYlr97sS+ZJ0W43b8Z0zC00+lnFZj6JSNxiA4DjboNQMuh1A==} + '@rollup/rollup-darwin-arm64@4.29.1': + resolution: {integrity: sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.16.1': - resolution: {integrity: sha512-TAUK/D8khRrRIa1KwRzo8JNKk3tcqaeXWdtsiLgA8zmACWwlWLjPCJ4DULGHQrMkeBjp1Cd3Yuwx04lZgFx5Vg==} + '@rollup/rollup-darwin-x64@4.29.1': + resolution: {integrity: sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.16.1': - resolution: {integrity: sha512-KO+WGZjrh6zyFTD1alIFkfdtxf8B4BC+hqd3kBZHscPLvE5FR/6QKsyuCT0JlERxxYBSUKNUQ/UHyX5uwO1x2A==} + '@rollup/rollup-freebsd-arm64@4.29.1': + resolution: {integrity: sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.29.1': + resolution: {integrity: sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.29.1': + resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.16.1': - resolution: {integrity: sha512-NqxbllzIB1WoAo4ThUXVtd21iiM5IHMTTXmXySKBLVcZvkU0HIZmatlP7hLzb5yQubcmdIeWmncd2NdsjocEiw==} + '@rollup/rollup-linux-arm-musleabihf@4.29.1': + resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.16.1': - resolution: {integrity: sha512-snma5NvV8y7IECQ5rq0sr0f3UUu+92NVmG/913JXJMcXo84h9ak9TA5UI9Cl2XRM9j3m37QwDBtEYnJzRkSmxA==} + '@rollup/rollup-linux-arm64-gnu@4.29.1': + resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.16.1': - resolution: {integrity: sha512-KOvqGprlD84ueivhCi2flvcUwDRD20mAsE3vxQNVEI2Di9tnPGAfEu6UcrSPZbM+jG2w1oSr43hrPo0RNg6GGg==} + '@rollup/rollup-linux-arm64-musl@4.29.1': + resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.16.1': - resolution: {integrity: sha512-/gsNwtiGLqYwN4vP+EIdUC6Q6LTlpupWqokqIndvZcjn9ig/5P01WyaYCU2wvfL/2Z82jp5kX8c1mDBOvCP3zg==} + '@rollup/rollup-linux-loongarch64-gnu@4.29.1': + resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': + resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.16.1': - resolution: {integrity: sha512-uU8zuGkQfGqfD9w6VRJZI4IuG4JIfNxxJgEmLMAmPVHREKGsxFVfgHy5c6CexQF2vOfgjB33OsET3Vdn2lln9A==} + '@rollup/rollup-linux-riscv64-gnu@4.29.1': + resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.16.1': - resolution: {integrity: sha512-lsjLtDgtcGFEuBP6yrXwkRN5/wKlvUZtfbKZZu0yaoNpiBL4epgnO21osAALIspVRnl4qZgyLFd8xjCYYWgwfw==} + '@rollup/rollup-linux-s390x-gnu@4.29.1': + resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.16.1': - resolution: {integrity: sha512-N2ZizKhUryqqrMfdCnjhJhZRgv61C6gK+hwVtCIKC8ts8J+go+vqENnGexwg21nHIOvLN5mBM8a7DI2vlyIOPg==} + '@rollup/rollup-linux-x64-gnu@4.29.1': + resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.16.1': - resolution: {integrity: sha512-5ICeMxqg66FrOA2AbnBQ2TJVxfvZsKLxmof0ibvPLaYtbsJqnTUtJOofgWb46Gjd4uZcA4rdsp4JCxegzQPqCg==} + '@rollup/rollup-linux-x64-musl@4.29.1': + resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.16.1': - resolution: {integrity: sha512-1vIP6Ce02L+qWD7uZYRiFiuAJo3m9kARatWmFSnss0gZnVj2Id7OPUU9gm49JPGasgcR3xMqiH3fqBJ8t00yVg==} + '@rollup/rollup-win32-arm64-msvc@4.29.1': + resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.16.1': - resolution: {integrity: sha512-Y3M92DcVsT6LoP+wrKpoUWPaazaP1fzbNkp0a0ZSj5Y//+pQVfVe/tQdsYQQy7dwXR30ZfALUIc9PCh9Izir6w==} + '@rollup/rollup-win32-ia32-msvc@4.29.1': + resolution: {integrity: sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.16.1': - resolution: {integrity: sha512-x0fvpHMuF7fK5r8oZxSi8VYXkrVmRgubXpO/wcf15Lk3xZ4Jvvh5oG+u7Su1776A7XzVKZhD2eRc4t7H50gL3w==} + '@rollup/rollup-win32-x64-msvc@4.29.1': + resolution: {integrity: sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==} cpu: [x64] os: [win32] @@ -1157,8 +1186,8 @@ packages: '@types/babel__traverse@7.17.1': resolution: {integrity: sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/eventsource@1.1.15': resolution: {integrity: sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==} @@ -1723,8 +1752,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} css-color-keywords@1.0.0: @@ -2109,6 +2138,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -2321,6 +2351,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -2471,6 +2502,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2870,6 +2902,10 @@ packages: canvas: optional: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -2910,15 +2946,15 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonpath-plus@10.0.7: + resolution: {integrity: sha512-GDA8d8fu9+s4QzAzo5LMGiLL/9YjecAX+ytlnqdeXYpU55qME57StDgaHt9R2pA7Dr8U31nwzxNJMJiHkrkRgw==} + engines: {node: '>=18.0.0'} + hasBin: true + jsonpath-plus@7.2.0: resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==} engines: {node: '>=12.0.0'} - jsonpath-plus@8.1.0: - resolution: {integrity: sha512-qVTiuKztFGw0dGhYi3WNqvddx3/SHtyDT0xJaeyz4uP0d1tkpG+0y5uYQ4OcIo1TLAz3PE/qDOW9F0uDt3+CTw==} - engines: {node: '>=14.0.0'} - hasBin: true - jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3012,8 +3048,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mime-db@1.52.0: @@ -3064,8 +3100,8 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3491,6 +3527,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@5.0.5: @@ -3498,8 +3535,8 @@ packages: engines: {node: '>=14'} hasBin: true - rollup@4.16.1: - resolution: {integrity: sha512-5CaD3MPDlPKfhqzRvWXK96G6ELJfPZNb3LHiZxTHgDdC6jvwfGz2E8nY+9g1ONk4ttHsK1WaFP19Js4PSr1E3g==} + rollup@4.29.1: + resolution: {integrity: sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -4009,8 +4046,8 @@ packages: vite: optional: true - vite@5.2.10: - resolution: {integrity: sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==} + vite@5.2.14: + resolution: {integrity: sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -4152,12 +4189,12 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.8.0: - resolution: {integrity: sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true @@ -4677,7 +4714,7 @@ snapshots: jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 @@ -4799,7 +4836,7 @@ snapshots: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -4842,6 +4879,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@lukeed/csprng@1.1.0': {} '@microsoft/fetch-event-source@2.0.1': {} @@ -4931,52 +4976,61 @@ snapshots: '@remix-run/router@1.16.0': {} - '@rollup/rollup-android-arm-eabi@4.16.1': + '@rollup/rollup-android-arm-eabi@4.29.1': + optional: true + + '@rollup/rollup-android-arm64@4.29.1': optional: true - '@rollup/rollup-android-arm64@4.16.1': + '@rollup/rollup-darwin-arm64@4.29.1': optional: true - '@rollup/rollup-darwin-arm64@4.16.1': + '@rollup/rollup-darwin-x64@4.29.1': optional: true - '@rollup/rollup-darwin-x64@4.16.1': + '@rollup/rollup-freebsd-arm64@4.29.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.16.1': + '@rollup/rollup-freebsd-x64@4.29.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.16.1': + '@rollup/rollup-linux-arm-gnueabihf@4.29.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.16.1': + '@rollup/rollup-linux-arm-musleabihf@4.29.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.16.1': + '@rollup/rollup-linux-arm64-gnu@4.29.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.16.1': + '@rollup/rollup-linux-arm64-musl@4.29.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.16.1': + '@rollup/rollup-linux-loongarch64-gnu@4.29.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.16.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.16.1': + '@rollup/rollup-linux-riscv64-gnu@4.29.1': optional: true - '@rollup/rollup-linux-x64-musl@4.16.1': + '@rollup/rollup-linux-s390x-gnu@4.29.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.16.1': + '@rollup/rollup-linux-x64-gnu@4.29.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.16.1': + '@rollup/rollup-linux-x64-musl@4.29.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.16.1': + '@rollup/rollup-win32-arm64-msvc@4.29.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.29.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.29.1': optional: true '@sinclair/typebox@0.27.8': {} @@ -5175,7 +5229,7 @@ snapshots: dependencies: '@babel/types': 7.23.9 - '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} '@types/eventsource@1.1.15': {} @@ -5366,10 +5420,10 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.6.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1))': + '@vitejs/plugin-react-swc@3.6.0(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1))': dependencies: '@swc/core': 1.3.107 - vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) + vite: 5.2.14(@types/node@20.11.17)(sass@1.66.1) transitivePeerDependencies: - '@swc/helpers' @@ -5848,7 +5902,7 @@ snapshots: create-require@1.1.1: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -6224,7 +6278,7 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 @@ -6233,19 +6287,19 @@ snapshots: object.entries: 1.1.7 semver: 6.3.1 - eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - eslint-plugin-import - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) @@ -6270,7 +6324,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -6282,7 +6336,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -6303,7 +6357,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -6402,7 +6456,7 @@ snapshots: '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 @@ -6455,7 +6509,7 @@ snapshots: execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -6491,7 +6545,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.8 fast-json-stable-stringify@2.1.0: {} @@ -6565,7 +6619,7 @@ snapshots: foreground-child@3.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@4.0.0: @@ -7118,7 +7172,7 @@ snapshots: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -7186,7 +7240,7 @@ snapshots: jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -7210,7 +7264,7 @@ snapshots: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.6.3 slash: 3.0.0 stack-utils: 2.0.5 @@ -7222,7 +7276,7 @@ snapshots: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.5 @@ -7462,13 +7516,15 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.8.0 + ws: 8.18.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + jsep@1.4.0: {} + jsesc@2.5.2: {} json-parse-even-better-errors@2.3.1: {} @@ -7504,9 +7560,13 @@ snapshots: optionalDependencies: graceful-fs: 4.2.10 - jsonpath-plus@7.2.0: {} + jsonpath-plus@10.0.7: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 - jsonpath-plus@8.1.0: {} + jsonpath-plus@7.2.0: {} jsx-ast-utils@3.3.5: dependencies: @@ -7588,7 +7648,7 @@ snapshots: merge2@1.4.1: {} - micromatch@4.0.5: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -7627,7 +7687,7 @@ snapshots: mute-stream@0.0.8: {} - nanoid@3.3.7: {} + nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -7821,13 +7881,13 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.0.0 source-map-js: 1.0.2 postcss@8.4.38: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.0.0 source-map-js: 1.2.0 @@ -8055,26 +8115,29 @@ snapshots: dependencies: glob: 10.3.12 - rollup@4.16.1: + rollup@4.29.1: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.16.1 - '@rollup/rollup-android-arm64': 4.16.1 - '@rollup/rollup-darwin-arm64': 4.16.1 - '@rollup/rollup-darwin-x64': 4.16.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.16.1 - '@rollup/rollup-linux-arm-musleabihf': 4.16.1 - '@rollup/rollup-linux-arm64-gnu': 4.16.1 - '@rollup/rollup-linux-arm64-musl': 4.16.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.16.1 - '@rollup/rollup-linux-riscv64-gnu': 4.16.1 - '@rollup/rollup-linux-s390x-gnu': 4.16.1 - '@rollup/rollup-linux-x64-gnu': 4.16.1 - '@rollup/rollup-linux-x64-musl': 4.16.1 - '@rollup/rollup-win32-arm64-msvc': 4.16.1 - '@rollup/rollup-win32-ia32-msvc': 4.16.1 - '@rollup/rollup-win32-x64-msvc': 4.16.1 + '@rollup/rollup-android-arm-eabi': 4.29.1 + '@rollup/rollup-android-arm64': 4.29.1 + '@rollup/rollup-darwin-arm64': 4.29.1 + '@rollup/rollup-darwin-x64': 4.29.1 + '@rollup/rollup-freebsd-arm64': 4.29.1 + '@rollup/rollup-freebsd-x64': 4.29.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.29.1 + '@rollup/rollup-linux-arm-musleabihf': 4.29.1 + '@rollup/rollup-linux-arm64-gnu': 4.29.1 + '@rollup/rollup-linux-arm64-musl': 4.29.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.29.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.29.1 + '@rollup/rollup-linux-riscv64-gnu': 4.29.1 + '@rollup/rollup-linux-s390x-gnu': 4.29.1 + '@rollup/rollup-linux-x64-gnu': 4.29.1 + '@rollup/rollup-linux-x64-musl': 4.29.1 + '@rollup/rollup-win32-arm64-msvc': 4.29.1 + '@rollup/rollup-win32-ia32-msvc': 4.29.1 + '@rollup/rollup-win32-x64-msvc': 4.29.1 fsevents: 2.3.3 run-async@2.4.1: {} @@ -8583,7 +8646,7 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.3 convert-source-map: 1.7.0 - vite-plugin-checker@0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): + vite-plugin-checker@0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)): dependencies: '@babel/code-frame': 7.23.5 ansi-escapes: 4.3.2 @@ -8596,7 +8659,7 @@ snapshots: semver: 7.5.4 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) + vite: 5.2.14(@types/node@20.11.17)(sass@1.66.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 @@ -8606,27 +8669,27 @@ snapshots: optionator: 0.9.3 typescript: 5.3.3 - vite-plugin-ejs@1.7.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): + vite-plugin-ejs@1.7.0(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)): dependencies: ejs: 3.1.10 - vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) + vite: 5.2.14(@types/node@20.11.17)(sass@1.66.1) - vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): + vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@5.2.14(@types/node@20.11.17)(sass@1.66.1)): dependencies: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.3.3) optionalDependencies: - vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) + vite: 5.2.14(@types/node@20.11.17)(sass@1.66.1) transitivePeerDependencies: - supports-color - typescript - vite@5.2.10(@types/node@20.11.17)(sass@1.66.1): + vite@5.2.14(@types/node@20.11.17)(sass@1.66.1): dependencies: esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.16.1 + rollup: 4.29.1 optionalDependencies: '@types/node': 20.11.17 fsevents: 2.3.3 @@ -8778,7 +8841,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.8.0: {} + ws@8.18.0: {} xml-name-validator@4.0.0: {} diff --git a/frontend/public/serviceImage.png b/frontend/public/serviceImage.png new file mode 100644 index 000000000..8006b13f5 Binary files /dev/null and b/frontend/public/serviceImage.png differ diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 16dd1305d..d15cb95a0 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -1,5 +1,5 @@ import React, { Suspense, useContext } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, useMatch } from 'react-router-dom'; import { accessErrorPage, clusterPath, @@ -24,6 +24,7 @@ import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext'; import { UserInfoRolesAccessProvider } from './contexts/UserInfoRolesAccessContext'; import PageContainer from './PageContainer/PageContainer'; +const AuthPage = React.lazy(() => import('components/AuthPage/AuthPage')); const Dashboard = React.lazy(() => import('components/Dashboard/Dashboard')); const ClusterPage = React.lazy( () => import('components/ClusterPage/ClusterPage') @@ -49,54 +50,59 @@ const queryClient = new QueryClient({ }); const App: React.FC = () => { const { isDarkMode } = useContext(ThemeModeContext); + const isAuthRoute = useMatch('/login'); return ( - - - }> - - - - - - - {['/', '/ui', '/ui/clusters'].map((path) => ( + + {isAuthRoute ? ( + + ) : ( + + }> + + + + + + + {['/', '/ui', '/ui/clusters'].map((path) => ( + } + /> + ))} } + path={getNonExactPath(clusterNewConfigPath)} + element={} /> - ))} - } - /> - } - /> - - } - /> - } /> - } - /> - - - - - - - - - - + } + /> + + } + /> + } /> + } + /> + + + + + + + + + + )} + ); diff --git a/frontend/src/components/AuthPage/AuthPage.styled.tsx b/frontend/src/components/AuthPage/AuthPage.styled.tsx new file mode 100644 index 000000000..16f86f714 --- /dev/null +++ b/frontend/src/components/AuthPage/AuthPage.styled.tsx @@ -0,0 +1,14 @@ +import styled, { css } from 'styled-components'; + +export const AuthPageStyled = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + min-height: 100vh; + background-color: ${theme.auth_page.backgroundColor}; + font-family: ${theme.auth_page.fontFamily}; + overflow-x: hidden; + ` +); diff --git a/frontend/src/components/AuthPage/AuthPage.tsx b/frontend/src/components/AuthPage/AuthPage.tsx new file mode 100644 index 000000000..ceae3069a --- /dev/null +++ b/frontend/src/components/AuthPage/AuthPage.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useAuthSettings } from 'lib/hooks/api/appConfig'; + +import Header from './Header/Header'; +import SignIn from './SignIn/SignIn'; +import * as S from './AuthPage.styled'; + +function AuthPage() { + const { data } = useAuthSettings(); + + return ( + +

+ {data && ( + + )} + + ); +} + +export default AuthPage; diff --git a/frontend/src/components/AuthPage/Header/Header.styled.tsx b/frontend/src/components/AuthPage/Header/Header.styled.tsx new file mode 100644 index 000000000..4ba86f2bc --- /dev/null +++ b/frontend/src/components/AuthPage/Header/Header.styled.tsx @@ -0,0 +1,33 @@ +import styled, { css } from 'styled-components'; + +export const HeaderStyled = styled.div` + display: grid; + grid-template-columns: repeat(47, 41.11px); + grid-template-rows: repeat(4, 41.11px); + justify-content: center; + margin-bottom: 13.5px; +`; + +export const HeaderCell = styled.div<{ $sections?: number }>( + ({ theme, $sections }) => css` + border: 1.23px solid ${theme.auth_page.header.cellBorderColor}; + border-radius: 75.98px; + ${$sections && `grid-column: span ${$sections};`} + ` +); + +export const StyledSVG = styled.svg` + grid-column: span 3; +`; + +export const StyledRect = styled.rect( + ({ theme }) => css` + fill: ${theme.auth_page.header.LogoBgColor}; + ` +); + +export const StyledPath = styled.path( + ({ theme }) => css` + fill: ${theme.auth_page.header.LogoTextColor}; + ` +); diff --git a/frontend/src/components/AuthPage/Header/Header.tsx b/frontend/src/components/AuthPage/Header/Header.tsx new file mode 100644 index 000000000..16980af29 --- /dev/null +++ b/frontend/src/components/AuthPage/Header/Header.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import * as S from './Header.styled'; +import HeaderLogo from './HeaderLogo'; + +function Header() { + return ( + + + {Array(2).fill()} + + {Array(2).fill()} + + {Array(2).fill()} + {Array(4).fill()} + {Array(2).fill()} + + {Array(2).fill()} + + {Array(3).fill()} + + {Array(2).fill()} + {Array(2).fill()} + {Array(2).fill()} + + + {Array(3).fill()} + {Array(8).fill()} + + {Array(2).fill()} + + {Array(3).fill()} + + {Array(6).fill()} + {Array(3).fill()} + + + {Array(2).fill()} + + + + + + + + {Array(2).fill()} + + + + {Array(3).fill()} + + + {Array(3).fill()} + + {Array(3).fill()} + {Array(3).fill()} + + + + + + {Array(2).fill()} + + {Array(2).fill()} + {Array(5).fill()} + {Array(2).fill()} + + + + {Array(5).fill()} + {Array(2).fill()} + + {Array(2).fill()} + + + + + ); +} + +export default Header; diff --git a/frontend/src/components/AuthPage/Header/HeaderLogo.tsx b/frontend/src/components/AuthPage/Header/HeaderLogo.tsx new file mode 100644 index 000000000..e5d9ca12d --- /dev/null +++ b/frontend/src/components/AuthPage/Header/HeaderLogo.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import * as S from './Header.styled'; + +const HeaderLogo = () => ( + + + + + + + + + + +); + +export default HeaderLogo; diff --git a/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx new file mode 100644 index 000000000..da1388b0a --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +export const Fieldset = styled.fieldset` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + border: none; + width: 100%; +`; + +export const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + width: 100%; + + ${Fieldset} div { + width: 100%; + } +`; + +export const Field = styled.div` + ${({ theme }) => theme.auth_page.signIn.label}; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 4px; +`; + +export const Label = styled.label` + font-size: 12px; + font-weight: 500; + line-height: 16px; +`; + +export const ErrorMessage = styled.div` + display: flex; + column-gap: 2px; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; + +export const ErrorMessageText = styled.span` + ${({ theme }) => theme.auth_page.signIn.errorMessage}; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; diff --git a/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx new file mode 100644 index 000000000..044f4781b --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; +import { useAuthenticate } from 'lib/hooks/api/appConfig'; +import AlertIcon from 'components/common/Icons/AlertIcon'; +import { useNavigate } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; + +import * as S from './BasicSignIn.styled'; + +interface FormValues { + username: string; + password: string; +} + +function BasicSignIn() { + const methods = useForm({ + defaultValues: { username: '', password: '' }, + }); + const navigate = useNavigate(); + const { mutateAsync, isLoading } = useAuthenticate(); + const client = useQueryClient(); + + const onSubmit = async (data: FormValues) => { + await mutateAsync(data, { + onSuccess: async (response) => { + if (response.raw.url.includes('error')) { + methods.setError('root', { message: 'error' }); + } else { + await client.invalidateQueries({ queryKey: ['app', 'info'] }); + navigate('/'); + } + }, + }); + }; + + return ( + + + + {methods.formState.errors.root && ( + + + + Username or password entered incorrectly + + + )} + ( + + Username + + + )} + /> + ( + + Password + + + )} + /> + + + + + ); +} + +export default BasicSignIn; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx new file mode 100644 index 000000000..d1eae050f --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx @@ -0,0 +1,66 @@ +import styled, { css } from 'styled-components'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; +import { Button } from 'components/common/Button/Button'; + +export const AuthCardStyled = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + width: 400px; + border: 1px solid black; + border: 1px solid ${theme.auth_page.signIn.authCard.borderColor}; + border-radius: ${theme.auth_page.signIn.authCard.borderRadius}; + background-color: ${theme.auth_page.signIn.authCard.backgroundColor}; + ` +); + +export const ServiceData = styled.div( + ({ theme }) => css` + display: flex; + gap: 8px; + align-items: center; + + svg, + img { + margin: 8px; + width: 48px; + height: 48px; + } + + ${GitHubIcon} { + fill: ${theme.auth_page.icons.githubColor}; + } + ` +); + +export const ServiceDataTextContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const ServiceNameStyled = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.authCard.serviceNamecolor}; + font-size: 16px; + font-weight: 500; + line-height: 24px; + ` +); + +export const ServiceTextStyled = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.authCard.serviceTextColor}; + font-size: 12px; + font-weight: 500; + line-height: 16px; + ` +); + +export const ServiceButton = styled(Button)` + width: 100%; + border-radius: 8px; + font-size: 14px; + text-decoration: none; +`; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx new file mode 100644 index 000000000..b9a09812b --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx @@ -0,0 +1,41 @@ +import React, { ElementType, useState } from 'react'; +import ServiceImage from 'components/common/Icons/ServiceImage'; + +import * as S from './AuthCard.styled'; + +interface Props { + serviceName: string; + authPath: string | undefined; + Icon?: ElementType; +} + +function AuthCard({ serviceName, authPath, Icon = ServiceImage }: Props) { + const [isLoading, setIsLoading] = useState(false); + + return ( + + + + + {serviceName} + + Use an account issued by the organization + + + + { + setIsLoading(true); + window.location.replace(`${window.basePath}${authPath}`); + }} + inProgress={isLoading} + > + {!isLoading && `Log in with ${serviceName}`} + + + ); +} + +export default AuthCard; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx new file mode 100644 index 000000000..bf238e9b2 --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const OAuthSignInStyled = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const ErrorMessage = styled.div` + display: flex; + column-gap: 2px; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 14px; + line-height: 20px; + margin-bottom: 8px; +`; + +export const ErrorMessageText = styled.span` + ${({ theme }) => theme.auth_page.signIn.errorMessage}; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx new file mode 100644 index 000000000..fca5b4925 --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx @@ -0,0 +1,55 @@ +import React, { ElementType } from 'react'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; +import GoogleIcon from 'components/common/Icons/GoogleIcon'; +import CognitoIcon from 'components/common/Icons/CognitoIcon'; +import OktaIcon from 'components/common/Icons/OktaIcon'; +import KeycloakIcon from 'components/common/Icons/KeycloakIcon'; +import ServiceImage from 'components/common/Icons/ServiceImage'; +import { OAuthProvider } from 'generated-sources'; +import { useLocation } from 'react-router-dom'; +import AlertIcon from 'components/common/Icons/AlertIcon'; + +import * as S from './OAuthSignIn.styled'; +import AuthCard from './AuthCard/AuthCard'; + +interface Props { + oAuthProviders: OAuthProvider[] | undefined; +} + +const ServiceIconMap: Record = { + github: GitHubIcon, + google: GoogleIcon, + cognito: CognitoIcon, + keycloak: KeycloakIcon, + okta: OktaIcon, + unknownService: ServiceImage, +}; + +function OAuthSignIn({ oAuthProviders }: Props) { + const { search } = useLocation(); + + return ( + + {search.includes('error') && ( + + + Invalid credentials + + )} + {oAuthProviders?.map((provider) => ( + + ))} + + ); +} + +export default OAuthSignIn; diff --git a/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx new file mode 100644 index 000000000..0f24b45fd --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx @@ -0,0 +1,19 @@ +import styled, { css } from 'styled-components'; + +export const SignInStyled = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 320px; + gap: 56px; + flex-grow: 1; +`; + +export const SignInTitle = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.titleColor}; + font-size: 24px; + font-weight: 600; + ` +); diff --git a/frontend/src/components/AuthPage/SignIn/SignIn.tsx b/frontend/src/components/AuthPage/SignIn/SignIn.tsx new file mode 100644 index 000000000..987ee5ebf --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/SignIn.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { AuthType, OAuthProvider } from 'generated-sources'; + +import BasicSignIn from './BasicSignIn/BasicSignIn'; +import * as S from './SignIn.styled'; +import OAuthSignIn from './OAuthSignIn/OAuthSignIn'; + +interface Props { + authType?: AuthType; + oAuthProviders?: OAuthProvider[]; +} + +function SignInForm({ authType, oAuthProviders }: Props) { + return ( + + Sign in + {(authType === AuthType.LDAP || authType === AuthType.LOGIN_FORM) && ( + + )} + {authType === AuthType.OAUTH2 && ( + + )} + + ); +} + +export default SignInForm; diff --git a/frontend/src/components/NavBar/UserInfo/UserInfo.tsx b/frontend/src/components/NavBar/UserInfo/UserInfo.tsx index dae43364c..b52cc7631 100644 --- a/frontend/src/components/NavBar/UserInfo/UserInfo.tsx +++ b/frontend/src/components/NavBar/UserInfo/UserInfo.tsx @@ -19,7 +19,7 @@ const UserInfo = () => { } > - + Log out diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx index 828b5d301..8964b6e17 100644 --- a/frontend/src/components/common/Button/Button.tsx +++ b/frontend/src/components/common/Button/Button.tsx @@ -9,6 +9,7 @@ export interface Props ButtonProps { to?: string | object; inProgress?: boolean; + className?: string; } export const Button: FC = ({ @@ -20,7 +21,7 @@ export const Button: FC = ({ }) => { if (to) { return ( - + {children} diff --git a/frontend/src/components/common/Icons/AlertIcon.tsx b/frontend/src/components/common/Icons/AlertIcon.tsx new file mode 100644 index 000000000..3c79f78e6 --- /dev/null +++ b/frontend/src/components/common/Icons/AlertIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const AlertIcon: React.FC = () => { + return ( + + + + ); +}; + +export default AlertIcon; diff --git a/frontend/src/components/common/Icons/CognitoIcon.tsx b/frontend/src/components/common/Icons/CognitoIcon.tsx new file mode 100644 index 000000000..2d0b0d38a --- /dev/null +++ b/frontend/src/components/common/Icons/CognitoIcon.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import styled from 'styled-components'; + +function CognitoIcon() { + return ( + + + + + + + + + + + + + + + ); +} + +export default styled(CognitoIcon)``; diff --git a/frontend/src/components/common/Icons/GoogleIcon.tsx b/frontend/src/components/common/Icons/GoogleIcon.tsx new file mode 100644 index 000000000..2e569dbfe --- /dev/null +++ b/frontend/src/components/common/Icons/GoogleIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; + +function GoogleIcon() { + return ( + + + + + + + ); +} + +export default styled(GoogleIcon)``; diff --git a/frontend/src/components/common/Icons/KeycloakIcon.tsx b/frontend/src/components/common/Icons/KeycloakIcon.tsx new file mode 100644 index 000000000..e6b45ef69 --- /dev/null +++ b/frontend/src/components/common/Icons/KeycloakIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styled from 'styled-components'; + +function KeycloakIcon() { + return ( + + + + + ); +} + +export default styled(KeycloakIcon)``; diff --git a/frontend/src/components/common/Icons/OktaIcon.tsx b/frontend/src/components/common/Icons/OktaIcon.tsx new file mode 100644 index 000000000..a9d6871b0 --- /dev/null +++ b/frontend/src/components/common/Icons/OktaIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from 'styled-components'; + +function OktaIcon() { + return ( + + + + ); +} + +export default styled(OktaIcon)``; diff --git a/frontend/src/components/common/Icons/ServiceImage.tsx b/frontend/src/components/common/Icons/ServiceImage.tsx new file mode 100644 index 000000000..9311334f1 --- /dev/null +++ b/frontend/src/components/common/Icons/ServiceImage.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +interface Props { + serviceName: string; +} + +function ServiceImage({ serviceName }: Props) { + return {serviceName}; +} + +export default ServiceImage; diff --git a/frontend/src/components/contexts/GlobalSettingsContext.tsx b/frontend/src/components/contexts/GlobalSettingsContext.tsx index 4de05307b..5e906c292 100644 --- a/frontend/src/components/contexts/GlobalSettingsContext.tsx +++ b/frontend/src/components/contexts/GlobalSettingsContext.tsx @@ -1,6 +1,7 @@ import { useAppInfo } from 'lib/hooks/api/appConfig'; import React from 'react'; import { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources'; +import { useNavigate } from 'react-router-dom'; interface GlobalSettingsContextProps { hasDynamicConfig: boolean; @@ -15,13 +16,26 @@ export const GlobalSettingsProvider: React.FC< React.PropsWithChildren > = ({ children }) => { const info = useAppInfo(); - const value = React.useMemo(() => { - const features = info.data?.enabledFeatures || []; - return { - hasDynamicConfig: features.includes( - ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG - ), - }; + const navigate = useNavigate(); + const [value, setValue] = React.useState({ + hasDynamicConfig: false, + }); + + React.useEffect(() => { + if (info.data?.redirect && !info.isFetching) { + navigate('login'); + return; + } + + const features = info?.data?.response?.enabledFeatures; + + if (features) { + setValue({ + hasDynamicConfig: features.includes( + ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG + ), + }); + } }, [info.data]); return ( diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 19423d2ac..d6f409ea2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,6 +11,7 @@ import { AuthorizationApi, ApplicationConfigApi, AclsApi, + UnmappedApi, } from 'generated-sources'; import { BASE_PARAMS } from 'lib/constants'; @@ -27,3 +28,4 @@ export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); export const authApiClient = new AuthorizationApi(apiClientConf); export const appConfigApiClient = new ApplicationConfigApi(apiClientConf); export const aclApiClient = new AclsApi(apiClientConf); +export const internalApiClient = new UnmappedApi(apiClientConf); diff --git a/frontend/src/lib/hooks/api/appConfig.ts b/frontend/src/lib/hooks/api/appConfig.ts index e3ee0fdcb..a91c6eb4b 100644 --- a/frontend/src/lib/hooks/api/appConfig.ts +++ b/frontend/src/lib/hooks/api/appConfig.ts @@ -1,21 +1,52 @@ -import { appConfigApiClient as api } from 'lib/api'; +import { + appConfigApiClient as appConfig, + internalApiClient as internalApi, +} from 'lib/api'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApplicationConfig, ApplicationConfigPropertiesKafkaClusters, + ApplicationInfo, } from 'generated-sources'; import { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants'; -export function useAppInfo() { +export function useAuthSettings() { return useQuery( - ['app', 'info'], - () => api.getApplicationInfo(), + ['app', 'authSettings'], + () => appConfig.getAuthenticationSettings(), QUERY_REFETCH_OFF_OPTIONS ); } +export function useAuthenticate() { + return useMutation({ + mutationFn: (params: { username: string; password: string }) => + internalApi.authenticateRaw(params, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }), + }); +} + +export function useAppInfo() { + return useQuery(['app', 'info'], async () => { + const data = await appConfig.getApplicationInfoRaw(); + + let response: ApplicationInfo = {}; + try { + response = await data.value(); + } catch { + response = {}; + } + + return { + redirect: data.raw.url.includes('auth'), + response, + }; + }); +} + export function useAppConfig() { - return useQuery(['app', 'config'], () => api.getCurrentConfig()); + return useQuery(['app', 'config'], () => appConfig.getCurrentConfig()); } function aggregateClusters( @@ -47,7 +78,7 @@ export function useUpdateAppConfig({ const client = useQueryClient(); return useMutation( async (cluster: ApplicationConfigPropertiesKafkaClusters) => { - const existingConfig = await api.getCurrentConfig(); + const existingConfig = await appConfig.getCurrentConfig(); const clusters = aggregateClusters( cluster, @@ -63,7 +94,7 @@ export function useUpdateAppConfig({ kafka: { clusters }, }, }; - return api.restartWithConfig({ restartRequest: { config } }); + return appConfig.restartWithConfig({ restartRequest: { config } }); }, { onSuccess: () => client.invalidateQueries(['app', 'config']), @@ -82,7 +113,7 @@ export function useAppConfigFilesUpload() { export function useValidateAppConfig() { return useMutation((config: ApplicationConfigPropertiesKafkaClusters) => - api.validateConfig({ + appConfig.validateConfig({ applicationConfig: { properties: { kafka: { clusters: [config] } } }, }) ); diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index f6cd2bacc..bdfe93271 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -57,6 +57,7 @@ const Colors = { '10': '#FAD1D1', '20': '#F5A3A3', '50': '#E51A1A', + '52': '#E63B19', '55': '#CF1717', '60': '#B81414', }, @@ -79,6 +80,45 @@ const Colors = { const baseTheme = { defaultIconColor: Colors.neutral[50], + auth_page: { + backgroundColor: Colors.brand[0], + fontFamily: 'Inter, sans-serif', + header: { + cellBorderColor: Colors.brand[10], + LogoBgColor: Colors.brand[90], + LogoTextColor: Colors.brand[0], + }, + signIn: { + titleColor: Colors.brand[90], + errorMessage: { + color: Colors.red[52], + }, + label: { + color: Colors.brand[70], + }, + authCard: { + borderRadius: '16px', + borderColor: Colors.brand[10], + backgroundColor: Colors.brand[0], + serviceNamecolor: Colors.brand[90], + serviceTextColor: Colors.brand[50], + }, + }, + footer: { + fontSize: '12px', + span: { + color: Colors.brand[70], + fontWeight: 500, + }, + p: { + color: Colors.brand[50], + fontWeight: 400, + }, + }, + icons: { + githubColor: Colors.brand[90], + }, + }, heading: { h1: { color: Colors.neutral[90], @@ -821,6 +861,38 @@ export type ThemeType = typeof theme; export const darkTheme: ThemeType = { ...baseTheme, + auth_page: { + backgroundColor: Colors.neutral[90], + fontFamily: baseTheme.auth_page.fontFamily, + header: { + cellBorderColor: Colors.brand[80], + LogoBgColor: Colors.brand[0], + LogoTextColor: Colors.brand[90], + }, + signIn: { + ...baseTheme.auth_page.signIn, + titleColor: Colors.brand[0], + label: { + color: Colors.brand[30], + }, + authCard: { + ...baseTheme.auth_page.signIn.authCard, + borderColor: Colors.brand[80], + backgroundColor: Colors.brand[85], + serviceNamecolor: Colors.brand[0], + }, + }, + footer: { + ...baseTheme.auth_page.footer, + span: { + color: Colors.brand[10], + fontWeight: 500, + }, + }, + icons: { + githubColor: Colors.brand[0], + }, + }, logo: { color: '#FDFDFD', }, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3a4e861e9..455ef39ae 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import tsconfigPaths from 'vite-tsconfig-paths'; import { ViteEjsPlugin } from 'vite-plugin-ejs'; import checker from 'vite-plugin-checker'; +import { IncomingMessage } from 'http'; export default defineConfig(({ mode }) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; @@ -87,6 +88,21 @@ export default defineConfig(({ mode }) => { ...defaultConfig.server, open: true, proxy: { + '/login': { + target: isProxy, + changeOrigin: true, + secure: false, + bypass: (req: IncomingMessage) => { + if (req.method === 'GET') { + return req.url; + } + }, + }, + '/logout': { + target: isProxy, + changeOrigin: true, + secure: false, + }, '/api': { target: isProxy, changeOrigin: true, diff --git a/pom.xml b/pom.xml index 7b2a670fb..43427b3f1 100644 --- a/pom.xml +++ b/pom.xml @@ -35,11 +35,10 @@ 3.25.3 1.11.4 1.14.19 - 7.4.4 + 7.8.0 3.1.0 3.0.13 2.14.0 - 3.8.0 1.6.2 1.18.34 3.25.5 @@ -60,7 +59,7 @@ v18.17.1 - v9.11.0 + v9.15.0 0.45.1