From 6ab40399ce890fc10b150a3eb42b722aeb1442ad Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 30 Apr 2025 23:38:15 +0300 Subject: [PATCH 1/5] Configure Axon Framework --- docker-compose.yml | 10 + server/build.gradle | 11 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- server/src/dev/resources/application.yml | 23 ++- .../org/eclipse/openvsx/axon/AxonConfig.java | 192 ++++++++++++++++++ .../axon/ByteaEnforcedPostgresSQLDialect.java | 46 +++++ .../openvsx/search/DatabaseSearchService.java | 9 +- .../openvsx/search/ElasticSearchService.java | 5 +- 8 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java create mode 100644 server/src/main/java/org/eclipse/openvsx/axon/ByteaEnforcedPostgresSQLDialect.java diff --git a/docker-compose.yml b/docker-compose.yml index d1a892f73..eb174bdbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,16 @@ services: profiles: - kibana + axonserver: + image: 'axoniq/axonserver:latest' + environment: + - 'AXONIQ_AXONSERVER_STANDALONE=TRUE' + ports: + - '8024:8024' + - '8124:8124' + profiles: + - axon + server: image: openjdk:17 working_dir: /app diff --git a/server/build.gradle b/server/build.gradle index c123c1b3a..b4655ac37 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -11,7 +11,7 @@ plugins { id 'jacoco' id 'nu.studer.jooq' version '8.2.1' id 'de.undercouch.download' version '5.4.0' - id 'org.springframework.boot' version '3.3.10' + id 'org.springframework.boot' version '3.4.5' id 'io.spring.dependency-management' version '1.1.0' id 'io.gatling.gradle' version '3.9.5' id 'java' @@ -37,11 +37,11 @@ def versions = [ tika: '3.1.0', bouncycastle: '1.80', commons_lang3: '3.12.0', - httpclient5: '5.2.1', jaxb_api: '2.3.1', jaxb_impl: '2.3.8', gatling: '3.13.5', - loki4j: '1.4.2' + loki4j: '1.4.2', + axon: '4.11.2' ] ext['junit-jupiter.version'] = versions.junit sourceCompatibility = versions.java @@ -92,6 +92,9 @@ dependencies { implementation "org.ehcache:ehcache:${versions.ehcache}" implementation "com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:${versions.bucket4j}" implementation "org.jobrunr:jobrunr-spring-boot-3-starter:${versions.jobrunr}" + implementation("org.axonframework:axon-spring-boot-starter:${versions.axon}") { + exclude group: 'org.axonframework', module: 'axon-server-connector' + } implementation "org.flywaydb:flyway-core:${versions.flyway}" implementation "com.google.cloud:google-cloud-storage:${versions.gcloud}" implementation "com.azure:azure-storage-blob:${versions.azure}" @@ -106,7 +109,7 @@ dependencies { implementation "javax.xml.bind:jaxb-api:${versions.jaxb_api}" implementation "com.sun.xml.bind:jaxb-impl:${versions.jaxb_impl}" implementation "org.apache.commons:commons-lang3:${versions.commons_lang3}" - implementation "org.apache.httpcomponents.client5:httpclient5:${versions.httpclient5}" + implementation "org.apache.httpcomponents.client5:httpclient5" implementation "org.apache.tika:tika-core:${versions.tika}" implementation "com.github.loki4j:loki-logback-appender:${versions.loki4j}" implementation "io.micrometer:micrometer-tracing" diff --git a/server/gradle/wrapper/gradle-wrapper.properties b/server/gradle/wrapper/gradle-wrapper.properties index fae08049a..5c82cb032 100644 --- a/server/gradle/wrapper/gradle-wrapper.properties +++ b/server/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index ad44dfd76..522396e3a 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -21,9 +21,10 @@ spring: config: classpath:ehcache.xml datasource: url: jdbc:postgresql://localhost:5432/postgres - username: gitpod - password: gitpod + username: openvsx + password: openvsx flyway: + schemas: public baseline-on-migrate: true baseline-version: 0.1.0 baseline-description: JobRunr tables @@ -135,10 +136,28 @@ bucket4j: time: 1 unit: seconds +axon: + serializer: + events: jackson + ovsx: + axon: + datasource: + jdbc-url: jdbc:postgresql://localhost:5432/postgres + username: openvsx + password: openvsx + schema: axon + jpa: + generate-ddl: true + properties: + hibernate: + dialect: org.eclipse.openvsx.axon.ByteaEnforcedPostgresSQLDialect + physical_naming_strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + databasesearch: enabled: false elasticsearch: + enabled: false clear-on-start: true eclipse: base-url: https://api.eclipse.org diff --git a/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java b/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java new file mode 100644 index 000000000..7cd12651f --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java @@ -0,0 +1,192 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.axon; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.axonframework.commandhandling.CommandBus; +import org.axonframework.commandhandling.DuplicateCommandHandlerResolver; +import org.axonframework.commandhandling.SimpleCommandBus; +import org.axonframework.common.jdbc.PersistenceExceptionResolver; +import org.axonframework.common.jpa.EntityManagerProvider; +import org.axonframework.common.jpa.SimpleEntityManagerProvider; +import org.axonframework.common.transaction.TransactionManager; +import org.axonframework.eventsourcing.eventstore.EventStorageEngine; +import org.axonframework.eventsourcing.eventstore.jpa.JpaEventStorageEngine; +import org.axonframework.eventsourcing.eventstore.jpa.SQLErrorCodesResolver; +import org.axonframework.messaging.interceptors.CorrelationDataInterceptor; +import org.axonframework.modelling.saga.repository.jpa.JpaSagaStore; +import org.axonframework.serialization.Serializer; +import org.axonframework.spring.messaging.unitofwork.SpringTransactionManager; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.SharedEntityManagerCreator; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +@Configuration +public class AxonConfig { + + @Bean(defaultCandidate = false) + @Qualifier("axon") + @ConfigurationProperties(prefix = "ovsx.axon.datasource") + public DataSource eventsDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean(defaultCandidate = false) + @Qualifier("axon") + @ConfigurationProperties(prefix = "ovsx.axon.jpa") + public JpaProperties eventsJpaProperties() { + return new JpaProperties(); + } + + @Qualifier("axon") + @Bean(defaultCandidate = false) + public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory( + @Qualifier("axon") DataSource dataSource, + @Qualifier("axon") JpaProperties jpaProperties + ) { + EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(jpaProperties); + return builder + .dataSource(dataSource) + .packages( + "org.axonframework.eventsourcing.eventstore.jpa", + "org.axonframework.modelling.saga.repository.jpa", + "org.axonframework.eventhandling.tokenstore", + "org.axonframework.eventhandling.deadletter.jpa" + ) + .persistenceUnit("axon") + .build(); + } + + private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) { + JpaVendorAdapter jpaVendorAdapter = createJpaVendorAdapter(jpaProperties); + Function> jpaPropertiesFactory = (dataSource) -> createJpaProperties(dataSource, + jpaProperties.getProperties()); + return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaPropertiesFactory, null); + } + + private JpaVendorAdapter createJpaVendorAdapter(JpaProperties jpaProperties) { + var adapter = new HibernateJpaVendorAdapter(); + adapter.setShowSql(jpaProperties.isShowSql()); + adapter.setGenerateDdl(jpaProperties.isGenerateDdl()); + adapter.setDatabasePlatform(jpaProperties.getDatabasePlatform()); + var database = jpaProperties.getDatabase(); + if(database != null) { + adapter.setDatabase(database); + } + return adapter; + } + + private Map createJpaProperties(DataSource dataSource, Map existingProperties) { + Map jpaProperties = new LinkedHashMap<>(existingProperties); + // ... map JPA properties that require the DataSource (e.g. DDL flags) + return jpaProperties; + } + + @Bean(defaultCandidate = false) + @Qualifier("axon") + public PlatformTransactionManager eventsPlatformTransactionManager( + @Qualifier("axon") EntityManagerFactory entityManagerFactory + ) { + return new JpaTransactionManager(entityManagerFactory); + } + + @Bean(defaultCandidate = false) + @Qualifier("axon") + public EntityManager eventsSharedEntityManager(@Qualifier("axon") EntityManagerFactory entityManagerFactory ) { + return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); + } + + @Bean + @Primary + public EntityManagerProvider eventsEntityManagerProvider(@Qualifier("axon") EntityManager entityManager) { + return new SimpleEntityManagerProvider(entityManager); + } + + @Bean + @Primary + public TransactionManager eventsTransactionManager(@Qualifier("axon") PlatformTransactionManager transactionManager) { + return new SpringTransactionManager(transactionManager); + } + + @Bean + @Primary + public PersistenceExceptionResolver eventsDataSourcePER(@Qualifier("axon")DataSource dataSource) throws SQLException { + return new SQLErrorCodesResolver(dataSource); + } + + @Bean + @Primary + public EventStorageEngine eventStorageEngine( + @Qualifier("eventSerializer") Serializer eventSerializer, + Serializer snapshotSerializer, + @Qualifier("axon") DataSource dataSource, + EntityManagerProvider entityManagerProvider, + PersistenceExceptionResolver persistenceExceptionResolver, + TransactionManager transactionManager + ) throws SQLException { + return JpaEventStorageEngine.builder() + .eventSerializer(eventSerializer) + .snapshotSerializer(snapshotSerializer) + .dataSource(dataSource) + .entityManagerProvider(entityManagerProvider) + .persistenceExceptionResolver(persistenceExceptionResolver) + .transactionManager(transactionManager) + .build(); + } + + @Bean + @Primary + public JpaSagaStore sagaStore( + Serializer serializer, + EntityManagerProvider entityManagerProvider + ) { + return JpaSagaStore.builder() + .entityManagerProvider(entityManagerProvider) + .serializer(serializer) + .build(); + } + + @Bean + @Primary + public SimpleCommandBus commandBus( + org.axonframework.config.Configuration axonConfiguration, + DuplicateCommandHandlerResolver duplicateCommandHandlerResolver, + TransactionManager txManager + ) { + var commandBus = SimpleCommandBus.builder() + .transactionManager(txManager) + .duplicateCommandHandlerResolver(duplicateCommandHandlerResolver) + .messageMonitor(axonConfiguration.messageMonitor(CommandBus.class, "commandBus")) + .build(); + + commandBus.registerHandlerInterceptor( + new CorrelationDataInterceptor(axonConfiguration.correlationDataProviders()) + ); + return commandBus; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/axon/ByteaEnforcedPostgresSQLDialect.java b/server/src/main/java/org/eclipse/openvsx/axon/ByteaEnforcedPostgresSQLDialect.java new file mode 100644 index 000000000..3598e294b --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/axon/ByteaEnforcedPostgresSQLDialect.java @@ -0,0 +1,46 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.axon; + +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.BinaryJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; + +import java.sql.Types; + +public class ByteaEnforcedPostgresSQLDialect extends PostgreSQLDialect { + + public ByteaEnforcedPostgresSQLDialect(){ + super(DatabaseVersion.make(9, 5)); + } + + @Override + protected String columnType(int sqlTypeCode) { + return sqlTypeCode == SqlTypes.BLOB ? "bytea" : super.columnType(sqlTypeCode); + } + + @Override + protected String castType(int sqlTypeCode) { + return sqlTypeCode == SqlTypes.BLOB ? "bytea" : super.castType(sqlTypeCode); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, + ServiceRegistry serviceRegistry) { + super.contributeTypes(typeContributions, serviceRegistry); + JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() + .getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptor(Types.BLOB, BinaryJdbcType.INSTANCE); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/search/DatabaseSearchService.java b/server/src/main/java/org/eclipse/openvsx/search/DatabaseSearchService.java index 1346dc898..aec7c5f34 100644 --- a/server/src/main/java/org/eclipse/openvsx/search/DatabaseSearchService.java +++ b/server/src/main/java/org/eclipse/openvsx/search/DatabaseSearchService.java @@ -26,7 +26,12 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import java.util.*; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static org.eclipse.openvsx.cache.CacheService.CACHE_AVERAGE_REVIEW_RATING; @@ -70,7 +75,7 @@ public SearchHits search(ISearchService.Options options) { .map(extensionSearch -> new SearchHit<>(null, null, null, 0.0f, null, null, null, null, null, null, extensionSearch)) .toList(); - return new SearchHitsImpl<>(totalHits, TotalHitsRelation.OFF, 0f, null, null, searchHits, null, null, null); + return new SearchHitsImpl<>(totalHits, TotalHitsRelation.OFF, 0f, Duration.of(0L, ChronoUnit.MILLIS), null, null, searchHits, null, null, null); } private List applyPaging(Options options, List sortedExtensions) { diff --git a/server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java b/server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java index d98ab85f4..7bde886c6 100644 --- a/server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java +++ b/server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java @@ -41,7 +41,9 @@ import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; +import java.time.Duration; import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -249,7 +251,7 @@ public void removeSearchEntry(Extension extension) { public SearchHits search(Options options) { var resultWindow = options.requestedOffset() + options.requestedSize(); if(resultWindow > getMaxResultWindow()) { - return new SearchHitsImpl<>(0, TotalHitsRelation.OFF, 0f, null, null, Collections.emptyList(), null, null, null); + return new SearchHitsImpl<>(0, TotalHitsRelation.OFF, 0f, Duration.of(0L, ChronoUnit.MILLIS), null, null, Collections.emptyList(), null, null, null); } var queryBuilder = new NativeQueryBuilder(); @@ -291,6 +293,7 @@ public SearchHits search(Options options) { firstSearchHitsPage.getTotalHits(), firstSearchHitsPage.getTotalHitsRelation(), firstSearchHitsPage.getMaxScore(), + Duration.of(0L, ChronoUnit.MILLIS), null, null, searchHits, From 584d9c4b92fd93419e6273f8528b436a7990ab52 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Wed, 30 Apr 2025 23:39:58 +0300 Subject: [PATCH 2/5] PersonalAccessToken events and query --- .../java/org/eclipse/openvsx/UserService.java | 15 ++++- .../events/PersonalAccessTokenAccessed.java | 14 +++++ .../events/PersonalAccessTokenCreated.java | 14 +++++ .../events/PersonalAccessTokenDeleted.java | 13 +++++ .../openvsx/events/UserDataCreated.java | 12 ++++ .../openvsx/query/GetUserAccessTokens.java | 12 ++++ .../query/PersonalAccessTokenHandler.java | 56 +++++++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java create mode 100644 server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java create mode 100644 server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenDeleted.java create mode 100644 server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java create mode 100644 server/src/main/java/org/eclipse/openvsx/query/GetUserAccessTokens.java create mode 100644 server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java diff --git a/server/src/main/java/org/eclipse/openvsx/UserService.java b/server/src/main/java/org/eclipse/openvsx/UserService.java index 2d4578ee1..b9090ba9f 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserService.java +++ b/server/src/main/java/org/eclipse/openvsx/UserService.java @@ -18,11 +18,16 @@ import org.apache.tika.mime.MediaType; import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypes; +import org.axonframework.eventhandling.gateway.EventGateway; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.entities.Namespace; import org.eclipse.openvsx.entities.NamespaceMembership; import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; +import org.eclipse.openvsx.events.PersonalAccessTokenAccessed; +import org.eclipse.openvsx.events.PersonalAccessTokenCreated; +import org.eclipse.openvsx.events.PersonalAccessTokenDeleted; +import org.eclipse.openvsx.events.UserDataCreated; import org.eclipse.openvsx.json.AccessTokenJson; import org.eclipse.openvsx.json.NamespaceDetailsJson; import org.eclipse.openvsx.json.ResultJson; @@ -57,6 +62,7 @@ public class UserService { private final ExtensionValidator validator; private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AttributesConfig attributesConfig; + private final EventGateway events; public UserService( EntityManager entityManager, @@ -65,7 +71,8 @@ public UserService( CacheService cache, ExtensionValidator validator, @Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository, - OAuth2AttributesConfig attributesConfig + OAuth2AttributesConfig attributesConfig, + EventGateway events ) { this.entityManager = entityManager; this.repositories = repositories; @@ -74,6 +81,7 @@ public UserService( this.validator = validator; this.clientRegistrationRepository = clientRegistrationRepository; this.attributesConfig = attributesConfig; + this.events = events; } public UserData findLoggedInUser() { @@ -95,6 +103,7 @@ public PersonalAccessToken useAccessToken(String tokenValue) { return null; } token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); + events.publish(new PersonalAccessTokenAccessed(token.getUser().getId(), token.getId(), token.getAccessedTimestamp())); return token; } @@ -261,7 +270,7 @@ public AccessTokenJson createAccessToken(UserData user, String description) { // Include the token value after creation so the user can copy it json.setValue(token.getValue()); json.setDeleteTokenUrl(createApiUrl(UrlUtil.getBaseUrl(), "user", "token", "delete", Long.toString(token.getId()))); - + events.publish(new PersonalAccessTokenCreated(user.getId(), token.getId(), token.getCreatedTimestamp(), token.getDescription())); return json; } @@ -278,6 +287,7 @@ public ResultJson deleteAccessToken(UserData user, long id) { } token.setActive(false); + events.publish(new PersonalAccessTokenDeleted(user.getId(), token.getId())); return ResultJson.success("Deleted access token for user " + user.getLoginName() + "."); } @@ -291,6 +301,7 @@ public UserData upsertUser(UserData newUser) { if (userData == null) { entityManager.persist(newUser); userData = newUser; + events.publish(new UserDataCreated(userData.getId())); } else { var updated = false; if (!StringUtils.equals(userData.getLoginName(), newUser.getLoginName())) { diff --git a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java new file mode 100644 index 000000000..0ae592af6 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java @@ -0,0 +1,14 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.events; + +import java.time.LocalDateTime; + +public record PersonalAccessTokenAccessed(long userId, long tokenId, LocalDateTime accessedTimestamp) {} diff --git a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java new file mode 100644 index 000000000..d450f3255 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java @@ -0,0 +1,14 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.events; + +import java.time.LocalDateTime; + +public record PersonalAccessTokenCreated(long userId, long tokenId, LocalDateTime createdTimestamp, String description) {} diff --git a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenDeleted.java b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenDeleted.java new file mode 100644 index 000000000..a2a1171f4 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenDeleted.java @@ -0,0 +1,13 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.events; + +public record PersonalAccessTokenDeleted(long userId, long tokenId) { +} diff --git a/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java b/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java new file mode 100644 index 000000000..68c4c2cc4 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java @@ -0,0 +1,12 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.events; + +public record UserDataCreated(long id) {} diff --git a/server/src/main/java/org/eclipse/openvsx/query/GetUserAccessTokens.java b/server/src/main/java/org/eclipse/openvsx/query/GetUserAccessTokens.java new file mode 100644 index 000000000..1d906b046 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/query/GetUserAccessTokens.java @@ -0,0 +1,12 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.query; + +public record GetUserAccessTokens(long userId) {} diff --git a/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java b/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java new file mode 100644 index 000000000..19f8312be --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java @@ -0,0 +1,56 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Personal License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.query; + +import org.axonframework.eventhandling.EventHandler; +import org.axonframework.queryhandling.QueryHandler; +import org.eclipse.openvsx.events.PersonalAccessTokenAccessed; +import org.eclipse.openvsx.events.PersonalAccessTokenCreated; +import org.eclipse.openvsx.events.PersonalAccessTokenDeleted; +import org.eclipse.openvsx.events.UserDataCreated; +import org.eclipse.openvsx.json.AccessTokenJson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +@Component +public class PersonalAccessTokenHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PersonalAccessTokenHandler.class); + + @EventHandler + public void on(PersonalAccessTokenCreated event) { + LOGGER.info("RECEIVED PersonalAccessTokenCreated | [{}] {} - {}", event.tokenId(), event.createdTimestamp(), event.description()); + } + + @EventHandler + public void on(PersonalAccessTokenDeleted event) { + LOGGER.info("RECEIVED PersonalAccessTokenDeleted | [{}]", event.tokenId()); + } + + @EventHandler + public void on(PersonalAccessTokenAccessed event) { + LOGGER.info("RECEIVED PersonalAccessTokenAccessed | [{}] {}", event.tokenId(), event.accessedTimestamp()); + } + + @EventHandler + public void on(UserDataCreated event) { + + } + + @QueryHandler + public List query(GetUserAccessTokens criteria) { + // return the query result based on given criteria + return Collections.emptyList(); + } +} From 28f5ac20c1b779161d06d2f28e9857086a28c97c Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 1 May 2025 11:51:28 +0300 Subject: [PATCH 3/5] Fix unit tests --- .../org/eclipse/openvsx/axon/AxonConfig.java | 2 + .../org/eclipse/openvsx/RegistryAPITest.java | 47 +++++++++++++++---- .../java/org/eclipse/openvsx/UserAPITest.java | 39 ++++++++++++--- .../openvsx/adapter/VSCodeAPITest.java | 38 ++++++++++----- .../adapter/VSCodeIdUpdateServiceTest.java | 6 +-- .../eclipse/openvsx/admin/AdminAPITest.java | 43 +++++++++++++---- .../AdminStatisticsJobRequestHandlerTest.java | 6 +-- .../openvsx/eclipse/EclipseServiceTest.java | 10 ++-- .../ExtensionVersionIntegrityServiceTest.java | 4 +- .../search/DatabaseSearchServiceTest.java | 6 +-- .../search/ElasticSearchServiceTest.java | 12 ++--- .../storage/AzureBlobStorageServiceTest.java | 12 ++--- .../openvsx/web/SitemapControllerTest.java | 6 +-- 13 files changed, 166 insertions(+), 65 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java b/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java index 7cd12651f..a6477cd8d 100644 --- a/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java +++ b/server/src/main/java/org/eclipse/openvsx/axon/AxonConfig.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -47,6 +48,7 @@ import java.util.function.Function; @Configuration +@Profile("!test") public class AxonConfig { @Bean(defaultCandidate = false) diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index f46031936..fce0cedc9 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -14,6 +14,10 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.apache.commons.lang3.ArrayUtils; +import org.axonframework.eventhandling.EventBus; +import org.axonframework.eventhandling.SimpleEventBus; +import org.axonframework.eventhandling.gateway.DefaultEventGateway; +import org.axonframework.eventhandling.gateway.EventGateway; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.ExtensionJsonCacheKeyGenerator; @@ -47,8 +51,6 @@ import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; @@ -60,6 +62,8 @@ import org.springframework.data.util.Streamable; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.support.TransactionTemplate; @@ -68,7 +72,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -87,7 +93,7 @@ @WebMvcTest(RegistryAPI.class) @AutoConfigureWebClient -@MockBean({ +@MockitoBean(types = { ClientRegistrationRepository.class, UpstreamRegistryService.class, GoogleCloudStorageService.class, AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, AzureDownloadCountService.class, CacheService.class, EclipseService.class, PublishExtensionVersionService.class, SimpleMeterRegistry.class, @@ -95,19 +101,19 @@ }) class RegistryAPITest { - @SpyBean + @MockitoSpyBean UserService users; - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean SearchUtilService search; - @MockBean + @MockitoBean ExtensionVersionIntegrityService integrityService; - @MockBean + @MockitoBean EntityManager entityManager; @Autowired @@ -2184,7 +2190,7 @@ private List mockSearch() { var entry1 = new ExtensionSearch(); entry1.setId(1); var searchHit = new SearchHit<>("0", "1", null, 1.0f, null, null, null, null, null, null, entry1); - var searchHits = new SearchHitsImpl<>(1, TotalHitsRelation.EQUAL_TO, 1.0f, "1", null, List.of(searchHit), null, null, null); + var searchHits = new SearchHitsImpl<>(1, TotalHitsRelation.EQUAL_TO, 1.0f, Duration.of(0, ChronoUnit.MILLIS), "1", null, List.of(searchHit), null, null, null); Mockito.when(search.isEnabled()) .thenReturn(true); var searchOptions = new ISearchService.Options("foo", null, null, 10, 0, "desc", "relevance", false, null); @@ -2522,5 +2528,28 @@ PublishExtensionVersionHandler publishExtensionVersionHandler( extensionControl ); } + + @Bean + public UserService userService( + EntityManager entityManager, + RepositoryService repositories, + StorageUtilService storageUtil, + CacheService cache, + ExtensionValidator validator, + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AttributesConfig attributesConfig, + EventGateway events + ) { + return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig, events); + } + + @Bean + public EventGateway eventGateway(EventBus eventBus) { + return DefaultEventGateway.builder().eventBus(eventBus).build(); + } + + @Bean EventBus eventBus() { + return SimpleEventBus.builder().build(); + } } } diff --git a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java index 294d735e1..88543f7f6 100644 --- a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java @@ -13,6 +13,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.axonframework.eventhandling.EventBus; +import org.axonframework.eventhandling.SimpleEventBus; +import org.axonframework.eventhandling.gateway.DefaultEventGateway; +import org.axonframework.eventhandling.gateway.EventGateway; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; @@ -33,12 +37,12 @@ import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.data.util.Streamable; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.support.TransactionTemplate; @@ -57,19 +61,19 @@ @WebMvcTest(UserAPI.class) @AutoConfigureWebClient -@MockBean({ +@MockitoBean(types = { EclipseService.class, ClientRegistrationRepository.class, StorageUtilService.class, CacheService.class, ExtensionValidator.class, SimpleMeterRegistry.class }) class UserAPITest { - @SpyBean + @MockitoSpyBean UserService users; - @MockBean + @MockitoBean EntityManager entityManager; - @MockBean + @MockitoBean RepositoryService repositories; @Autowired @@ -575,5 +579,28 @@ TokenService tokenService( LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator() { return new LatestExtensionVersionCacheKeyGenerator(); } + + @Bean + public UserService userService( + EntityManager entityManager, + RepositoryService repositories, + StorageUtilService storageUtil, + CacheService cache, + ExtensionValidator validator, + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AttributesConfig attributesConfig, + EventGateway events + ) { + return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig, events); + } + + @Bean + public EventGateway eventGateway(EventBus eventBus) { + return DefaultEventGateway.builder().eventBus(eventBus).build(); + } + + @Bean EventBus eventBus() { + return SimpleEventBus.builder().build(); + } } } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index f5ebedf90..87c7d17a0 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -13,6 +13,10 @@ import com.google.common.io.CharStreams; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.axonframework.eventhandling.EventBus; +import org.axonframework.eventhandling.SimpleEventBus; +import org.axonframework.eventhandling.gateway.DefaultEventGateway; +import org.axonframework.eventhandling.gateway.EventGateway; import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.MockTransactionTemplate; import org.eclipse.openvsx.UserService; @@ -40,7 +44,6 @@ import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.data.elasticsearch.core.SearchHit; @@ -49,6 +52,7 @@ import org.springframework.data.util.Streamable; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.support.TransactionTemplate; @@ -59,7 +63,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.time.Duration; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipFile; @@ -72,24 +78,24 @@ @WebMvcTest(VSCodeAPI.class) @AutoConfigureWebClient -@MockBean({ +@MockitoBean(types = { ClientRegistrationRepository.class, GoogleCloudStorageService.class, AzureBlobStorageService.class, AwsStorageService.class, AzureDownloadCountService.class, CacheService.class, UpstreamVSCodeService.class, - VSCodeIdService.class, EntityManager.class, EclipseService.class, ExtensionValidator.class, SimpleMeterRegistry.class, + VSCodeIdService.class, EclipseService.class, ExtensionValidator.class, SimpleMeterRegistry.class, FileCacheDurationConfig.class }) class VSCodeAPITest { - @MockBean + @MockitoBean EntityManager entityManager; - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean SearchUtilService search; - @MockBean + @MockitoBean ExtensionVersionIntegrityService integrityService; @Autowired @@ -632,7 +638,7 @@ private Extension mockSearch(String targetPlatform, String namespaceName, boolea List> searchResults = !builtInExtensionNamespace.equals(namespaceName) ? Collections.singletonList(new SearchHit<>("0", "1", null, 1.0f, null, null, null, null, null, null, entry1)) : Collections.emptyList(); - var searchHits = new SearchHitsImpl<>(searchResults.size(), TotalHitsRelation.EQUAL_TO, 1.0f, "1", null, + var searchHits = new SearchHitsImpl<>(searchResults.size(), TotalHitsRelation.EQUAL_TO, 1.0f, Duration.of(0, ChronoUnit.MILLIS), "1", null, searchResults, null, null, null); Mockito.when(integrityService.isEnabled()) @@ -982,16 +988,26 @@ LocalVSCodeService localVSCodeService( } @Bean - UserService userService( + public UserService userService( EntityManager entityManager, RepositoryService repositories, StorageUtilService storageUtil, CacheService cache, ExtensionValidator validator, ClientRegistrationRepository clientRegistrationRepository, - OAuth2AttributesConfig attributesConfig + OAuth2AttributesConfig attributesConfig, + EventGateway events ) { - return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); + return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig, events); + } + + @Bean + public EventGateway eventGateway(EventBus eventBus) { + return DefaultEventGateway.builder().eventBus(eventBus).build(); + } + + @Bean EventBus eventBus() { + return SimpleEventBus.builder().build(); } @Bean diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java index adb204e3b..1af76058a 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java @@ -17,8 +17,8 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; @@ -28,10 +28,10 @@ @ExtendWith(SpringExtension.class) class VSCodeIdUpdateServiceTest { - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean VSCodeIdService idService; @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index 0be83bbe4..9ee959f26 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -13,6 +13,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.axonframework.eventhandling.EventBus; +import org.axonframework.eventhandling.SimpleEventBus; +import org.axonframework.eventhandling.gateway.DefaultEventGateway; +import org.axonframework.eventhandling.gateway.EventGateway; import org.eclipse.openvsx.*; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; @@ -38,8 +42,6 @@ import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.data.util.Streamable; @@ -47,6 +49,8 @@ import org.springframework.http.MediaType; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.support.TransactionTemplate; @@ -69,7 +73,7 @@ @WebMvcTest(AdminAPI.class) @AutoConfigureWebClient -@MockBean({ +@MockitoBean(types = { ClientRegistrationRepository.class, UpstreamRegistryService.class, GoogleCloudStorageService.class, AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, AzureDownloadCountService.class, CacheService.class, PublishExtensionVersionHandler.class, SearchUtilService.class, EclipseService.class, @@ -77,19 +81,19 @@ }) class AdminAPITest { - @SpyBean + @MockitoSpyBean UserService users; - @MockBean + @MockitoBean JobRequestScheduler scheduler; - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean EntityManager entityManager; - @MockBean + @MockitoBean ExtensionVersionIntegrityService integrityService; @Autowired @@ -1329,5 +1333,28 @@ VersionService versionService() { LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator() { return new LatestExtensionVersionCacheKeyGenerator(); } + + @Bean + public UserService userService( + EntityManager entityManager, + RepositoryService repositories, + StorageUtilService storageUtil, + CacheService cache, + ExtensionValidator validator, + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AttributesConfig attributesConfig, + EventGateway events + ) { + return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig, events); + } + + @Bean + public EventGateway eventGateway(EventBus eventBus) { + return DefaultEventGateway.builder().eventBus(eventBus).build(); + } + + @Bean EventBus eventBus() { + return SimpleEventBus.builder().build(); + } } } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandlerTest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandlerTest.java index 466a015ff..b08a3100e 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandlerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminStatisticsJobRequestHandlerTest.java @@ -16,8 +16,8 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Map; @@ -25,10 +25,10 @@ @ExtendWith(SpringExtension.class) class AdminStatisticsJobRequestHandlerTest { - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean AdminStatisticsService service; @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java index 0c9508e62..e9fba2c3a 100644 --- a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java @@ -32,10 +32,10 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.data.util.Streamable; import org.springframework.http.*; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.client.HttpClientErrorException; @@ -52,20 +52,20 @@ import static org.mockito.ArgumentMatchers.eq; @ExtendWith(SpringExtension.class) -@MockBean({ +@MockitoBean(types = { EntityManager.class, SearchUtilService.class, GoogleCloudStorageService.class, AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, AzureDownloadCountService.class, CacheService.class, UserService.class, PublishExtensionVersionHandler.class, SimpleMeterRegistry.class, FileCacheDurationConfig.class }) class EclipseServiceTest { - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean TokenService tokens; - @MockBean + @MockitoBean RestTemplate restTemplate; @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityServiceTest.java b/server/src/test/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityServiceTest.java index 958729c92..2820fd793 100644 --- a/server/src/test/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/publish/ExtensionVersionIntegrityServiceTest.java @@ -23,8 +23,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.io.IOException; @@ -34,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.*; @ExtendWith(SpringExtension.class) -@MockBean({ CacheService.class, RepositoryService.class, EntityManager.class }) +@MockitoBean(types = { CacheService.class, RepositoryService.class, EntityManager.class }) class ExtensionVersionIntegrityServiceTest { @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/search/DatabaseSearchServiceTest.java b/server/src/test/java/org/eclipse/openvsx/search/DatabaseSearchServiceTest.java index d57fe276e..4a030d9f5 100644 --- a/server/src/test/java/org/eclipse/openvsx/search/DatabaseSearchServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/search/DatabaseSearchServiceTest.java @@ -20,10 +20,10 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.util.Streamable; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.time.LocalDateTime; @@ -34,10 +34,10 @@ @ExtendWith(SpringExtension.class) class DatabaseSearchServiceTest { - @MockBean + @MockitoBean EntityManager entityManager; - @MockBean + @MockitoBean RepositoryService repositories; @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java b/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java index 2c88c1710..fd6b1817d 100644 --- a/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java @@ -9,6 +9,7 @@ ********************************************************************************/ package org.eclipse.openvsx.search; +import jakarta.persistence.EntityManager; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.repositories.RepositoryService; @@ -19,7 +20,6 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexOperations; @@ -27,9 +27,9 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.util.Streamable; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; -import jakarta.persistence.EntityManager; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -39,16 +39,16 @@ import static org.mockito.ArgumentMatchers.any; @ExtendWith(SpringExtension.class) -@MockBean({JobRequestScheduler.class}) +@MockitoBean(types = {JobRequestScheduler.class}) class ElasticSearchServiceTest { - @MockBean + @MockitoBean EntityManager entityManager; - @MockBean + @MockitoBean RepositoryService repositories; - @MockBean + @MockitoBean ElasticsearchOperations searchOperations; @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/storage/AzureBlobStorageServiceTest.java b/server/src/test/java/org/eclipse/openvsx/storage/AzureBlobStorageServiceTest.java index 78fbf63fd..c3c321846 100644 --- a/server/src/test/java/org/eclipse/openvsx/storage/AzureBlobStorageServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/storage/AzureBlobStorageServiceTest.java @@ -9,10 +9,6 @@ * ****************************************************************************** */ package org.eclipse.openvsx.storage; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.net.URI; - import org.eclipse.openvsx.cache.FilesCacheKeyGenerator; import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionVersion; @@ -22,12 +18,16 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + @ExtendWith(SpringExtension.class) -@MockBean({ StorageUtilService.class }) +@MockitoBean(types = { StorageUtilService.class }) class AzureBlobStorageServiceTest { @Autowired diff --git a/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java b/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java index 8afa3571c..b38a7fc54 100644 --- a/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java @@ -24,9 +24,9 @@ import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import java.util.List; @@ -37,12 +37,12 @@ @WebMvcTest(SitemapController.class) @AutoConfigureWebClient -@MockBean({ +@MockitoBean(types = { EclipseService.class, SimpleMeterRegistry.class, UserService.class, TokenService.class, EntityManager.class }) class SitemapControllerTest { - @MockBean + @MockitoBean RepositoryService repositories; @Autowired From b2b537cea4d168add8c06f961e7271776c9190b0 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 1 May 2025 16:00:20 +0300 Subject: [PATCH 4/5] Add Redis as read database --- docker-compose.yml | 8 ++++ server/build.gradle | 8 +++- server/src/dev/resources/application.yml | 3 +- .../openvsx/redis/EmbeddedRedisServer.java | 33 ++++++++++++++ .../eclipse/openvsx/redis/RedisConfig.java | 43 +++++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/redis/EmbeddedRedisServer.java create mode 100644 server/src/main/java/org/eclipse/openvsx/redis/RedisConfig.java diff --git a/docker-compose.yml b/docker-compose.yml index eb174bdbe..ddcb94bd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,14 @@ services: profiles: - axon + valkey: + image: valkey/valkey + ports: + - '6379:6379' + profiles: + - valkey + - redis + server: image: openjdk:17 working_dir: /app diff --git a/server/build.gradle b/server/build.gradle index b4655ac37..d1964e685 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -41,7 +41,9 @@ def versions = [ jaxb_impl: '2.3.8', gatling: '3.13.5', loki4j: '1.4.2', - axon: '4.11.2' + axon: '4.11.2', + jedis: '5.2.0', + embedded_redis: '1.4.3' ] ext['junit-jupiter.version'] = versions.junit sourceCompatibility = versions.java @@ -84,6 +86,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-actuator" implementation "org.springframework.boot:spring-boot-starter-cache" implementation "org.springframework.boot:spring-boot-starter-aop" + implementation "org.springframework.boot:spring-boot-starter-data-redis" implementation "org.springframework.security:spring-security-oauth2-client" implementation "org.springframework.security:spring-security-oauth2-jose" implementation "org.springframework.session:spring-session-jdbc" @@ -115,6 +118,9 @@ dependencies { implementation "io.micrometer:micrometer-tracing" implementation "io.micrometer:micrometer-tracing-bridge-otel" implementation "io.opentelemetry:opentelemetry-exporter-zipkin" + implementation "redis.clients:jedis:${versions.jedis}" + implementation "com.github.codemonstur:embedded-redis:${versions.embedded_redis}" + runtimeOnly "io.micrometer:micrometer-registry-prometheus" runtimeOnly "org.postgresql:postgresql" jooqGenerator "org.postgresql:postgresql" diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index 522396e3a..67133230c 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -153,7 +153,8 @@ ovsx: hibernate: dialect: org.eclipse.openvsx.axon.ByteaEnforcedPostgresSQLDialect physical_naming_strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy - + redis: + embedded: true databasesearch: enabled: false elasticsearch: diff --git a/server/src/main/java/org/eclipse/openvsx/redis/EmbeddedRedisServer.java b/server/src/main/java/org/eclipse/openvsx/redis/EmbeddedRedisServer.java new file mode 100644 index 000000000..e8c29d7e1 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/redis/EmbeddedRedisServer.java @@ -0,0 +1,33 @@ +package org.eclipse.openvsx.redis; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.stereotype.Component; +import redis.embedded.RedisServer; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; + +@Component +@ConditionalOnProperty(value = "ovsx.redis.embedded", havingValue = "true") +public class EmbeddedRedisServer { + + private final int port; + private RedisServer server; + + public EmbeddedRedisServer(RedisStandaloneConfiguration configuration) { + port = configuration.getPort(); + } + + @PostConstruct + public void start() throws IOException { + server = new RedisServer(port); + server.start(); + } + + @PreDestroy + public void stop() throws IOException { + server.stop(); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/redis/RedisConfig.java b/server/src/main/java/org/eclipse/openvsx/redis/RedisConfig.java new file mode 100644 index 000000000..23facb04c --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/redis/RedisConfig.java @@ -0,0 +1,43 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; + +@Configuration +@EnableRedisRepositories +@Profile("!test") +public class RedisConfig { + + @Bean + @ConfigurationProperties("ovsx.redis") + public RedisStandaloneConfiguration redisStandaloneConfiguration() { + return new RedisStandaloneConfiguration(); + } + + @Bean + JedisConnectionFactory jedisConnectionFactory(RedisStandaloneConfiguration configuration) { + return new JedisConnectionFactory(configuration); + } + + @Bean + public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(jedisConnectionFactory); + return template; + } +} From 68f70bfec07a04acdfd8e5d9af415a55f49b8651 Mon Sep 17 00:00:00 2001 From: amvanbaren Date: Thu, 1 May 2025 16:05:32 +0300 Subject: [PATCH 5/5] Handle user access token events and query --- .../java/org/eclipse/openvsx/UserAPI.java | 32 ++++++---- .../java/org/eclipse/openvsx/UserService.java | 4 +- .../events/PersonalAccessTokenAccessed.java | 4 +- .../events/PersonalAccessTokenCreated.java | 4 +- .../openvsx/events/UserDataCreated.java | 2 +- .../eclipse/openvsx/json/AccessTokenJson.java | 4 +- .../query/PersonalAccessTokenHandler.java | 64 ++++++++++++++++--- .../openvsx/query/UserAccessTokensData.java | 39 +++++++++++ .../query/UserAccessTokensRepository.java | 15 +++++ 9 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensData.java create mode 100644 server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensRepository.java diff --git a/server/src/main/java/org/eclipse/openvsx/UserAPI.java b/server/src/main/java/org/eclipse/openvsx/UserAPI.java index 8ae378610..e67e8de38 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/UserAPI.java @@ -10,12 +10,16 @@ package org.eclipse.openvsx; import jakarta.servlet.http.HttpServletRequest; +import org.axonframework.messaging.responsetypes.ResponseTypes; +import org.axonframework.queryhandling.QueryGateway; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.NamespaceMembership; import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.query.GetUserAccessTokens; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.security.CodedAuthException; +import org.eclipse.openvsx.security.IdPrincipal; import org.eclipse.openvsx.storage.StorageUtilService; import org.eclipse.openvsx.util.ErrorResultException; import org.eclipse.openvsx.util.NotFoundException; @@ -27,6 +31,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.*; @@ -35,6 +40,7 @@ import java.util.LinkedHashMap; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static org.eclipse.openvsx.entities.FileResource.*; @@ -51,17 +57,20 @@ public class UserAPI { private final UserService users; private final EclipseService eclipse; private final StorageUtilService storageUtil; + private final QueryGateway queries; public UserAPI( RepositoryService repositories, UserService users, EclipseService eclipse, - StorageUtilService storageUtil + StorageUtilService storageUtil, + QueryGateway queries ) { this.repositories = repositories; this.users = users; this.eclipse = eclipse; this.storageUtil = storageUtil; + this.queries = queries; } @GetMapping( @@ -133,19 +142,16 @@ public CsrfTokenJson getCsrfToken(HttpServletRequest request) { path = "/user/tokens", produces = MediaType.APPLICATION_JSON_VALUE ) - public List getAccessTokens() { - var user = users.findLoggedInUser(); - if (user == null) { - throw new ResponseStatusException(HttpStatus.FORBIDDEN); - } + public CompletableFuture> getAccessTokens(@AuthenticationPrincipal IdPrincipal principal) { var serverUrl = UrlUtil.getBaseUrl(); - return repositories.findActiveAccessTokens(user) - .map(token -> { - var json = token.toAccessTokenJson(); - json.setDeleteTokenUrl(createApiUrl(serverUrl, "user", "token", "delete", Long.toString(token.getId()))); - return json; - }) - .toList(); + return queries.query(new GetUserAccessTokens(principal.getId()), ResponseTypes.multipleInstancesOf(AccessTokenJson.class)) + .thenApply(tokens -> + tokens.stream().map(token -> { + token.setDeleteTokenUrl(serverUrl + token.getDeleteTokenUrl()); + return token; + }) + .toList() + ); } @PostMapping( diff --git a/server/src/main/java/org/eclipse/openvsx/UserService.java b/server/src/main/java/org/eclipse/openvsx/UserService.java index b9090ba9f..438131b22 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserService.java +++ b/server/src/main/java/org/eclipse/openvsx/UserService.java @@ -103,7 +103,7 @@ public PersonalAccessToken useAccessToken(String tokenValue) { return null; } token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); - events.publish(new PersonalAccessTokenAccessed(token.getUser().getId(), token.getId(), token.getAccessedTimestamp())); + events.publish(new PersonalAccessTokenAccessed(token.getUser().getId(), token.getId(), TimeUtil.toUTCString(token.getAccessedTimestamp()))); return token; } @@ -270,7 +270,7 @@ public AccessTokenJson createAccessToken(UserData user, String description) { // Include the token value after creation so the user can copy it json.setValue(token.getValue()); json.setDeleteTokenUrl(createApiUrl(UrlUtil.getBaseUrl(), "user", "token", "delete", Long.toString(token.getId()))); - events.publish(new PersonalAccessTokenCreated(user.getId(), token.getId(), token.getCreatedTimestamp(), token.getDescription())); + events.publish(new PersonalAccessTokenCreated(user.getId(), token.getId(), TimeUtil.toUTCString(token.getCreatedTimestamp()), token.getDescription())); return json; } diff --git a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java index 0ae592af6..45c06c33f 100644 --- a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java +++ b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenAccessed.java @@ -9,6 +9,4 @@ * ****************************************************************************** */ package org.eclipse.openvsx.events; -import java.time.LocalDateTime; - -public record PersonalAccessTokenAccessed(long userId, long tokenId, LocalDateTime accessedTimestamp) {} +public record PersonalAccessTokenAccessed(long userId, long tokenId, String accessedTimestamp) {} diff --git a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java index d450f3255..edcd67787 100644 --- a/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java +++ b/server/src/main/java/org/eclipse/openvsx/events/PersonalAccessTokenCreated.java @@ -9,6 +9,4 @@ * ****************************************************************************** */ package org.eclipse.openvsx.events; -import java.time.LocalDateTime; - -public record PersonalAccessTokenCreated(long userId, long tokenId, LocalDateTime createdTimestamp, String description) {} +public record PersonalAccessTokenCreated(long userId, long tokenId, String createdTimestamp, String description) {} diff --git a/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java b/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java index 68c4c2cc4..27056b2e0 100644 --- a/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java +++ b/server/src/main/java/org/eclipse/openvsx/events/UserDataCreated.java @@ -9,4 +9,4 @@ * ****************************************************************************** */ package org.eclipse.openvsx.events; -public record UserDataCreated(long id) {} +public record UserDataCreated(long userId) {} diff --git a/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java b/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java index d69368e71..c2bf3f70c 100644 --- a/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java +++ b/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java @@ -9,11 +9,11 @@ ********************************************************************************/ package org.eclipse.openvsx.json; -import javax.annotation.Nullable; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.annotation.Nullable; + @JsonInclude(Include.NON_NULL) public class AccessTokenJson extends ResultJson { diff --git a/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java b/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java index 19f8312be..dd428323e 100644 --- a/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/query/PersonalAccessTokenHandler.java @@ -16,41 +16,87 @@ import org.eclipse.openvsx.events.PersonalAccessTokenDeleted; import org.eclipse.openvsx.events.UserDataCreated; import org.eclipse.openvsx.json.AccessTokenJson; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.openvsx.util.UrlUtil; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.List; +import java.util.Optional; @Component public class PersonalAccessTokenHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(PersonalAccessTokenHandler.class); + private final UserAccessTokensRepository repository; + + public PersonalAccessTokenHandler(UserAccessTokensRepository repository) { + this.repository = repository; + } @EventHandler public void on(PersonalAccessTokenCreated event) { - LOGGER.info("RECEIVED PersonalAccessTokenCreated | [{}] {} - {}", event.tokenId(), event.createdTimestamp(), event.description()); + var accessToken = new AccessTokenJson(); + accessToken.setId(event.tokenId()); + accessToken.setCreatedTimestamp(event.createdTimestamp()); + accessToken.setDescription(event.description()); + accessToken.setDeleteTokenUrl(UrlUtil.createApiUrl("", "user", "token", "delete", Long.toString(event.tokenId()))); + + var userAccessTokens = repository.findById(event.userId()).get(); + var accessTokens = userAccessTokens.getAccessTokens(); + if(accessTokens != null) { + accessTokens.add(accessToken); + } else { + userAccessTokens.setAccessTokens(List.of(accessToken)); + } + + repository.save(userAccessTokens); } @EventHandler public void on(PersonalAccessTokenDeleted event) { - LOGGER.info("RECEIVED PersonalAccessTokenDeleted | [{}]", event.tokenId()); + var userAccessTokens = repository.findById(event.userId()).get(); + var removed = false; + var iterator = userAccessTokens.getAccessTokens().iterator(); + while(iterator.hasNext()) { + var accessToken = iterator.next(); + if(accessToken.getId() == event.tokenId()) { + iterator.remove(); + removed = true; + break; + } + } + if(!removed) { + throw new IllegalStateException("Access token doesn't exist"); + } + + repository.save(userAccessTokens); } @EventHandler public void on(PersonalAccessTokenAccessed event) { - LOGGER.info("RECEIVED PersonalAccessTokenAccessed | [{}] {}", event.tokenId(), event.accessedTimestamp()); + var userAccessTokens = repository.findById(event.userId()).get(); + var accessToken = userAccessTokens.getAccessTokens().stream() + .filter(t -> t.getId() == event.tokenId()) + .findFirst() + .get(); + + accessToken.setAccessedTimestamp(event.accessedTimestamp()); + repository.save(userAccessTokens); } @EventHandler public void on(UserDataCreated event) { + if(repository.findById(event.userId()).isPresent()) { + throw new IllegalStateException("User access tokens already exist"); + } + var userAccessTokens = new UserAccessTokensData(); + userAccessTokens.setUserId(event.userId()); + repository.save(userAccessTokens); } @QueryHandler - public List query(GetUserAccessTokens criteria) { - // return the query result based on given criteria - return Collections.emptyList(); + public List handle(GetUserAccessTokens query) { + var accessTokens = repository.findById(query.userId()).get().getAccessTokens(); + return Optional.ofNullable(accessTokens).orElse(Collections.emptyList()); } } diff --git a/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensData.java b/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensData.java new file mode 100644 index 000000000..0151d999a --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensData.java @@ -0,0 +1,39 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.query; + +import org.eclipse.openvsx.json.AccessTokenJson; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.util.List; + +@RedisHash("UserAccessTokens") +public class UserAccessTokensData { + @Id + private long userId; + private List accessTokens; + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public List getAccessTokens() { + return accessTokens; + } + + public void setAccessTokens(List accessTokens) { + this.accessTokens = accessTokens; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensRepository.java b/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensRepository.java new file mode 100644 index 000000000..c8aeb5c32 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/query/UserAccessTokensRepository.java @@ -0,0 +1,15 @@ +/** ****************************************************************************** + * Copyright (c) 2025 Precies. Software OU and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.query; + +import org.springframework.data.repository.CrudRepository; + +public interface UserAccessTokensRepository extends CrudRepository { +}