Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/src/dev/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ 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'
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/** ******************************************************************************
* 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.access_token;

import org.eclipse.openvsx.entities.PersonalAccessToken;
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.springframework.stereotype.Component;

@Component
public class ExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler<HandlerJobRequest> {

private final RepositoryService repositories;

public ExpirePersonalAccessTokensJobRequestHandler(RepositoryService repositories) {
this.repositories = repositories;
}

@Override
public void run(HandlerJobRequest handlerJobRequest) throws Exception {
var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS);
repositories.expireAccessTokens(timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** ******************************************************************************
* 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.access_token;

import org.eclipse.openvsx.entities.PersonalAccessToken;
import org.eclipse.openvsx.mail.MailService;
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.springframework.stereotype.Component;

@Component
public class NotifyPersonalAccessTokenExpiryJobRequestHandler implements JobRequestHandler<HandlerJobRequest> {

private final MailService mail;
private final RepositoryService repositories;

public NotifyPersonalAccessTokenExpiryJobRequestHandler(MailService mail, RepositoryService repositories) {
this.mail = mail;
this.repositories = repositories;
}

@Override
public void run(HandlerJobRequest handlerJobRequest) throws Exception {
var timestamp = TimeUtil.getCurrentUTC().minusDays(PersonalAccessToken.EXPIRY_DAYS - 7);
var tokens = repositories.findAccessTokensCreatedBefore(timestamp);
tokens.forEach(mail::scheduleAccessTokenExpiryNotification);
}
}
Original file line number Diff line number Diff line change
@@ -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.access_token;

import org.eclipse.openvsx.migration.HandlerJobRequest;
import org.jobrunr.jobs.lambdas.JobRequestHandler;
import org.jobrunr.scheduling.JobRequestScheduler;
import org.jobrunr.scheduling.cron.Cron;
import org.springframework.stereotype.Component;

import java.time.ZoneId;

@Component
public class ScheduleExpirePersonalAccessTokensJobRequestHandler implements JobRequestHandler<HandlerJobRequest> {

private final JobRequestScheduler scheduler;

public ScheduleExpirePersonalAccessTokensJobRequestHandler(JobRequestScheduler scheduler) {
this.scheduler = scheduler;
}

@Override
public void run(HandlerJobRequest handlerJobRequest) throws Exception {
var zone = ZoneId.of("UTC");
var expireSchedule = Cron.daily(1, 38);
var expireJobRequest = new HandlerJobRequest<>(ExpirePersonalAccessTokensJobRequestHandler.class);
scheduler.scheduleRecurrently("ExpirePersonalAccessTokens", expireSchedule, zone, expireJobRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/** ******************************************************************************
* 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.access_token;

import org.eclipse.openvsx.migration.HandlerJobRequest;
import org.eclipse.openvsx.util.TimeUtil;
import org.jobrunr.scheduling.JobRequestScheduler;
import org.jobrunr.scheduling.cron.Cron;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.UUID;

@Component
public class SchedulePersonalAccessTokenJobsListener {

private final JobRequestScheduler scheduler;

@Value("${ovsx.access-token-expire.delay:0}")
int delay;

public SchedulePersonalAccessTokenJobsListener(JobRequestScheduler scheduler) {
this.scheduler = scheduler;
}

@EventListener
public void applicationStarted(ApplicationStartedEvent event) {
var jobIdText = "ScheduleExpirePersonalAccessTokens";
var jobId = UUID.nameUUIDFromBytes(jobIdText.getBytes(StandardCharsets.UTF_8));
var scheduleExpireJobRequest = new HandlerJobRequest<>(ScheduleExpirePersonalAccessTokensJobRequestHandler.class);
if(delay > 0) {
scheduler.schedule(jobId, TimeUtil.getCurrentUTC().plusDays(delay), scheduleExpireJobRequest);
} else {
scheduler.enqueue(jobId, scheduleExpireJobRequest);
}

var zone = ZoneId.of("UTC");
var notifySchedule = Cron.daily(2, 8);
var notifyJobRequest = new HandlerJobRequest<>(NotifyPersonalAccessTokenExpiryJobRequestHandler.class);
scheduler.scheduleRecurrently("NotifyPersonalAccessTokenExpiry", notifySchedule, zone, notifyJobRequest);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
30 changes: 30 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/mail/MailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.springframework.beans.factory.annotation.Autowired;
Expand All @@ -18,6 +19,8 @@

import java.util.Map;

import static org.eclipse.openvsx.entities.PersonalAccessToken.EXPIRY_DAYS;

@Component
public class MailService {

Expand All @@ -30,11 +33,38 @@ 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 variables = Map.<String, Object>of(
"name", user.getFullName(),
"tokenName", token.getDescription(),
"expiryDate", token.getCreatedTimestamp().plusDays(EXPIRY_DAYS)
);
var jobRequest = new SendMailJobRequest(
user.getEmail(),
accessTokenExpirySubject,
accessTokenExpiryTemplate,
variables
);

scheduler.enqueue(jobRequest);
}

public void scheduleRevokedAccessTokensMail(UserData user) {
if(disabled) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import org.springframework.data.repository.Repository;
import org.springframework.data.util.Streamable;

import java.time.LocalDateTime;

public interface PersonalAccessTokenRepository extends Repository<PersonalAccessToken, Long> {

Streamable<PersonalAccessToken> findAll();
Expand All @@ -35,4 +37,10 @@ public interface PersonalAccessTokenRepository extends Repository<PersonalAccess
@Modifying
@Query("update PersonalAccessToken t set t.active = false where t.user = ?1 and t.active = true")
int updateActiveSetFalse(UserData user);

Streamable<PersonalAccessToken> findByCreatedTimestampLessThanEqualAndActiveTrue(LocalDateTime timestamp);

@Modifying
@Query("update PersonalAccessToken t set t.active = false where t.createdTimestamp <= ?1 and t.active = true")
void expireAccessTokens(LocalDateTime timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -659,4 +659,12 @@ public List<MigrationItem> findRemoveFileResourceTypeResourceMigrationItems(int
public boolean isDeleteAllVersions(String namespaceName, String extensionName, List<TargetPlatformVersionJson> targetVersions, UserData user) {
return extensionVersionJooqRepo.isDeleteAllVersions(namespaceName, extensionName, targetVersions, user);
}

public Streamable<PersonalAccessToken> findAccessTokensCreatedBefore(LocalDateTime timestamp) {
return tokenRepo.findByCreatedTimestampLessThanEqualAndActiveTrue(timestamp);
}

public void expireAccessTokens(LocalDateTime timestamp) {
tokenRepo.expireAccessTokens(timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>Hi <span th:text="${name}">John Doe</span>,</p>
<p>Your access token <code th:text="${tokenName}">Test token</code> will expire on <span th:text="${#temporals.format(expiryDate, 'dd-MM-YYYY')}">14-11-2025</span>.</p>
<p>
Regards, <br />
<em>The Open VSX Team</em>
</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ void testExecuteQueries() {
() -> repositories.findVersion(userData,"version", "targetPlatform", "extensionName", "namespace"),
() -> repositories.findLatestVersion(userData, "namespaceName", "extensionName"),
() -> repositories.isDeleteAllVersions("namespaceName", "extensionName", Collections.emptyList(), userData),
() -> repositories.deactivateAccessTokens(userData)
() -> repositories.deactivateAccessTokens(userData),
() -> repositories.expireAccessTokens(NOW),
() -> repositories.findAccessTokensCreatedBefore(NOW)
);

// check that we did not miss anything
Expand Down