GitHubService.java
/*
* BSD 2-Clause License
*
* Copyright (c) 2022, [Aleksandra Serba, Marcin Czerniak, Bartosz Wawrzyniak, Adrian Antkowiak]
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package dev.vernite.vernite.integration.git.github;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import org.apache.hc.core5.net.URIBuilder;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import dev.vernite.vernite.common.exception.ExternalApiException;
import dev.vernite.vernite.integration.git.Branch;
import dev.vernite.vernite.integration.git.Issue;
import dev.vernite.vernite.integration.git.PullRequest;
import dev.vernite.vernite.integration.git.Repository;
import dev.vernite.vernite.integration.git.github.api.GitHubApiClient;
import dev.vernite.vernite.integration.git.github.api.GitHubConfiguration;
import dev.vernite.vernite.integration.git.github.api.model.BranchName;
import dev.vernite.vernite.integration.git.github.api.model.GitHubComment;
import dev.vernite.vernite.integration.git.github.api.model.GitHubIssue;
import dev.vernite.vernite.integration.git.github.api.model.GitHubPullRequest;
import dev.vernite.vernite.integration.git.github.api.model.GitHubRelease;
import dev.vernite.vernite.integration.git.github.api.model.GitHubRepository;
import dev.vernite.vernite.integration.git.github.api.model.Installations;
import dev.vernite.vernite.integration.git.github.api.model.Repositories;
import dev.vernite.vernite.integration.git.github.api.model.request.OauthRefreshTokenRequest;
import dev.vernite.vernite.integration.git.github.api.model.request.OauthTokenRequest;
import dev.vernite.vernite.integration.git.github.model.Authorization;
import dev.vernite.vernite.integration.git.github.model.AuthorizationRepository;
import dev.vernite.vernite.integration.git.github.model.CommentIntegration;
import dev.vernite.vernite.integration.git.github.model.CommentIntegrationRepository;
import dev.vernite.vernite.integration.git.github.model.Installation;
import dev.vernite.vernite.integration.git.github.model.InstallationRepository;
import dev.vernite.vernite.integration.git.github.model.ProjectIntegration;
import dev.vernite.vernite.integration.git.github.model.ProjectIntegrationRepository;
import dev.vernite.vernite.integration.git.github.model.TaskIntegration;
import dev.vernite.vernite.integration.git.github.model.TaskIntegrationId;
import dev.vernite.vernite.integration.git.github.model.TaskIntegrationRepository;
import dev.vernite.vernite.project.Project;
import dev.vernite.vernite.release.Release;
import dev.vernite.vernite.task.Task;
import dev.vernite.vernite.task.comment.Comment;
import dev.vernite.vernite.user.User;
import io.jsonwebtoken.Jwts;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Service for GitHub integration.
*/
@Service
public class GitHubService {
private final GitHubApiClient client;
private GitHubConfiguration config;
private AuthorizationRepository authorizationRepository;
private InstallationRepository installationRepository;
private ProjectIntegrationRepository projectIntegrationRepository;
private TaskIntegrationRepository taskIntegrationRepository;
private CommentIntegrationRepository commentIntegrationRepository;
public GitHubService(GitHubConfiguration config, AuthorizationRepository authorizationRepository,
InstallationRepository installationRepository, ProjectIntegrationRepository projectIntegrationRepository,
TaskIntegrationRepository taskIntegrationRepository,
CommentIntegrationRepository commentIntegrationRepository) {
this.config = config;
this.authorizationRepository = authorizationRepository;
this.installationRepository = installationRepository;
this.projectIntegrationRepository = projectIntegrationRepository;
this.taskIntegrationRepository = taskIntegrationRepository;
this.commentIntegrationRepository = commentIntegrationRepository;
var webClient = WebClient.builder().baseUrl(config.getApiURL())
.defaultStatusHandler(HttpStatusCode::isError,
resp -> Mono.error(new ExternalApiException("github", "github error" + resp.statusCode())))
.build();
var adapter = WebClientAdapter.forClient(webClient);
client = HttpServiceProxyFactory.builder(adapter).build().createClient(GitHubApiClient.class);
}
/**
* Get the GitHub OAuth installation URL for the given state.
*
* @param state the state to pass to GitHub
* @return the installation URL
* @throws URISyntaxException if the URL cannot be built
*/
public URI getAuthorizationUrl(String state) throws URISyntaxException {
var builder = new URIBuilder(GitHubConfiguration.GITHUB_AUTH_URL);
builder.addParameter("client_id", config.getClientId());
builder.addParameter("state", state);
return builder.build();
}
/**
* Create an authorization for the given user and code.
*
* @param user the user to create the authorization for
* @param code the code to use to create the authorization
* @return the authorization
*/
public Mono<Authorization> createAuthorization(User user, String code) {
var request = new OauthTokenRequest(config.getClientId(), config.getClientSecret(), code);
return client.createOauthAccessToken(request)
.flatMap(token -> client.getAuthenticatedUser("Bearer " + token.getAccessToken()).map(githubUser -> {
var auth = authorizationRepository.findById(githubUser.getId()).orElseGet(Authorization::new);
auth.update(token, githubUser, user);
return authorizationRepository.save(auth);
}));
}
/**
* Retrieves repositories available for user from GitHub api.
*
* @param user the user
* @return list with all repositories
*/
public Flux<Repository> getUserRepositories(User user) {
return Flux.fromIterable(authorizationRepository.findByUser(user))
.flatMap(this::refreshToken)
.flatMap(this::getUserInstallations)
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMap(client::getInstallationRepositories)
.flatMapIterable(Repositories::getRepositoryList)
.map(repo -> new Repository(repo.getId(), repo.getName(), repo.getFullName(), repo.getHtmlUrl(),
repo.isPrivate(), "github"));
}
/**
* Create a project integration for the given project and repository.
*
* @param user the user; must have an authorization to the
* repository
* @param project the project
* @param repositoryFullName the repository full name
* @return the project integration
*/
public Mono<ProjectIntegration> createProjectIntegration(User user, Project project, String repositoryFullName) {
return Flux.fromIterable(authorizationRepository.findByUser(user))
.flatMap(this::refreshToken)
.flatMap(this::getUserInstallations)
.flatMap(this::refreshToken)
.filterWhen(inst -> hasRepository(inst, repositoryFullName))
.reduce(Optional.<Installation>empty(), (acc, inst) -> Optional.of(inst))
.filter(Optional::isPresent)
.map(Optional::get)
.map(inst -> new ProjectIntegration(repositoryFullName, project, inst))
.map(projectIntegrationRepository::save);
}
/**
* Get issues for the given project.
*
* @param project the project
* @return the issues
*/
public Flux<Issue> getIssues(Project project) {
var integrationOptional = projectIntegrationRepository.findByProject(project);
if (integrationOptional.isEmpty()) {
return Flux.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMapMany(token -> client.getRepositoryIssues(token, owner, repo))
.map(GitHubIssue::toIssue);
}
/**
* Connect the given task to the given issue.
*
* @param task the task
* @param id the issue id
* @return the issue
*/
public Mono<Issue> connectIssue(Task task, long id) {
var integrationOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
Set<Long> assignees = new HashSet<>();
if (task.getAssignee() != null) {
authorizationRepository.findByUser(task.getAssignee()).forEach(auth -> assignees.add(auth.getId()));
}
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMap(token -> client.getRepositoryIssue(token, owner, repo, id))
.map(GitHubIssue::toIssue)
.map(issue -> new TaskIntegration(task, integration, id, TaskIntegration.Type.ISSUE))
.map(taskIntegrationRepository::save)
.then(patchIssue(task));
}
/**
* Create a GitHub issue for the given task.
*
* @param task the task
* @return the issue
*/
public Mono<Issue> createIssue(Task task) {
var integrationOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
var issue = new GitHubIssue(0, null, null, task.getName(), task.getDescription(), new ArrayList<>());
Set<Long> assignees = new HashSet<>();
if (task.getAssignee() != null) {
authorizationRepository.findByUser(task.getAssignee()).forEach(auth -> assignees.add(auth.getId()));
}
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMap(inst -> setCollaborators(inst, owner, repo, issue, assignees))
.flatMap(inst -> client.createRepositoryIssue("Bearer " + inst.getToken(), owner, repo, issue))
.map(newIssue -> {
var i = new TaskIntegration(task, integration, newIssue.getNumber(), TaskIntegration.Type.ISSUE);
taskIntegrationRepository.save(i);
return newIssue.toIssue();
});
}
/**
* Update the GitHub issue for the given task.
*
* @param task the task
* @return the issue
*/
public Mono<Issue> patchIssue(Task task) {
var integrationProjectOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationProjectOptional.isEmpty()) {
return Mono.empty();
}
var integrationProject = integrationProjectOptional.get();
var integrationOptional = taskIntegrationRepository.findById(
new TaskIntegrationId(task.getId(), integrationProject.getId(), TaskIntegration.Type.ISSUE.ordinal()));
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var issue = new GitHubIssue(integration.getIssueId(), integration.link(),
task.getStatus().isFinal() ? "closed" : "open", task.getName(), task.getDescription(),
new ArrayList<>());
Set<Long> assignees = new HashSet<>();
if (task.getAssignee() != null) {
authorizationRepository.findByUser(task.getAssignee()).forEach(auth -> assignees.add(auth.getId()));
}
var owner = integrationProject.getRepositoryOwner();
var repo = integrationProject.getRepositoryName();
return Mono.just(integrationProject.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMap(inst -> setCollaborators(inst, owner, repo, issue, assignees))
.flatMap(inst -> client.patchRepositoryIssue("Bearer " + inst.getToken(), owner, repo,
issue.getNumber(), issue))
.map(gitIssue -> gitIssue.toIssue());
}
/**
* Delete the GitHub issue connection for the given task.
*
* @param task the task
*/
public void deleteIssue(Task task) {
var integrationOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationOptional.isEmpty()) {
return;
}
var integration = integrationOptional.get();
taskIntegrationRepository
.findById(
new TaskIntegrationId(task.getId(), integration.getId(), TaskIntegration.Type.ISSUE.ordinal()))
.ifPresent(taskIntegrationRepository::delete);
}
/**
* Get the GitHub pull requests for the given project.
*
* @param project the project
* @return the pull requests
*/
public Flux<PullRequest> getPullRequests(Project project) {
var integrationOptional = projectIntegrationRepository.findByProject(project);
if (integrationOptional.isEmpty()) {
return Flux.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMapMany(token -> client.getRepositoryPullRequests(token, owner, repo))
.map(GitHubPullRequest::toPullRequest);
}
/**
* Connect the given task to the given GitHub pull request.
*
* @param task the task
* @param id the pull request id
* @return the pull request
*/
public Mono<PullRequest> connectPullRequest(Task task, long id) {
var integrationOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
Set<Long> assignees = new HashSet<>();
if (task.getAssignee() != null) {
authorizationRepository.findByUser(task.getAssignee()).forEach(auth -> assignees.add(auth.getId()));
}
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMap(token -> client.getRepositoryPullRequest(token, owner, repo, id))
.map(pull -> {
var i = new TaskIntegration(task, integration, id, TaskIntegration.Type.PULL_REQUEST);
i.setMerged(pull.isMerged());
taskIntegrationRepository.save(i);
return pull;
})
.map(GitHubPullRequest::toPullRequest);
}
/**
* Patch the GitHub pull request for the given task.
*
* @param task the task
* @return the pull request
*/
public Mono<PullRequest> patchPullRequest(Task task) {
var integrationProjectOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationProjectOptional.isEmpty()) {
return Mono.empty();
}
var integrationProject = integrationProjectOptional.get();
var integrationOptional = taskIntegrationRepository.findById(new TaskIntegrationId(task.getId(),
integrationProject.getId(), TaskIntegration.Type.PULL_REQUEST.ordinal()));
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var pullRequest = new GitHubPullRequest(integration.getIssueId(), null, null, task.getName(),
task.getDescription(), new ArrayList<>(), null, false);
Set<Long> assignees = new HashSet<>();
if (task.getAssignee() != null) {
authorizationRepository.findByUser(task.getAssignee()).forEach(auth -> assignees.add(auth.getId()));
}
var owner = integrationProject.getRepositoryOwner();
var repo = integrationProject.getRepositoryName();
if (task.getStatus().isFinal() && !integration.isMerged()) {
return refreshToken(integrationProject.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMap(inst -> client.mergePullRequest("Bearer " + inst.getToken(), owner, repo,
integration.getIssueId()))
.map(merge -> {
integration.setMerged(merge.isMerged());
return integration;
})
.map(taskIntegrationRepository::save)
.then(Mono.empty());
}
return Mono.just(integrationProject.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMap(inst -> setCollaborators(inst, owner, repo, (GitHubIssue) pullRequest, assignees))
.flatMap(inst -> client.patchRepositoryPullRequest("Bearer " + inst.getToken(), owner, repo,
pullRequest.getNumber(), pullRequest))
.map(pull -> pull.toPullRequest());
}
/**
* Delete the GitHub pull request for the given task.
*
* @param task the task
*/
public void deletePullRequest(Task task) {
var integrationOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationOptional.isEmpty()) {
return;
}
var integration = integrationOptional.get();
taskIntegrationRepository
.findById(new TaskIntegrationId(task.getId(), integration.getId(),
TaskIntegration.Type.PULL_REQUEST.ordinal()))
.ifPresent(taskIntegrationRepository::delete);
}
/**
* Get git branches for the given project.
*
* @param project the project
* @return the branches
*/
public Flux<Branch> getBranches(Project project) {
var integrationOptional = projectIntegrationRepository.findByProject(project);
if (integrationOptional.isEmpty()) {
return Flux.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMapMany(token -> client.getRepositoryBranches(token, owner, repo))
.map(BranchName::toBranch);
}
/**
* Create new release for the given project.
*
* @param release the release
* @param branch the branch
* @return the release
*/
public Mono<Long> publishRelease(Release release, String branch) {
var integrationOptional = projectIntegrationRepository.findByProject(release.getProject());
if (integrationOptional.isEmpty()) {
return Mono.empty();
}
var integration = integrationOptional.get();
var owner = integration.getRepositoryOwner();
var repo = integration.getRepositoryName();
return Mono.just(integration.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.map(Installation::getToken)
.map(token -> "Bearer " + token)
.flatMap(token -> client.createRepositoryRelease(token, owner, repo, new GitHubRelease(release)))
.map(GitHubRelease::getId);
}
/**
* Create a new GitHub issue comment for given comment.
*
* @param comment the comment
* @return the comment
*/
public Mono<GitHubComment> createComment(Comment comment) {
var task = comment.getTask();
var integrationProjectOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationProjectOptional.isEmpty()) {
return Mono.empty();
}
var integrationProject = integrationProjectOptional.get();
var comments = new ArrayList<Mono<GitHubComment>>();
var integrationOptional = taskIntegrationRepository.findById(
new TaskIntegrationId(task.getId(), integrationProject.getId(), TaskIntegration.Type.ISSUE.ordinal()));
integrationOptional.ifPresent(integration -> comments.add(createCommentUtil(integrationProject, integration,
comment)));
integrationOptional = taskIntegrationRepository.findById(new TaskIntegrationId(task.getId(),
integrationProject.getId(), TaskIntegration.Type.PULL_REQUEST.ordinal()));
integrationOptional.ifPresent(integration -> comments.add(createCommentUtil(integrationProject, integration,
comment)));
return Flux.concat(comments).collectList().map(list -> list.get(0));
}
private Mono<GitHubComment> createCommentUtil(ProjectIntegration integrationProject,
TaskIntegration taskIntegration, Comment comment) {
return Mono.just(integrationProject.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMap(inst -> client.createIssueComment("Bearer " + inst.getToken(),
integrationProject.getRepositoryOwner(),
integrationProject.getRepositoryName(), taskIntegration.getIssueId(),
new GitHubComment(comment)))
.map(gitComment -> {
commentIntegrationRepository.save(new CommentIntegration(gitComment.getId(), comment));
return gitComment;
});
}
/**
* Patch the GitHub issue comment for given comment.
*
* @param comment the comment
* @return the comment
*/
public Mono<GitHubComment> patchComment(Comment comment) {
var task = comment.getTask();
var integrationProjectOptional = projectIntegrationRepository.findByProject(task.getStatus().getProject());
if (integrationProjectOptional.isEmpty()) {
return Mono.empty();
}
var integrationProject = integrationProjectOptional.get();
var integrations = commentIntegrationRepository.findByComment(comment);
if (integrations.isEmpty()) {
return Mono.empty();
}
return Mono.just(integrationProject.getInstallation())
.filter(inst -> !inst.isSuspended())
.flatMap(this::refreshToken)
.flatMapMany(inst -> Flux.concat(integrations.stream()
.map(integration -> client.patchIssueComment("Bearer " + inst.getToken(),
integrationProject.getRepositoryOwner(), integrationProject.getRepositoryName(),
integration.getId(), new GitHubComment(comment)))
.toList()))
.collectList().map(list -> list.get(0));
}
/**
* Delete the GitHub issue comment for given comment.
*
* @param comment the comment
*/
public void deleteComment(Comment comment) {
commentIntegrationRepository.findByComment(comment).forEach(commentIntegrationRepository::delete);
}
private Mono<Installation> refreshToken(Installation installation) {
return installation.shouldRefreshToken()
? client.createInstallationAccessToken("Bearer " + createJWT(), installation.getId()).map(token -> {
installation.refreshToken(token);
return installationRepository.save(installation);
})
: Mono.just(installation);
}
private Mono<Authorization> refreshToken(Authorization authorization) {
if (authorization.shouldRefreshToken()) {
var request = new OauthRefreshTokenRequest(authorization.getRefreshToken(), "refresh_token",
config.getClientId(), config.getClientSecret());
return client.refreshOauthAccessToken(request).map(token -> {
authorization.refreshToken(token);
return authorizationRepository.save(authorization);
});
}
return Mono.just(authorization);
}
private Flux<Installation> getUserInstallations(Authorization authorization) {
return client.getUserInstallations("Bearer " + authorization.getAccessToken())
.map(Installations::getInstallationList)
.flatMapMany(Flux::fromIterable)
.filter(installation -> installation.getAppId() == config.getAppId())
.map(installation -> {
var inst = installationRepository.findById(installation.getId()).orElseGet(Installation::new);
inst.update(installation);
return installationRepository.save(inst);
});
}
private Mono<Boolean> hasRepository(Installation installation, String repositoryFullName) {
return client.getInstallationRepositories("Bearer " + installation.getToken())
.map(Repositories::getRepositoryList)
.flatMapMany(Flux::fromIterable)
.map(GitHubRepository::getFullName)
.any(repositoryFullName::equals);
}
private Mono<Installation> setCollaborators(Installation installation, String owner, String name,
GitHubIssue issue, Set<Long> assignees) {
if (assignees.isEmpty()) {
return Mono.just(installation);
}
return client.getRepositoryCollaborators("Bearer " + installation.getToken(), owner, name)
.filter(user -> assignees.contains(user.getId()))
.map(user -> issue.getAssignees().add(user.getLogin()))
.then(Mono.just(installation));
}
private String createJWT() {
var now = Instant.now();
return Jwts.builder().setIssuedAt(Date.from(now)).setIssuer(Long.toString(config.getAppId()))
.signWith(config.getJwtKey()).setExpiration(Date.from(now.plusSeconds(60))).compact();
}
}