Skip to content

Commit

Permalink
GH-384 Support OpenAPI with Swagger UI (Resolve #384)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzikoysk committed Feb 26, 2021
1 parent 9acce2f commit 9fa0c39
Show file tree
Hide file tree
Showing 26 changed files with 336 additions and 72 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ Requirements:
* [x] Supports requests to primary repository without its name in url
* [x] Dashboard
* [x] Customizable front page
* [x] CLI
* [x] Command line interface
* [x] Repository browser
* [x] Admin panel
* [x] Snapshots
* [x] Statistics
* [x] REST API
* [x] OpenAPI with Swagger UI
* [x] 90%+ test coverage
* [x] Documentation

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "3.9"
services:
reposilite:
image: reposilite:2.9.17
image: reposilite:2.9.18
build:
context: .
dockerfile: Dockerfile
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/docker.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
</ul>
<h2><a class="anchor" aria-hidden="true" id="installation"></a><a href="#installation" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Installation</h2>
<p>First of all, you have to pull the image from <a href="https://hub.docker.com/r/dzikoysk/reposilite">DockerHub</a>:</p>
<pre><code class="hljs css language-shell-session"><span class="hljs-comment">// released builds, e.g. 2.9.17</span>
<pre><code class="hljs css language-shell-session"><span class="hljs-comment">// released builds, e.g. 2.9.18</span>
$ docker pull dzikoysk/reposilite:<span class="hljs-number">2.9</span><span class="hljs-number">.13</span>

<span class="hljs-comment">// nightly builds</span>
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/docker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
</ul>
<h2><a class="anchor" aria-hidden="true" id="installation"></a><a href="#installation" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Installation</h2>
<p>First of all, you have to pull the image from <a href="https://hub.docker.com/r/dzikoysk/reposilite">DockerHub</a>:</p>
<pre><code class="hljs css language-shell-session"><span class="hljs-comment">// released builds, e.g. 2.9.17</span>
<pre><code class="hljs css language-shell-session"><span class="hljs-comment">// released builds, e.g. 2.9.18</span>
$ docker pull dzikoysk/reposilite:<span class="hljs-number">2.9</span><span class="hljs-number">.13</span>

<span class="hljs-comment">// nightly builds</span>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<groupId>org.panda-lang</groupId>
<artifactId>reposilite-parent</artifactId>
<packaging>pom</packaging>
<version>2.9.17</version>
<version>2.9.18</version>

<modules>
<module>reposilite-backend</module>
Expand Down
14 changes: 13 additions & 1 deletion reposilite-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<parent>
<artifactId>reposilite-parent</artifactId>
<groupId>org.panda-lang</groupId>
<version>2.9.17</version>
<version>2.9.18</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand All @@ -41,6 +41,18 @@
</properties>

<dependencies>
<!-- OpenAPI -->
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-openapi</artifactId>
<version>3.13.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>3.25.2</version>
</dependency>

<!-- Web -->
<dependency>
<groupId>io.javalin</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.panda_lang.reposilite.console.Console;
import org.panda_lang.reposilite.console.ConsoleConfiguration;
import org.panda_lang.reposilite.error.FailureService;
import org.panda_lang.reposilite.frontend.FrontendProvider;
import org.panda_lang.reposilite.resource.FrontendProvider;
import org.panda_lang.reposilite.metadata.MetadataConfiguration;
import org.panda_lang.reposilite.metadata.MetadataService;
import org.panda_lang.reposilite.repository.DeployService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

public final class ReposiliteConstants {

public static final String VERSION = "2.9.17";
public static final String NAME = "Reposilite";

public static final String VERSION = "2.9.18";

public static final String REMOTE_VERSION = "https://repo.panda-lang.org/org/panda-lang/reposilite/latest";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import io.javalin.Javalin;
import io.javalin.core.JavalinConfig;
import io.javalin.core.JavalinServer;
import io.javalin.plugin.openapi.OpenApiOptions;
import io.javalin.plugin.openapi.OpenApiPlugin;
import io.javalin.plugin.openapi.ui.SwaggerOptions;
import io.swagger.v3.oas.models.info.Info;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
Expand All @@ -27,11 +31,12 @@
import org.panda_lang.reposilite.config.Configuration;
import org.panda_lang.reposilite.console.CliController;
import org.panda_lang.reposilite.console.RemoteExecutionEndpoint;
import org.panda_lang.reposilite.error.FailureService;
import org.panda_lang.reposilite.frontend.FrontendController;
import org.panda_lang.reposilite.error.FailureHandler;
import org.panda_lang.reposilite.repository.DeployEndpoint;
import org.panda_lang.reposilite.repository.LookupApiEndpoint;
import org.panda_lang.reposilite.repository.LookupController;
import org.panda_lang.reposilite.resource.FrontendHandler;
import org.panda_lang.reposilite.resource.WebJarsHandler;
import org.panda_lang.reposilite.utils.FilesUtils;
import org.panda_lang.utilities.commons.function.Option;

Expand All @@ -47,7 +52,6 @@ public final class ReposiliteHttpServer {
}

void start(Configuration configuration, Runnable onStart) {
FailureService failureService = reposilite.getFailureService();
DeployEndpoint deployEndpoint = new DeployEndpoint(reposilite.getContextFactory(), reposilite.getDeployService());

LookupController lookupController = new LookupController(
Expand All @@ -72,7 +76,8 @@ void start(Configuration configuration, Runnable onStart) {

this.javalin = create(configuration)
.before(ctx -> reposilite.getStatsService().record(ctx.req.getRequestURI()))
.get("/js/app.js", new FrontendController(reposilite))
.get("/webjars/*", new WebJarsHandler())
.get("/js/app.js", new FrontendHandler(reposilite))
.get("/api/auth", new AuthEndpoint(reposilite.getAuthService()))
.post("/api/execute", new RemoteExecutionEndpoint(reposilite.getAuthenticator(), reposilite.getContextFactory(), reposilite.getConsole()))
.ws("/api/cli", cliController)
Expand All @@ -83,14 +88,18 @@ void start(Configuration configuration, Runnable onStart) {
.put("/*", deployEndpoint)
.post("/*", deployEndpoint)
.after("/*", new PostAuthHandler())
.exception(Exception.class, (exception, ctx) -> failureService.throwException(ctx.req.getRequestURI(), exception));
.exception(Exception.class, new FailureHandler(reposilite.getFailureService()));

if (!servlet) {
javalin.start(configuration.hostname, configuration.port);
onStart.run();
}
}

void stop() {
getJavalin().peek(Javalin::stop);
}

private Javalin create(Configuration configuration) {
return servlet
? Javalin.createStandalone(config -> configure(configuration, config))
Expand Down Expand Up @@ -118,10 +127,25 @@ private void configure(Configuration configuration, JavalinConfig config) {
}
}

config.enableCorsForAllOrigins();
config.enforceSsl = configuration.enforceSsl;
config.enableCorsForAllOrigins();
config.showJavalinBanner = false;

if (configuration.swagger) {
Info applicationInfo = new Info()
.description(ReposiliteConstants.NAME)
.version(ReposiliteConstants.VERSION);

SwaggerOptions swaggerOptions = new SwaggerOptions("/swagger")
.title("Reposilite API documentation");

OpenApiOptions options = new OpenApiOptions(applicationInfo)
.path("/swagger-docs")
.swagger(swaggerOptions);

config.registerPlugin(new OpenApiPlugin(options));
}

if (configuration.debugEnabled) {
config.requestCacheSize = FilesUtils.displaySizeToBytesCount(System.getProperty("reposilite.requestCacheSize", "8MB"));
Reposilite.getLogger().debug("requestCacheSize set to " + config.requestCacheSize + " bytes");
Expand All @@ -132,12 +156,6 @@ private void configure(Configuration configuration, JavalinConfig config) {
config.server(() -> server);
}

void stop() {
if (javalin != null) {
javalin.stop();
}
}

public boolean isAlive() {
return getJavalin()
.map(Javalin::server)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,42 @@
package org.panda_lang.reposilite.auth;

import io.javalin.http.Context;
import org.panda_lang.reposilite.RepositoryController;
import io.javalin.http.Handler;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiParam;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import org.panda_lang.reposilite.error.ErrorDto;
import org.panda_lang.reposilite.error.ResponseUtils;

public final class AuthEndpoint implements RepositoryController {
public final class AuthEndpoint implements Handler {

private final AuthService authService;

public AuthEndpoint(AuthService authService) {
this.authService = authService;
}

@OpenApi(
operationId = "auth",
summary = "Get token details",
description = "Returns details about the requested token",
tags = { "Auth" },
headers = {
@OpenApiParam(name = "Authorization", description = "Alias and token provided as basic auth credentials", required = true)
},
responses = {
@OpenApiResponse(status = "200", description = "Details about the token for succeeded authentication", content = {
@OpenApiContent(from = AuthDto.class)
}),
@OpenApiResponse(status = "401", description = "Error message related to the unauthorized access in case of any failure", content = {
@OpenApiContent(from = ErrorDto.class)
})
}
)
@Override
public Context handleContext(Context ctx) {
return ResponseUtils.response(ctx, authService.authByHeader(ctx.headerMap()));
public void handle(Context ctx) {
ResponseUtils.response(ctx, authService.authByHeader(ctx.headerMap()));
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public final class Configuration implements Serializable {
@Description("# Cloudflare: CF-Connecting-IP")
@Description("# Popular: X-Real-IP")
public String forwardedIp = "X-Forwarded-For";
@Description("# Enable Swagger (/swagger-docs) and Swagger UI (/swagger)")
public Boolean swagger = false;
@Description("# Debug")
public Boolean debugEnabled = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,26 @@
package org.panda_lang.reposilite.console;

import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.plugin.openapi.annotations.HttpMethod;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiParam;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import org.apache.http.HttpStatus;
import org.panda_lang.reposilite.Reposilite;
import org.panda_lang.reposilite.ReposiliteContext;
import org.panda_lang.reposilite.ReposiliteContextFactory;
import org.panda_lang.reposilite.RepositoryController;
import org.panda_lang.reposilite.auth.Authenticator;
import org.panda_lang.reposilite.auth.Session;
import org.panda_lang.reposilite.error.ErrorDto;
import org.panda_lang.reposilite.error.ResponseUtils;
import org.panda_lang.utilities.commons.StringUtils;
import org.panda_lang.utilities.commons.function.Result;

import java.util.List;

public final class RemoteExecutionEndpoint implements RepositoryController {
public final class RemoteExecutionEndpoint implements Handler {

private static final int MAX_COMMAND_LENGTH = 1024;

Expand All @@ -44,37 +50,64 @@ public RemoteExecutionEndpoint(Authenticator authenticator, ReposiliteContextFac
this.console = console;
}

@OpenApi(
operationId = "cli",
method = HttpMethod.POST,
summary = "Remote command execution",
description = "Execute command using POST request. The commands are the same as in the console and can be listed using the 'help' command.",
tags = { "Cli" },
headers = {
@OpenApiParam(name = "Authorization", description = "Alias and token provided as basic auth credentials", required = true)
},
responses = {
@OpenApiResponse(status = "200", description = "Status of the executed command", content = {
@OpenApiContent(from = RemoteExecutionDto.class)
}),
@OpenApiResponse(
status = "400",
description = "Error message related to the invalid command format (0 < command length < " + MAX_COMMAND_LENGTH + ")",
content = @OpenApiContent(from = ErrorDto.class)
),
@OpenApiResponse(status = "401", description = "Error message related to the unauthorized access", content = {
@OpenApiContent(from = ErrorDto.class)
})
}
)
@Override
public Context handleContext(Context ctx) {
public void handle(Context ctx) {
ReposiliteContext context = contextFactory.create(ctx);
Reposilite.getLogger().info("REMOTE EXECUTION " + context.uri() + " from " + context.address());

Result<Session, String> authResult = authenticator.authByHeader(context.headers());

if (authResult.isErr()) {
return ResponseUtils.errorResponse(ctx, HttpStatus.SC_UNAUTHORIZED, authResult.getError());
ResponseUtils.errorResponse(ctx, HttpStatus.SC_UNAUTHORIZED, authResult.getError());
return;
}

Session session = authResult.get();

if (!session.isManager()) {
return ResponseUtils.errorResponse(ctx, HttpStatus.SC_UNAUTHORIZED, "Authenticated user is not a manger");
ResponseUtils.errorResponse(ctx, HttpStatus.SC_UNAUTHORIZED, "Authenticated user is not a manger");
return;
}

String command = ctx.body();

if (StringUtils.isEmpty(command)) {
return ResponseUtils.errorResponse(ctx, HttpStatus.SC_BAD_REQUEST, "Missing command");
ResponseUtils.errorResponse(ctx, HttpStatus.SC_BAD_REQUEST, "Missing command");
return;
}

if (command.length() > MAX_COMMAND_LENGTH) {
return ResponseUtils.errorResponse(ctx, HttpStatus.SC_BAD_REQUEST, "The given command exceeds allowed length (" + command.length() + " > " + MAX_COMMAND_LENGTH + ")");
ResponseUtils.errorResponse(ctx, HttpStatus.SC_BAD_REQUEST, "The given command exceeds allowed length (" + command.length() + " > " + MAX_COMMAND_LENGTH + ")");
return;
}

Reposilite.getLogger().info(session.getAlias() + " (" + context.address() + ") requested command: " + command);
Result<List<String>, List<String>> result = console.execute(command);

return ctx.json(new RemoteExecutionDto(result.isOk(), result.isOk() ? result.get() : result.getError()));
ctx.json(new RemoteExecutionDto(result.isOk(), result.isOk() ? result.get() : result.getError()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@
* limitations under the License.
*/

package org.panda_lang.reposilite;
package org.panda_lang.reposilite.error;

import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.jetbrains.annotations.NotNull;
import io.javalin.http.ExceptionHandler;

public interface RepositoryController extends Handler {
public final class FailureHandler implements ExceptionHandler<Exception> {

@Override
default void handle(@NotNull Context ctx) throws Exception {
handleContext(ctx);
private final FailureService failureService;

public FailureHandler(FailureService failureService) {
this.failureService = failureService;
}

Context handleContext(Context ctx) throws Exception;
@Override
public void handle(Exception exception, Context context) {
failureService.throwException(context.req.getRequestURI(), exception);
}

}
Loading

0 comments on commit 9fa0c39

Please sign in to comment.