diff --git a/server/src/dev/resources/application.yml b/server/src/dev/resources/application.yml index e98cc1272..21da3de74 100644 --- a/server/src/dev/resources/application.yml +++ b/server/src/dev/resources/application.yml @@ -167,6 +167,9 @@ ovsx: revoked-access-tokens: subject: 'Open VSX Access Tokens Revoked' template: 'revoked-access-tokens.html' + access-token-expiry: + subject: 'Open VSX Access Token Expiry Notification' + template: 'access-token-expiry-notification.html' # dynamic tier-based rate limiting configuration rate-limit: enabled: false diff --git a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java index 7ef3ea8b8..ebe3b45ab 100644 --- a/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java +++ b/server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java @@ -13,6 +13,7 @@ import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; @@ -59,6 +60,7 @@ public class LocalRegistryService implements IExtensionRegistry { private final ExtensionService extensions; private final VersionService versions; private final UserService users; + private final AccessTokenService tokens; private final SearchUtilService search; private final ExtensionValidator validator; private final StorageUtilService storageUtil; @@ -73,6 +75,7 @@ public LocalRegistryService( ExtensionService extensions, VersionService versions, UserService users, + AccessTokenService tokens, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, @@ -86,6 +89,7 @@ public LocalRegistryService( this.extensions = extensions; this.versions = versions; this.users = users; + this.tokens = tokens; this.search = search; this.validator = validator; this.storageUtil = storageUtil; @@ -579,7 +583,7 @@ private Map> getMemberships(Collection createAccessToken(@RequestParam(required return new ResponseEntity<>(HttpStatus.FORBIDDEN); } - return new ResponseEntity<>(users.createAccessToken(user, description), HttpStatus.CREATED); + return new ResponseEntity<>(tokens.createAccessToken(user, description), HttpStatus.CREATED); } @PostMapping( @@ -189,7 +193,7 @@ public ResponseEntity deleteAccessToken(@PathVariable long id) { } try { - return ResponseEntity.ok(users.deleteAccessToken(user, id)); + return ResponseEntity.ok(tokens.deactivateAccessToken(user, id)); } catch(NotFoundException e) { return new ResponseEntity<>(ResultJson.error("Token does not exist."), HttpStatus.NOT_FOUND); } diff --git a/server/src/main/java/org/eclipse/openvsx/UserService.java b/server/src/main/java/org/eclipse/openvsx/UserService.java index c1d4c80df..81c748998 100644 --- a/server/src/main/java/org/eclipse/openvsx/UserService.java +++ b/server/src/main/java/org/eclipse/openvsx/UserService.java @@ -22,9 +22,7 @@ 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.json.AccessTokenJson; import org.eclipse.openvsx.json.NamespaceDetailsJson; import org.eclipse.openvsx.json.ResultJson; import org.eclipse.openvsx.repositories.RepositoryService; @@ -33,7 +31,6 @@ import org.eclipse.openvsx.storage.StorageUtilService; import org.eclipse.openvsx.util.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.CacheEvict; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -47,7 +44,6 @@ import java.util.stream.Collectors; import static org.eclipse.openvsx.cache.CacheService.CACHE_NAMESPACE_DETAILS_JSON; -import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; @Component public class UserService { @@ -60,9 +56,6 @@ public class UserService { private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AttributesConfig attributesConfig; - @Value("${ovsx.token-prefix:}") - String tokenPrefix; - public UserService( EntityManager entityManager, RepositoryService repositories, @@ -93,24 +86,6 @@ public UserData findLoggedInUser() { return null; } - @Transactional - public PersonalAccessToken useAccessToken(String tokenValue) { - var token = repositories.findAccessToken(tokenValue); - if (token == null || !token.isActive()) { - return null; - } - token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); - return token; - } - - public String generateTokenValue() { - String value; - do { - value = tokenPrefix + UUID.randomUUID(); - } while (repositories.hasAccessToken(value)); - return value; - } - public boolean hasPublishPermission(UserData user, Namespace namespace) { if (UserData.ROLE_PRIVILEGED.equals(user.getRole())) { // Privileged users can publish to every namespace. @@ -255,39 +230,6 @@ public ResultJson updateNamespaceDetailsLogo(String namespaceName, MultipartFile return ResultJson.success("Updated logo for namespace " + namespace.getName()); } - @Transactional - public AccessTokenJson createAccessToken(UserData user, String description) { - var token = new PersonalAccessToken(); - token.setUser(user); - token.setValue(generateTokenValue()); - token.setActive(true); - token.setCreatedTimestamp(TimeUtil.getCurrentUTC()); - token.setDescription(description); - entityManager.persist(token); - var json = token.toAccessTokenJson(); - // 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()))); - - return json; - } - - @Transactional - public ResultJson deleteAccessToken(UserData user, long id) { - var token = repositories.findAccessToken(id); - if (token == null || !token.isActive()) { - throw new NotFoundException(); - } - - user = entityManager.merge(user); - if(!token.getUser().equals(user)) { - throw new NotFoundException(); - } - - token.setActive(false); - return ResultJson.success("Deleted access token for user " + user.getLoginName() + "."); - } - public boolean canLogin() { return !getLoginProviders().isEmpty(); } diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java new file mode 100644 index 000000000..e05e169a8 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenConfig.java @@ -0,0 +1,82 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +public class AccessTokenConfig { + /** + * The token prefix to use when generating a new access token. + *

+ * Property: {@code ovsx.access-token.prefix} + * Old Property: {@code ovsx.token-prefix} + * Default: {@code ''} + */ + @Value("#{'${ovsx.access-token.prefix:${ovsx.token-prefix:}}'}") + String prefix; + + /** + * The expiration period for personal access tokens. + *

+ * If {@code 0} is provided, the access tokens do not expire. + *

+ * Property: {@code ovsx.access-token.expiration} + * Default: {@code P90D}, expires in 90 days + */ + @Value("${ovsx.access-token.expiration:P90D}") + Duration expiration; + + /** + * The duration before the expiration of an access token + * to send out a notification email to users. + *

+ * Property: {@code ovsx.access-token.notification} + * Default: {@code P7D}, 7 days prior to expiration + */ + @Value("${ovsx.access-token.notification:P7D}") + Duration notification; + + /** + * The maximum number of expiring token notifications to handle + * within one job execution. + *

+ * Property: {@code ovsx.access-token.max-token-notifications} + * Default: {@code 100} + */ + @Value("${ovsx.access-token.max-token-notifications:100}") + int maxTokenNotifications; + + /** + * The cron schedule for the job to disable expired + * access tokens. + *

+ * Property: {@code ovsx.access-token.expiration-schedule} + * Default: every 15 min + */ + @Value("${ovsx.access-token.expiration-schedule:0 */15 * * * *}") + String expirationSchedule; + + /** + * The cron schedule for the job to send out notifications + * for soon to be expired access tokens. + *

+ * Property: {@code ovsx.access-token.notification-schedule} + * Default: every 15 min + */ + @Value("${ovsx.access-token.notification-schedule:30 */15 * * * *}") + String notificationSchedule; +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java new file mode 100644 index 000000000..17d150665 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java @@ -0,0 +1,130 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.entities.UserData; +import org.eclipse.openvsx.json.AccessTokenJson; +import org.eclipse.openvsx.json.ResultJson; +import org.eclipse.openvsx.mail.MailService; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.NotFoundException; +import org.eclipse.openvsx.util.TimeUtil; +import org.eclipse.openvsx.util.UrlUtil; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.eclipse.openvsx.util.UrlUtil.createApiUrl; + +@Service +public class AccessTokenService { + private final AccessTokenConfig config; + private final EntityManager entityManager; + private final RepositoryService repositories; + private final MailService mail; + + public AccessTokenService( + AccessTokenConfig config, + EntityManager entityManager, + RepositoryService repositories, + MailService mail + ) { + this.config = config; + this.entityManager = entityManager; + this.repositories = repositories; + this.mail = mail; + } + + @Transactional + public AccessTokenJson createAccessToken(UserData user, String description) { + var token = new PersonalAccessToken(); + token.setUser(user); + token.setValue(generateTokenValue()); + token.setActive(true); + + var createdAt = TimeUtil.getCurrentUTC(); + token.setCreatedTimestamp(createdAt); + + if (config.expiration != null && config.expiration.isPositive()) { + token.setExpiresTimestamp(createdAt.plus(config.expiration)); + } + + token.setDescription(description); + entityManager.persist(token); + var json = token.toAccessTokenJson(); + // 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()))); + + return json; + } + + // public to be accessible from tests + public String generateTokenValue() { + String value; + do { + value = config.prefix + UUID.randomUUID(); + } while (repositories.hasAccessToken(value)); + return value; + } + + @Transactional + public ResultJson deactivateAccessToken(UserData user, long id) { + var token = repositories.findAccessToken(id); + if (token == null || !token.isActive()) { + throw new NotFoundException(); + } + + user = entityManager.merge(user); + if(!token.getUser().equals(user)) { + throw new NotFoundException(); + } + + token.setActive(false); + return ResultJson.success("Deactivated access token for user " + user.getLoginName() + "."); + } + + @Transactional + public PersonalAccessToken useAccessToken(String tokenValue) { + var token = repositories.findAccessToken(tokenValue); + if (token == null || !token.isActive()) { + return null; + } + token.setAccessedTimestamp(TimeUtil.getCurrentUTC()); + return token; + } + + @Transactional + public int expireAccessTokens() { + return repositories.expireAccessTokens(TimeUtil.getCurrentUTC()); + } + + @Transactional + public void scheduleTokenExpirationNotification(PersonalAccessToken token) { + token = entityManager.merge(token); + try { + mail.scheduleAccessTokenExpiryNotification(token); + } finally { + token.setNotified(true); + } + } + + @Transactional + public int setExpirationTimeForLegacyAccessTokens(LocalDateTime expirationTime) { + return repositories.updateExpiresTimeForLegacyAccessTokens(expirationTime); + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java new file mode 100644 index 000000000..0efbab687 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ExpirePersonalAccessTokensHandler.java @@ -0,0 +1,36 @@ +/** ****************************************************************************** + * 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.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ExpirePersonalAccessTokensHandler implements JobRequestHandler> { + + private final Logger logger = LoggerFactory.getLogger(ExpirePersonalAccessTokensHandler.class); + + private final AccessTokenService tokens; + + public ExpirePersonalAccessTokensHandler(AccessTokenService tokens) { + this.tokens = tokens; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + var count = tokens.expireAccessTokens(); + if (count > 0) { + logger.info("Expired {} personal access token(s)", count); + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java new file mode 100644 index 000000000..4b35c8f32 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/LegacyPersonalAccessTokenExpirationHandler.java @@ -0,0 +1,42 @@ +/** ****************************************************************************** + * 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.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class LegacyPersonalAccessTokenExpirationHandler implements JobRequestHandler> { + + private final Logger logger = LoggerFactory.getLogger(LegacyPersonalAccessTokenExpirationHandler.class); + + private final AccessTokenConfig config; + private final AccessTokenService tokens; + + public LegacyPersonalAccessTokenExpirationHandler(AccessTokenConfig config, AccessTokenService tokens) { + this.config = config; + this.tokens = tokens; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + if (config.expiration != null && config.expiration.isPositive()) { + var expirationTime = TimeUtil.getCurrentUTC().plus(config.expiration); + var count = tokens.setExpirationTimeForLegacyAccessTokens(expirationTime); + if (count > 0) { + logger.info("Set expiration time for {} legacy personal access token(s)", count); + } + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java new file mode 100644 index 000000000..034cf6b7a --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/NotifyPersonalAccessTokenExpirationHandler.java @@ -0,0 +1,55 @@ +/** ****************************************************************************** + * 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.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.util.TimeUtil; +import org.jobrunr.jobs.lambdas.JobRequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Component; + +@Component +public class NotifyPersonalAccessTokenExpirationHandler implements JobRequestHandler> { + + private final Logger logger = LoggerFactory.getLogger(NotifyPersonalAccessTokenExpirationHandler.class); + + private final AccessTokenConfig config; + private final AccessTokenService tokens; + private final RepositoryService repositories; + + public NotifyPersonalAccessTokenExpirationHandler( + AccessTokenConfig config, + AccessTokenService tokens, + RepositoryService repositories + ) { + this.config = config; + this.tokens = tokens; + this.repositories = repositories; + } + + @Override + public void run(HandlerJobRequest handlerJobRequest) throws Exception { + if (config.notification.isPositive()) { + var expireBefore = TimeUtil.getCurrentUTC().plus(config.notification); + var page = PageRequest.of(0, config.maxTokenNotifications); + var expiringAccessTokens = repositories.findExpiringAccessTokensWithoutNotification(expireBefore, page); + for (var token : expiringAccessTokens) { + tokens.scheduleTokenExpirationNotification(token); + } + + if (!expiringAccessTokens.isEmpty()) { + logger.info("Scheduled {} notification(s) for expiring personal access tokens", expiringAccessTokens.size()); + } + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java new file mode 100644 index 000000000..bd0090afa --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/accesstoken/ScheduleTokenExpirationJobs.java @@ -0,0 +1,65 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ +package org.eclipse.openvsx.accesstoken; + +import org.eclipse.openvsx.migration.HandlerJobRequest; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + + +@Component +public class ScheduleTokenExpirationJobs { + + private final Logger logger = LoggerFactory.getLogger(ScheduleTokenExpirationJobs.class); + + private final AccessTokenConfig config; + private final JobRequestScheduler scheduler; + + public ScheduleTokenExpirationJobs(AccessTokenConfig config, JobRequestScheduler scheduler) { + this.config = config; + this.scheduler = scheduler; + } + + @EventListener + public void scheduleJobs(ApplicationStartedEvent event) { + if (config.expiration != null && config.expiration.isPositive()) { + scheduler.enqueue(new HandlerJobRequest<>(LegacyPersonalAccessTokenExpirationHandler.class)); + } + + if (StringUtils.hasText(config.expirationSchedule)) { + logger.info("Scheduling access token expiration job with schedule '{}'", config.expirationSchedule); + scheduler.scheduleRecurrently( + "access-token-expiration", + config.expirationSchedule, + new HandlerJobRequest<>(ExpirePersonalAccessTokensHandler.class) + ); + } else { + scheduler.deleteRecurringJob("access-token-expiration"); + } + + if (StringUtils.hasText(config.notificationSchedule) && config.notification.isPositive()) { + logger.info("Scheduling access token notification job with schedule '{}'", config.notificationSchedule); + scheduler.scheduleRecurrently( + "access-token-notification", + config.notificationSchedule, + new HandlerJobRequest<>(NotifyPersonalAccessTokenExpirationHandler.class)); + } else { + scheduler.deleteRecurringJob("access-token-notification"); + } + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java index f9f15c4ef..20e2e99e2 100644 --- a/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java +++ b/server/src/main/java/org/eclipse/openvsx/admin/AdminService.java @@ -15,6 +15,7 @@ import org.eclipse.openvsx.ExtensionService; import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.*; @@ -46,6 +47,7 @@ public class AdminService { private final ExtensionService extensions; private final EntityManager entityManager; private final UserService users; + private final AccessTokenService tokens; private final ExtensionValidator validator; private final SearchUtilService search; private final EclipseService eclipse; @@ -61,6 +63,7 @@ public AdminService( ExtensionService extensions, EntityManager entityManager, UserService users, + AccessTokenService tokens, ExtensionValidator validator, SearchUtilService search, EclipseService eclipse, @@ -75,6 +78,7 @@ public AdminService( this.extensions = extensions; this.entityManager = entityManager; this.users = users; + this.tokens = tokens; this.validator = validator; this.search = search; this.eclipse = eclipse; @@ -476,7 +480,7 @@ public UserData checkAdminUser() { public UserData checkAdminUser(String tokenValue) { var user = Optional.of(tokenValue) - .map(users::useAccessToken) + .map(tokens::useAccessToken) .map(PersonalAccessToken::getUser) .orElse(null); diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java index 90d16bbb7..3eaf04fdb 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseService.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -43,7 +44,7 @@ import java.util.Map; import java.util.regex.Pattern; -@Component +@Service public class EclipseService { private static final String VAR_PERSON_ID = "personId"; @@ -61,7 +62,7 @@ public class EclipseService { protected final Logger logger = LoggerFactory.getLogger(EclipseService.class); - private final TokenService tokens; + private final EclipseTokenService tokens; private final ExtensionService extensions; private final EntityManager entityManager; private final RestTemplate restTemplate; @@ -77,7 +78,7 @@ public class EclipseService { List publisherAgreementAllowedVersions; public EclipseService( - TokenService tokens, + EclipseTokenService tokens, ExtensionService extensions, EntityManager entityManager, RestTemplate restTemplate diff --git a/server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java similarity index 96% rename from server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java rename to server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java index c9200d149..37c49b642 100644 --- a/server/src/main/java/org/eclipse/openvsx/eclipse/TokenService.java +++ b/server/src/main/java/org/eclipse/openvsx/eclipse/EclipseTokenService.java @@ -26,9 +26,9 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -37,16 +37,16 @@ import java.util.List; import java.util.Optional; -@Component -public class TokenService { +@Service +public class EclipseTokenService { - protected final Logger logger = LoggerFactory.getLogger(TokenService.class); + protected final Logger logger = LoggerFactory.getLogger(EclipseTokenService.class); private final TransactionTemplate transactions; private final EntityManager entityManager; private final ClientRegistrationRepository clientRegistrationRepository; - public TokenService( + public EclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, @Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository diff --git a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java index a1ef42c62..6af7f6823 100644 --- a/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java +++ b/server/src/main/java/org/eclipse/openvsx/entities/PersonalAccessToken.java @@ -9,20 +9,21 @@ ********************************************************************************/ package org.eclipse.openvsx.entities; +import jakarta.persistence.*; +import org.eclipse.openvsx.json.AccessTokenJson; +import org.eclipse.openvsx.util.TimeUtil; + import java.io.Serial; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; -import jakarta.persistence.*; - -import org.eclipse.openvsx.json.AccessTokenJson; -import org.eclipse.openvsx.util.TimeUtil; - @Entity @Table(uniqueConstraints = { @UniqueConstraint(columnNames = "value") }) public class PersonalAccessToken implements Serializable { + public static final int EXPIRY_DAYS = 90; + @Serial private static final long serialVersionUID = 1L; @@ -44,6 +45,10 @@ public class PersonalAccessToken implements Serializable { private LocalDateTime accessedTimestamp; + private LocalDateTime expiresTimestamp; + + private boolean notified; + @Column(length = 2048) private String description; @@ -54,10 +59,16 @@ public AccessTokenJson toAccessTokenJson() { var json = new AccessTokenJson(); json.setId(this.getId()); // The value is not included: it is displayed only when the token is created - if (this.getCreatedTimestamp() != null) + if (this.getCreatedTimestamp() != null) { json.setCreatedTimestamp(TimeUtil.toUTCString(this.getCreatedTimestamp())); - if (this.getAccessedTimestamp() != null) + } + if (this.getAccessedTimestamp() != null) { json.setAccessedTimestamp(TimeUtil.toUTCString(this.getAccessedTimestamp())); + } + if (this.getExpiresTimestamp() != null) { + json.setExpiresTimestamp(TimeUtil.toUTCString(this.getExpiresTimestamp())); + } + json.setNotified(this.isNotified()); json.setDescription(this.getDescription()); return json; } @@ -110,6 +121,22 @@ public void setAccessedTimestamp(LocalDateTime timestamp) { this.accessedTimestamp = timestamp; } + public LocalDateTime getExpiresTimestamp() { + return expiresTimestamp; + } + + public void setExpiresTimestamp(LocalDateTime expiresTimestamp) { + this.expiresTimestamp = expiresTimestamp; + } + + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + public String getDescription() { return description; } @@ -129,11 +156,13 @@ public boolean equals(Object o) { && Objects.equals(value, that.value) && Objects.equals(createdTimestamp, that.createdTimestamp) && Objects.equals(accessedTimestamp, that.accessedTimestamp) + && Objects.equals(expiresTimestamp, that.expiresTimestamp) + && Objects.equals(notified, that.notified) && Objects.equals(description, that.description); } @Override public int hashCode() { - return Objects.hash(id, user, value, active, createdTimestamp, accessedTimestamp, description); + return Objects.hash(id, user, value, active, createdTimestamp, accessedTimestamp, expiresTimestamp, notified, description); } } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java b/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java index 996cf78bb..3480b7dec 100644 --- a/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java +++ b/server/src/main/java/org/eclipse/openvsx/extension_control/ExtensionControlService.java @@ -31,7 +31,7 @@ import org.springframework.stereotype.Component; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; @@ -124,7 +124,7 @@ public void updateExtension(ExtensionId extensionId, boolean deprecated, Extensi } public JsonNode getExtensionControlJson() throws IOException { - var url = new URL("https://github.com/open-vsx/publish-extensions/raw/master/extension-control/extensions.json"); + var url = URI.create("https://github.com/open-vsx/publish-extensions/raw/master/extension-control/extensions.json").toURL(); return new ObjectMapper().readValue(url, JsonNode.class); } 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 a1116d72b..d3b0a6ef9 100644 --- a/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java +++ b/server/src/main/java/org/eclipse/openvsx/json/AccessTokenJson.java @@ -33,6 +33,11 @@ public static AccessTokenJson error(String message) { @Nullable private String accessedTimestamp; + @Nullable + private String expiresTimestamp; + + private boolean notified; + private String description; private String deleteTokenUrl; @@ -71,6 +76,23 @@ public void setAccessedTimestamp(@Nullable String accessedTimestamp) { this.accessedTimestamp = accessedTimestamp; } + @Nullable + public String getExpiresTimestamp() { + return expiresTimestamp; + } + + public void setExpiresTimestamp(@Nullable String expiresTimestamp) { + this.expiresTimestamp = expiresTimestamp; + } + + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + public String getDescription() { return description; } diff --git a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java index 223918e65..219889b7a 100644 --- a/server/src/main/java/org/eclipse/openvsx/mail/MailService.java +++ b/server/src/main/java/org/eclipse/openvsx/mail/MailService.java @@ -9,6 +9,7 @@ * ****************************************************************************** */ package org.eclipse.openvsx.mail; +import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; @@ -20,6 +21,7 @@ import java.util.Map; + @Component public class MailService { private final Logger logger = LoggerFactory.getLogger(MailService.class); @@ -33,18 +35,58 @@ public class MailService { @Value("${ovsx.mail.revoked-access-tokens.template:}") String revokedAccessTokensTemplate; + @Value("${ovsx.mail.access-token-expiry.subject:}") + String accessTokenExpirySubject; + + @Value("${ovsx.mail.access-token-expiry.template:}") + String accessTokenExpiryTemplate; + public MailService(@Autowired(required = false) JavaMailSender sender, JobRequestScheduler scheduler) { this.disabled = sender == null; this.scheduler = scheduler; } + public void scheduleAccessTokenExpiryNotification(PersonalAccessToken token) { + if (disabled) { + return; + } + + var user = token.getUser(); + var email = user.getEmail(); + + if (email == null) { + logger.warn("Could not send mail to user '{}' due to expired access token notification: email not known", user.getLoginName()); + return; + } + + // the fullName might be null + var name = user.getFullName() == null ? user.getLoginName() : user.getFullName(); + // the token description might be null as well + var tokenName = token.getDescription() != null ? token.getDescription() : ""; + + var variables = Map.of( + "name", name, + "tokenName", tokenName, + "expiryDate", token.getExpiresTimestamp() + ); + var jobRequest = new SendMailJobRequest( + email, + accessTokenExpirySubject, + accessTokenExpiryTemplate, + variables + ); + + scheduler.enqueue(jobRequest); + logger.debug("Scheduled notification email for expiring token {} to {}", tokenName, email); + } + public void scheduleRevokedAccessTokensMail(UserData user) { if (disabled) { return; } if (user.getEmail() == null) { - logger.info("Could not schedule mail to user '{}' due to revoked access tokens: email not known", user.getLoginName()); + logger.warn("Could not send mail to user '{}' due to revoked access token: email not known", user.getLoginName()); return; } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java index aa8a95bfe..8a9c9c990 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/DataMirrorService.java @@ -17,6 +17,7 @@ import org.eclipse.openvsx.LocalRegistryService; import org.eclipse.openvsx.UpstreamRegistryService; import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.admin.AdminService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.ExtensionJson; @@ -47,6 +48,7 @@ public class DataMirrorService { private final RepositoryService repositories; private final EntityManager entityManager; private final UserService users; + private final AccessTokenService tokens; private final ExtensionService extensions; private final UpstreamRegistryService upstream; private final LocalRegistryService local; @@ -68,6 +70,7 @@ public DataMirrorService( RepositoryService repositories, EntityManager entityManager, UserService users, + AccessTokenService tokens, ExtensionService extensions, UpstreamRegistryService upstream, LocalRegistryService local, @@ -81,6 +84,7 @@ public DataMirrorService( this.repositories = repositories; this.entityManager = entityManager; this.users = users; + this.tokens = tokens; this.extensions = extensions; this.upstream = upstream; this.local = local; @@ -169,7 +173,7 @@ public UserData getOrAddUser(UserJson json) { public String getOrAddAccessTokenValue(UserData user, String description) { var token = repositories.findAccessToken(user, description); return token == null - ? users.createAccessToken(user, description).getValue() + ? tokens.createAccessToken(user, description).getValue() : token.getValue(); } diff --git a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java index 4066ceae0..9fdee1abf 100644 --- a/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java +++ b/server/src/main/java/org/eclipse/openvsx/mirror/MirrorExtensionService.java @@ -11,7 +11,7 @@ import org.eclipse.openvsx.ExtensionService; import org.eclipse.openvsx.UpstreamRegistryService; -import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.json.ExtensionJson; import org.eclipse.openvsx.json.UserJson; @@ -32,7 +32,6 @@ import java.time.LocalDate; import java.util.*; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.zip.ZipFile; import static org.eclipse.openvsx.entities.FileResource.DOWNLOAD_SIG; @@ -48,7 +47,7 @@ public class MirrorExtensionService { private final UpstreamRegistryService upstream; private final RestTemplate backgroundRestTemplate; private final RestTemplate backgroundNonRedirectingRestTemplate; - private final UserService users; + private final AccessTokenService tokens; private final ExtensionService extensions; private final ExtensionVersionIntegrityService integrityService; @@ -58,7 +57,7 @@ public MirrorExtensionService( UpstreamRegistryService upstream, RestTemplate backgroundRestTemplate, RestTemplate backgroundNonRedirectingRestTemplate, - UserService users, + AccessTokenService tokens, ExtensionService extensions, ExtensionVersionIntegrityService integrityService ) { @@ -67,7 +66,7 @@ public MirrorExtensionService( this.upstream = upstream; this.backgroundRestTemplate = backgroundRestTemplate; this.backgroundNonRedirectingRestTemplate = backgroundNonRedirectingRestTemplate; - this.users = users; + this.tokens = tokens; this.extensions = extensions; this.integrityService = integrityService; } @@ -141,7 +140,7 @@ private void mirrorExtensionVersions(String namespaceName, String extensionName, versions.stream() .filter(version -> targetVersions.stream().noneMatch(extVersion -> extVersion.getVersion().equals(version))) .map(version -> upstream.getExtension(namespaceName, extensionName, targetPlatform, version)) - .collect(Collectors.toList()) + .toList() ); } toAdd.sort(Comparator.comparing(extensionJson -> TimeUtil.fromUTCString(extensionJson.getTimestamp()))); @@ -175,8 +174,8 @@ private void mirrorExtensionVersion(ExtensionJson json) { throw new AssertionError("Expected location header from redirected vsix url"); } - var tokens = vsixLocation.getPath().split("/"); - var filename = tokens[tokens.length-1]; + var segments = vsixLocation.getPath().split("/"); + var filename = segments[segments.length-1]; if (!filename.endsWith(".vsix")) { throw new AssertionError("Invalid vsix filename from redirected vsix url"); } @@ -206,7 +205,7 @@ private void mirrorExtensionVersion(ExtensionJson json) { var description = "MirrorExtensionVersion"; var accessTokenValue = data.getOrAddAccessTokenValue(user, description); - var token = users.useAccessToken(accessTokenValue); + var token = tokens.useAccessToken(accessTokenValue); extensions.mirrorVersion(extensionFile, signatureName, token, filename, json.getTimestamp()); logger.atDebug() .setMessage("completed mirroring of extension version: {}") diff --git a/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java b/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java index f6e0e13bb..8f6464a33 100644 --- a/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java +++ b/server/src/main/java/org/eclipse/openvsx/ratelimit/IdentityService.java @@ -14,7 +14,7 @@ import com.giffing.bucket4j.spring.boot.starter.context.ExpressionParams; import jakarta.servlet.http.HttpServletRequest; -import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.ratelimit.config.RateLimitConfig; import org.eclipse.openvsx.ratelimit.config.RateLimitProperties; import org.slf4j.Logger; @@ -39,7 +39,7 @@ public class IdentityService { private final TierService tierService; private final CustomerService customerService; - private final UserService userService; + private final AccessTokenService tokenService; private final RateLimitProperties rateLimitProperties; public IdentityService( @@ -47,14 +47,14 @@ public IdentityService( ConfigurableBeanFactory beanFactory, TierService tierService, CustomerService customerService, - UserService userService, + AccessTokenService tokenService, RateLimitProperties rateLimitProperties ) { this.expressionParser = expressionParser; this.beanFactory = beanFactory; this.tierService = tierService; this.customerService = customerService; - this.userService = userService; + this.tokenService = tokenService; this.rateLimitProperties = rateLimitProperties; } @@ -67,7 +67,7 @@ public ResolvedIdentity resolveIdentity(HttpServletRequest request) { // This will update the database with the time the token is last accessed, // but we need to ensure that we only take valid tokens into account for rate limiting. // If this turns out to be a bottleneck, we need to cache the token hashcode. - var tokenEntity = userService.useAccessToken(token); + var tokenEntity = tokenService.useAccessToken(token); if (tokenEntity != null) { // if a valid token is present we use it as a cache key cacheKey = "token_" + token.hashCode(); diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java index 52dc993b5..8b3195d30 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/PersonalAccessTokenRepository.java @@ -11,11 +11,15 @@ import org.eclipse.openvsx.entities.PersonalAccessToken; import org.eclipse.openvsx.entities.UserData; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; import org.springframework.data.util.Streamable; +import java.time.LocalDateTime; +import java.util.List; + public interface PersonalAccessTokenRepository extends Repository { Streamable findAll(); @@ -35,4 +39,14 @@ public interface PersonalAccessTokenRepository extends Repository findByExpiresTimestampLessThanEqualAndActiveTrueAndNotifiedFalseOrderById(LocalDateTime timestamp, Pageable pageable); + + @Modifying + @Query("update PersonalAccessToken t set t.active = false where t.expiresTimestamp <= ?1 and t.active = true") + int expireAccessTokens(LocalDateTime timestamp); } \ No newline at end of file diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index fda5b565b..cb13c80e9 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -390,6 +390,10 @@ public PersonalAccessToken findAccessToken(long id) { return tokenRepo.findById(id); } + public List findExpiringAccessTokensWithoutNotification(LocalDateTime expirationTime, Pageable pageable) { + return tokenRepo.findByExpiresTimestampLessThanEqualAndActiveTrueAndNotifiedFalseOrderById(expirationTime, pageable); + } + public Streamable findAllPersistedLogs() { return persistedLogRepo.findByOrderByTimestampAsc(); } @@ -647,6 +651,14 @@ public int deactivateAccessTokens(UserData user) { return tokenRepo.updateActiveSetFalse(user); } + public int expireAccessTokens(LocalDateTime timestamp) { + return tokenRepo.expireAccessTokens(timestamp); + } + + public int updateExpiresTimeForLegacyAccessTokens(LocalDateTime timestamp) { + return tokenRepo.updateExpiresTimeForLegacyAccessTokens(timestamp); + } + public List findActiveExtensionNames(Namespace namespace) { return extensionJooqRepo.findActiveExtensionNames(namespace); } diff --git a/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java b/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java index afd3005ec..beec1cbc1 100644 --- a/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java +++ b/server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java @@ -13,9 +13,8 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.UserService; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.UserData; -import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.util.ErrorResultException; import org.springframework.context.event.EventListener; import org.springframework.security.authentication.AuthenticationServiceException; @@ -45,8 +44,7 @@ public class OAuth2UserServices { private final UserService users; - private final TokenService tokens; - private final RepositoryService repositories; + private final EclipseTokenService tokens; private final EntityManager entityManager; private final EclipseService eclipse; private final OAuth2AttributesConfig attributesConfig; @@ -55,15 +53,13 @@ public class OAuth2UserServices { public OAuth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService tokens, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { this.users = users; this.tokens = tokens; - this.repositories = repositories; this.entityManager = entityManager; this.eclipse = eclipse; this.attributesConfig = attributesConfig; diff --git a/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql b/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql new file mode 100644 index 000000000..1294a18ae --- /dev/null +++ b/server/src/main/resources/db/migration/V1_63__PersonalAccessToken_ExpiresTimestamp.sql @@ -0,0 +1,7 @@ +-- add new columns wrt access token expiration +ALTER TABLE personal_access_token + ADD COLUMN expires_timestamp TIMESTAMP WITHOUT TIME ZONE, + ADD COLUMN notified BOOLEAN; + +UPDATE personal_access_token + SET expires_timestamp = NULL, notified = FALSE; diff --git a/server/src/main/resources/mail-templates/access-token-expiry-notification.html b/server/src/main/resources/mail-templates/access-token-expiry-notification.html new file mode 100644 index 000000000..cf1a5bc24 --- /dev/null +++ b/server/src/main/resources/mail-templates/access-token-expiry-notification.html @@ -0,0 +1,17 @@ + + + + + + +

Hi John Doe,

+

+ Your access token Test token will expire on 14-11-2025. + Please generate a new token to continue accessing the service. +

+

+ Regards,
+ The Open VSX Team +

+ + diff --git a/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java b/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java index 10cffed5c..63f1d399a 100644 --- a/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/LocalRegistryServiceTest.java @@ -13,6 +13,7 @@ package org.eclipse.openvsx; import jakarta.persistence.EntityManager; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.eclipse.EclipseService; import org.eclipse.openvsx.entities.Namespace; @@ -62,6 +63,9 @@ class LocalRegistryServiceTest { @Mock UserService users; + @Mock + AccessTokenService tokens; + @Mock SearchUtilService searchUtilService; @@ -93,6 +97,7 @@ void setUp() { extensions, versions, users, + tokens, searchUtilService, validator, storageUtilService, diff --git a/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java b/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java index eec3a188a..921dc1ac0 100644 --- a/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java +++ b/server/src/test/java/org/eclipse/openvsx/MockTransactionTemplate.java @@ -29,4 +29,4 @@ public T execute(TransactionCallback action) throws TransactionException public void afterPropertiesSet() { // Method override to prevent IllegalArgumentException from being thrown } -} \ No newline at end of file +} diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index 8950b37a8..a1376f2bb 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -14,15 +14,18 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.ExtensionJsonCacheKeyGenerator; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.extension_control.ExtensionControlService; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.publish.PublishExtensionVersionService; @@ -93,7 +96,7 @@ AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, EclipseService.class, PublishExtensionVersionService.class, SimpleMeterRegistry.class, JobRequestScheduler.class, ExtensionControlService.class, FileCacheDurationConfig.class, CdnServiceConfig.class, - ExtensionScanPersistenceService.class, LogService.class + ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class, MailService.class }) class RegistryAPITest { @@ -2552,25 +2555,34 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService tokenService( + AccessTokenConfig config, + EntityManager entityManager, + RepositoryService repositories, + MailService mailService + ) { + return new AccessTokenService(config, entityManager, repositories, mailService); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -2578,29 +2590,31 @@ LocalRegistryService localRegistryService( EntityManager entityManager, RepositoryService repositories, ExtensionService extensions, - @Qualifier("registryTest") VersionService versions, + VersionService versionService, UserService users, + AccessTokenService tokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, repositories, extensions, - versions, + versionService, users, + tokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } @@ -2674,7 +2688,6 @@ LocalStorageService localStorageService() { ExtensionJsonCacheKeyGenerator extensionJsonCacheKeyGenerator() { return new ExtensionJsonCacheKeyGenerator(); } @Bean - @Qualifier("registryTest") VersionService versionService() { return new VersionService(); } diff --git a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java index 2ec958d2a..8c21a0c1f 100644 --- a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java @@ -13,12 +13,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; +import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.repositories.RepositoryService; @@ -75,13 +78,16 @@ EclipseService.class, ClientRegistrationRepository.class, StorageUtilService.class, CacheService.class, ExtensionValidator.class, SimpleMeterRegistry.class, SearchUtilService.class, PublishExtensionVersionHandler.class, JobRequestScheduler.class, VersionService.class, ExtensionVersionIntegrityService.class, ExtensionScanService.class, - ExtensionScanPersistenceService.class, LogService.class + ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class, MailService.class }) class UserAPITest { @MockitoSpyBean UserService users; + @MockitoSpyBean + AccessTokenService tokens; + @MockitoBean EntityManager entityManager; @@ -143,7 +149,7 @@ void testAccessTokensNotLoggedIn() throws Exception { @Test void testCreateAccessToken() throws Exception { mockUserData(); - Mockito.doReturn("foobar").when(users).generateTokenValue(); + Mockito.doReturn("foobar").when(tokens).generateTokenValue(); mockMvc.perform(post("/user/token/create?description={description}", "This is my token") .with(user("test_user")) .with(csrf().asHeader())) @@ -177,7 +183,7 @@ void testDeleteAccessToken() throws Exception { .with(user("test_user")) .with(csrf().asHeader())) .andExpect(status().isOk()) - .andExpect(content().json(successJson("Deleted access token for user test_user."))); + .andExpect(content().json(successJson("Deactivated access token for user test_user."))); } @Test @@ -789,25 +795,34 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService accessTokenService( + AccessTokenConfig config, + EntityManager entityManager, + RepositoryService repositories, + MailService mailService + ) { + return new AccessTokenService(config, entityManager, repositories, mailService); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -822,13 +837,14 @@ LocalRegistryService localRegistryService( ExtensionService extensions, VersionService versions, UserService users, + AccessTokenService accessTokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, @@ -836,13 +852,14 @@ LocalRegistryService localRegistryService( extensions, versions, users, + accessTokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java index 0da5dfe69..c688c9a2a 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java @@ -40,7 +40,7 @@ @ExtendWith(SpringExtension.class) @MockitoBean( types = { VSCodeAPI.class, SimpleMeterRegistry.class, SearchUtilService.class, - VersionService.class, StorageUtilService.class, ExtensionVersionIntegrityService.class, + StorageUtilService.class, ExtensionVersionIntegrityService.class, WebResourceService.class, CacheService.class }) public class LocalVSCodeServiceTest { @@ -118,11 +118,6 @@ private ExtensionVersion mockExtensionVersion(Extension extension, long id, Stri @TestConfiguration static class TestConfig { - @Bean - VersionService versionService() { - return new VersionService(); - } - @Bean LocalVSCodeService vsCodeService( RepositoryService repositories, 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 5e611e7bc..7bed165ce 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -20,7 +20,7 @@ import org.eclipse.openvsx.cache.FilesCacheKeyGenerator; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.repositories.RepositoryService; @@ -1021,8 +1021,8 @@ private Path mockExtensionBrowse(String namespaceName, String extensionName, Str @Import(SecurityConfig.class) static class TestConfig { @Bean - IExtensionQueryRequestHandler extensionQueryRequestHandler(LocalVSCodeService local, UpstreamVSCodeService upstream) { - return new DefaultExtensionQueryRequestHandler(local, upstream); + IExtensionQueryRequestHandler extensionQueryRequestHandler(LocalVSCodeService localVSCodeService, UpstreamVSCodeService upstream) { + return new DefaultExtensionQueryRequestHandler(localVSCodeService, upstream); } @Bean @@ -1033,22 +1033,21 @@ TransactionTemplate transactionTemplate() { @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -1064,14 +1063,14 @@ WebResourceService webResourceService( @Bean LocalVSCodeService localVSCodeService( RepositoryService repositories, - VersionService versions, + VersionService versionService, SearchUtilService search, StorageUtilService storageUtil, ExtensionVersionIntegrityService integrityService, WebResourceService webResourceService, CacheService cache ) { - return new LocalVSCodeService(repositories, versions, search, storageUtil, integrityService, webResourceService, cache); + return new LocalVSCodeService(repositories, versionService, search, storageUtil, integrityService, webResourceService, cache); } @Bean @@ -1124,7 +1123,7 @@ LocalStorageService localStorage() { } @Bean - VersionService getVersionService() { + VersionService versionService() { return new VersionService(); } 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 1af76058a..dcf0c5c68 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java @@ -38,7 +38,7 @@ class VSCodeIdUpdateServiceTest { VSCodeIdUpdateService updateService; @Test - void testUpdateAllNoChanges() throws InterruptedException { + void testUpdateAllNoChanges() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -99,7 +99,7 @@ void testUpdateAllNoChanges() throws InterruptedException { } @Test - void testUpdateAllRandomNoChanges() throws InterruptedException { + void testUpdateAllRandomNoChanges() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -160,7 +160,7 @@ void testUpdateAllRandomNoChanges() throws InterruptedException { } @Test - void testUpdateAllChange() throws InterruptedException { + void testUpdateAllChange() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -230,7 +230,7 @@ void testUpdateAllChange() throws InterruptedException { } @Test - void testUpdateRandom() throws InterruptedException { + void testUpdateRandom() { var namespaceName = "foo"; var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; @@ -259,7 +259,7 @@ void testUpdateRandom() throws InterruptedException { } @Test - void testUpdateRandomExistsDb() throws InterruptedException { + void testUpdateRandomExistsDb() { var namespaceName = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var namespacePublicId2 = UUID.randomUUID().toString(); @@ -292,7 +292,7 @@ void testUpdateRandomExistsDb() throws InterruptedException { } @Test - void testMustUpdateRandom() throws InterruptedException { + void testMustUpdateRandom() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -345,7 +345,7 @@ void testMustUpdateRandom() throws InterruptedException { } @Test - void testMustUpdateRandomExists() throws InterruptedException { + void testMustUpdateRandomExists() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -398,7 +398,7 @@ void testMustUpdateRandomExists() throws InterruptedException { } @Test - void testMustUpdateRandomExistsDb() throws InterruptedException { + void testMustUpdateRandomExistsDb() { var namespaceName1 = "foo"; var namespaceUuid1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -456,7 +456,7 @@ void testMustUpdateRandomExistsDb() throws InterruptedException { @Test - void testUpdateNoChange() throws InterruptedException { + void testUpdateNoChange() { var namespaceName = "foo"; var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; @@ -484,7 +484,7 @@ void testUpdateNoChange() throws InterruptedException { } @Test - void testUpdateUpstream() throws InterruptedException { + void testUpdateUpstream() { var namespaceName = "foo"; var namespacePublicId = "123-456-789"; var extensionName = "bar"; @@ -514,7 +514,7 @@ void testUpdateUpstream() throws InterruptedException { } @Test - void testUpdateDuplicateRecursive() throws InterruptedException { + void testUpdateDuplicateRecursive() { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; 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 eff82205f..22a580713 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -14,11 +14,13 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.persistence.EntityManager; import org.eclipse.openvsx.*; +import org.eclipse.openvsx.accesstoken.AccessTokenConfig; +import org.eclipse.openvsx.accesstoken.AccessTokenService; import org.eclipse.openvsx.adapter.VSCodeIdService; import org.eclipse.openvsx.cache.CacheService; import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.json.*; import org.eclipse.openvsx.mail.MailService; @@ -80,7 +82,7 @@ AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, PublishExtensionVersionHandler.class, SearchUtilService.class, EclipseService.class, SimpleMeterRegistry.class, FileCacheDurationConfig.class, MailService.class, CdnServiceConfig.class, - ExtensionScanService.class, ExtensionScanPersistenceService.class, LogService.class + ExtensionScanService.class, ExtensionScanPersistenceService.class, LogService.class, AccessTokenConfig.class }) class AdminAPITest { @@ -651,7 +653,7 @@ void testRevokePublisherAgreement() throws Exception { .andExpect(content().json(successJson("Deactivated 1 tokens, deactivated 1 extensions of user github/test."))); assertThat(token.isActive()).isFalse(); - assertThat(versions.get(0).isActive()).isFalse(); + assertThat(versions.getFirst().isActive()).isFalse(); } @Test @@ -1454,25 +1456,34 @@ UserService userService( return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository, attributesConfig); } + @Bean + AccessTokenService tokenService( + AccessTokenConfig config, + EntityManager entityManager, + RepositoryService repositories, + MailService mailService + ) { + return new AccessTokenService(config, entityManager, repositories, mailService); + } + @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean - TokenService tokenService( + EclipseTokenService eclipseTokenService( TransactionTemplate transactions, EntityManager entityManager, ClientRegistrationRepository clientRegistrationRepository ) { - return new TokenService(transactions, entityManager, clientRegistrationRepository); + return new EclipseTokenService(transactions, entityManager, clientRegistrationRepository); } @Bean @@ -1481,6 +1492,7 @@ AdminService adminService( ExtensionService extensions, EntityManager entityManager, UserService users, + AccessTokenService tokenService, ExtensionValidator validator, SearchUtilService search, EclipseService eclipse, @@ -1496,6 +1508,7 @@ AdminService adminService( extensions, entityManager, users, + tokenService, validator, search, eclipse, @@ -1513,30 +1526,31 @@ LocalRegistryService localRegistryService( EntityManager entityManager, RepositoryService repositories, ExtensionService extensions, - @Qualifier("adminTest") VersionService versions, + VersionService versionService, UserService users, + AccessTokenService tokenService, SearchUtilService search, ExtensionValidator validator, StorageUtilService storageUtil, EclipseService eclipse, CacheService cache, - FileCacheDurationConfig fileCacheDurationConfig, ExtensionVersionIntegrityService integrityService, - SimilarityService similarityService + SimilarityCheckService similarityCheckService ) { return new LocalRegistryService( entityManager, repositories, extensions, - versions, + versionService, users, + tokenService, search, validator, storageUtil, eclipse, cache, integrityService, - similarityCheckService(similarityConfig(), similarityService(repositories), repositories) + similarityCheckService ); } @@ -1626,7 +1640,6 @@ LocalStorageService localStorage() { } @Bean - @Qualifier("adminTest") VersionService versionService() { return new VersionService(); } 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 bfc9771ec..6552d775e 100644 --- a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java @@ -75,7 +75,7 @@ class EclipseServiceTest { RepositoryService repositories; @MockitoBean - TokenService tokens; + EclipseTokenService tokens; @MockitoBean RestTemplate restTemplate; @@ -399,7 +399,7 @@ TransactionTemplate transactionTemplate() { @Bean EclipseService eclipseService( - TokenService tokens, + EclipseTokenService tokens, ExtensionService extensions, EntityManager entityManager, RestTemplate restTemplate @@ -478,5 +478,4 @@ LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator( return new LatestExtensionVersionCacheKeyGenerator(); } } - } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index f0b433269..761e83453 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; import java.lang.reflect.Modifier; @@ -282,6 +281,9 @@ void testExecuteQueries() { () -> repositories.findLatestVersion(userData, "namespaceName", "extensionName"), () -> repositories.isDeleteAllVersions("namespaceName", "extensionName", Collections.emptyList(), userData), () -> repositories.deactivateAccessTokens(userData), + () -> repositories.expireAccessTokens(NOW), + () -> repositories.findExpiringAccessTokensWithoutNotification(NOW, page), + () -> repositories.updateExpiresTimeForLegacyAccessTokens(NOW), () -> repositories.findSimilarExtensionsByLevenshtein("extensionName", "namespaceName", "displayName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findSimilarNamespacesByLevenshtein("namespaceName", Collections.emptyList(), 0.5, false, 10), () -> repositories.findExtensionScans(extVersion), diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java index eca56505b..ba09022f0 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java @@ -138,5 +138,4 @@ void threat_allowsNullDescription() { assertNull(threat.getDescription()); assertEquals("LOW", threat.getSeverity()); } - } 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 efdced8d6..bf005c4f3 100644 --- a/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java @@ -299,5 +299,4 @@ LatestExtensionVersionCacheKeyGenerator latestExtensionVersionCacheKeyGenerator( return new LatestExtensionVersionCacheKeyGenerator(); } } - } \ No newline at end of file diff --git a/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java b/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java index 49d5ddbf1..6626e293b 100644 --- a/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java +++ b/server/src/test/java/org/eclipse/openvsx/util/CollectionUtilTest.java @@ -18,10 +18,9 @@ class CollectionUtilTest { @Test - void testLimit() throws Exception { + void testLimit() { var source = Lists.newArrayList(1, 2, 3, 4, 5); var result = CollectionUtil.limit(source, 3); assertThat(result).isEqualTo(Lists.newArrayList(1, 2, 3)); } - } \ No newline at end of file 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 b38a7fc54..a46dda194 100644 --- a/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/web/SitemapControllerTest.java @@ -13,7 +13,7 @@ import jakarta.persistence.EntityManager; import org.eclipse.openvsx.UserService; import org.eclipse.openvsx.eclipse.EclipseService; -import org.eclipse.openvsx.eclipse.TokenService; +import org.eclipse.openvsx.eclipse.EclipseTokenService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.security.OAuth2AttributesConfig; import org.eclipse.openvsx.security.OAuth2UserServices; @@ -38,7 +38,7 @@ @WebMvcTest(SitemapController.class) @AutoConfigureWebClient @MockitoBean(types = { - EclipseService.class, SimpleMeterRegistry.class, UserService.class, TokenService.class, EntityManager.class + EclipseService.class, SimpleMeterRegistry.class, UserService.class, EclipseTokenService.class, EntityManager.class }) class SitemapControllerTest { @@ -73,13 +73,12 @@ static class TestConfig { @Bean OAuth2UserServices oauth2UserServices( UserService users, - TokenService tokens, - RepositoryService repositories, + EclipseTokenService eclipseTokenService, EntityManager entityManager, EclipseService eclipse, OAuth2AttributesConfig attributesConfig ) { - return new OAuth2UserServices(users, tokens, repositories, entityManager, eclipse, attributesConfig); + return new OAuth2UserServices(users, eclipseTokenService, entityManager, eclipse, attributesConfig); } @Bean diff --git a/webui/src/extension-registry-types.ts b/webui/src/extension-registry-types.ts index b3c267c14..6bbdb2ae9 100644 --- a/webui/src/extension-registry-types.ts +++ b/webui/src/extension-registry-types.ts @@ -180,6 +180,8 @@ export interface PersonalAccessToken { value?: string; createdTimestamp: TimestampString; accessedTimestamp?: TimestampString; + expiresTimestamp?: TimestampString; + notified?: boolean; description: string; deleteTokenUrl: UrlString; } diff --git a/webui/src/pages/user/user-settings-tokens.tsx b/webui/src/pages/user/user-settings-tokens.tsx index 4a97dfca4..88a001362 100644 --- a/webui/src/pages/user/user-settings-tokens.tsx +++ b/webui/src/pages/user/user-settings-tokens.tsx @@ -105,6 +105,7 @@ export const UserSettingsTokens: FunctionComponent = () => { {token.description} Created: + Expires: {token.expiresTimestamp ? : 'never'} Accessed: {token.accessedTimestamp ? : 'never'} diff --git a/webui/src/utils.ts b/webui/src/utils.ts index b8c9fd9f1..bedf77f65 100644 --- a/webui/src/utils.ts +++ b/webui/src/utils.ts @@ -63,23 +63,46 @@ export function toRelativeTime(timestamp?: string): string | undefined { } const date = new Date(timestamp); const elapsed = Date.now() - date.getTime(); - if (elapsed < msPerMinute) { - return 'now'; - } else if (elapsed < msPerHour) { - const value = Math.round(elapsed / msPerMinute); - return `${value} minute${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerDay) { - const value = Math.round(elapsed / msPerHour); - return `${value} hour${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerMonth) { - const value = Math.round(elapsed / msPerDay); - return `${value} day${value !== 1 ? 's' : ''} ago`; - } else if (elapsed < msPerYear) { - const value = Math.round(elapsed / msPerMonth); - return `${value} month${value !== 1 ? 's' : ''} ago`; + + if (elapsed < 0) { + const remaining = -elapsed + if (remaining < msPerMinute) { + return 'now'; + } else if (remaining < msPerHour) { + const value = Math.round(remaining / msPerMinute); + return `in ${value} minute${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerDay) { + const value = Math.round(remaining / msPerHour); + return `in ${value} hour${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerMonth) { + const value = Math.round(remaining / msPerDay); + return `in ${value} day${value !== 1 ? 's' : ''}`; + } else if (remaining < msPerYear) { + const value = Math.round(remaining / msPerMonth); + return `in ${value} month${value !== 1 ? 's' : ''}`; + } else { + const value = Math.round(remaining / msPerYear); + return `in ${value} year${value !== 1 ? 's' : ''}`; + } } else { - const value = Math.round(elapsed / msPerYear); - return `${value} year${value !== 1 ? 's' : ''} ago`; + if (elapsed < msPerMinute) { + return 'now'; + } else if (elapsed < msPerHour) { + const value = Math.round(elapsed / msPerMinute); + return `${value} minute${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerDay) { + const value = Math.round(elapsed / msPerHour); + return `${value} hour${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerMonth) { + const value = Math.round(elapsed / msPerDay); + return `${value} day${value !== 1 ? 's' : ''} ago`; + } else if (elapsed < msPerYear) { + const value = Math.round(elapsed / msPerMonth); + return `${value} month${value !== 1 ? 's' : ''} ago`; + } else { + const value = Math.round(elapsed / msPerYear); + return `${value} year${value !== 1 ? 's' : ''} ago`; + } } }