From 7243f3d5a5507d954a409338a3e031475ebd2b09 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 17 Feb 2020 13:42:48 +0100 Subject: [PATCH 01/33] bootstrap openapi documentation --- scm-webapp/pom.xml | 59 ++++++++++++++++++- scm-webapp/src/main/doc/openapi.md | 3 + .../v2/resources/AuthenticationResource.java | 20 +++++++ .../api/v2/resources/RepositoryResource.java | 47 ++++++++++++--- .../v2/resources/RepositoryRootResource.java | 10 +++- 5 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 scm-webapp/src/main/doc/openapi.md diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 924e176024..4520700dbc 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -426,6 +426,13 @@ provided + + io.swagger.core.v3 + swagger-annotations + 2.1.1 + provided + + @@ -471,6 +478,50 @@ + + io.openapitools.swagger + swagger-maven-plugin + 2.1.2 + + + sonia.scm.api.v2.resources + + ${basedir}/target/openapi/META-INF/scm + openapi + JSON,YAML + true + + + + http://localhost:8081/scm/api + local endpoint url + + + + SCM-Manager REST-API + ${project.version} + + scmmanager@googlegroups.com + SCM-Manager + https://scm-manager.org + + + http://www.opensource.org/licenses/bsd-license.php + BSD + + + src/main/doc/openapi.md + + + + + + generate + + + + + sonia.scm.maven smp-maven-plugin @@ -511,9 +562,15 @@ org.apache.maven.plugins maven-war-plugin - 2.2 + 3.1.0 true + + + target/openapi + WEB-INF/classes + + diff --git a/scm-webapp/src/main/doc/openapi.md b/scm-webapp/src/main/doc/openapi.md new file mode 100644 index 0000000000..c014899c30 --- /dev/null +++ b/scm-webapp/src/main/doc/openapi.md @@ -0,0 +1,3 @@ +# openapi docs from code + +describe hateoas diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index deab53a708..3bb37b99d4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -3,6 +3,10 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.subject.Subject; @@ -19,6 +23,22 @@ import javax.ws.rs.core.Response; import java.net.URI; import java.util.Optional; +@SecuritySchemes({ + @SecurityScheme( + name = "Basic Authentication", + description = "HTTP Basic authentication with username and password", + scheme = "Basic", + type = SecuritySchemeType.HTTP + ), + @SecurityScheme( + name = "Bearer Token Authentication", + in = SecuritySchemeIn.HEADER, + paramName = "Authorization", + scheme = "Bearer", + bearerFormat = "JWT", + type = SecuritySchemeType.APIKEY + ) +}) @Path(AuthenticationResource.PATH) @AllowAnonymousAccess public class AuthenticationResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 2294dc600e..ebae9e53e9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -3,6 +3,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -87,14 +91,39 @@ public class RepositoryResource { @GET @Path("") @Produces(VndMediaType.REPOSITORY) - @TypeHint(RepositoryDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Returns a single repository", description = "Returns the repository for the given namespace and name.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.REPOSITORY, + schema = @Schema(implementation = RepositoryDto.class) + ) + ) + @ApiResponse( + responseCode = "401", + description = "not authenticated / invalid credentials" + ) + @ApiResponse( + responseCode = "403", + description = "not authorized, the current user has no privileges to read the repository" + ) + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified name available in the namespace", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name){ return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } @@ -168,7 +197,7 @@ public class RepositoryResource { } @Path("branches/") - public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) { + public BranchRootResource branches() { return branchRootResource.get(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java index a7a6365c37..7b8c1120e1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -1,12 +1,20 @@ package sonia.scm.api.v2.resources; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.tags.Tag; + import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.Path; /** - * RESTful Web Service Resource to manage repositories. + * RESTful Web Service Resource to manage repositories. */ +@OpenAPIDefinition( + tags = { + @Tag(name = "Repository", description = "Repository related endpoints") + } +) @Path(RepositoryRootResource.REPOSITORIES_PATH_V2) public class RepositoryRootResource { static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; From b7e95f3cc4fe1d95d6ff4e14e98e3d1201e5171a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 17 Feb 2020 14:09:26 +0100 Subject: [PATCH 02/33] create openapi docs for scm-git-plugin --- pom.xml | 12 ++++ .../sonia/scm/api/v2/resources/ErrorDto.java | 0 scm-plugins/pom.xml | 38 +++++++++++ .../api/v2/resources/GitConfigResource.java | 53 ++++++++++++--- .../GitRepositoryConfigResource.java | 65 +++++++++++++++---- scm-webapp/pom.xml | 1 - 6 files changed, 144 insertions(+), 25 deletions(-) rename {scm-webapp => scm-core}/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java (100%) diff --git a/pom.xml b/pom.xml index 48e94c11de..535b9d9af8 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,12 @@ ${jaxrs.version} + + io.swagger.core.v3 + swagger-annotations + 2.1.1 + + com.fasterxml.jackson.core jackson-core @@ -465,6 +471,12 @@ 2.8.2 + + io.openapitools.swagger + swagger-maven-plugin + 2.1.2 + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index e6b2929bd2..b74effd8ce 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -61,6 +61,13 @@ provided + + + io.swagger.core.v3 + swagger-annotations + provided + + @@ -136,6 +143,37 @@ + + io.openapitools.swagger + swagger-maven-plugin + + + sonia.scm.api.v2.resources + + ${basedir}/target/classes/META-INF/scm + openapi + JSON,YAML + true + + + SCM-Manager Plugin REST-API + ${project.version} + + http://www.opensource.org/licenses/bsd-license.php + BSD + + + + + + + + generate + + + + + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index 7cda4bc9d3..d3332d5a37 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -3,10 +3,17 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.web.GitVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; @@ -14,13 +21,15 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the git plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Git", description = "Configuration for the git repository type") +}) @Path(GitConfigResource.GIT_CONFIG_PATH_V2) public class GitConfigResource { @@ -45,13 +54,24 @@ public class GitConfigResource { @GET @Path("") @Produces(GitVndMediaType.GIT_CONFIG) - @TypeHint(GitConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:git\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Git configuration", description = "Returns the global git configuration.", tags = "Git") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = GitVndMediaType.GIT_CONFIG, + schema = @Schema(implementation = GitConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:git\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { GitConfig config = repositoryHandler.getConfig(); @@ -80,7 +100,20 @@ public class GitConfigResource { @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:git\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(GitConfigDto configDto) { GitConfig config = dtoToConfigMapper.map(configDto); @@ -94,7 +127,7 @@ public class GitConfigResource { } @Path("{namespace}/{name}") - public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { + public GitRepositoryConfigResource getRepositoryConfig() { return gitRepositoryConfigResource.get(); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java index 175caf8840..88a9c5d669 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java @@ -2,6 +2,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitRepositoryConfig; @@ -11,6 +15,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.store.ConfigurationStore; import sonia.scm.web.GitVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.Consumes; @@ -42,13 +47,31 @@ public class GitRepositoryConfigResource { @GET @Path("/") @Produces(GitVndMediaType.GIT_REPOSITORY_CONFIG) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository config"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Git repository configuration", description = "Returns the repository related git configuration.", tags = "Git") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = GitVndMediaType.GIT_REPOSITORY_CONFIG, + schema = @Schema(implementation = GitRepositoryConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository config") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified namespace and name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { Repository repository = getRepository(namespace, name); RepositoryPermissions.read(repository).check(); @@ -61,13 +84,27 @@ public class GitRepositoryConfigResource { @PUT @Path("/") @Consumes(GitVndMediaType.GIT_REPOSITORY_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to change this repositories config"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Modifies git repository configuration", description = "Modifies the repository related git configuration.", tags = "Git") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the privilege to change this repositories config") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified namespace and name available/name available", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) { Repository repository = getRepository(namespace, name); RepositoryPermissions.custom("git", repository).check(); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 4520700dbc..d8068f1bb8 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -481,7 +481,6 @@ io.openapitools.swagger swagger-maven-plugin - 2.1.2 sonia.scm.api.v2.resources From 1ff0c46b0e82193da35c5598365fab815dee81d8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 10:13:01 +0100 Subject: [PATCH 03/33] add security requirements --- .../main/java/sonia/scm/api/v2/resources/IndexResource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index 1eec99ea96..611fba6f2c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -1,6 +1,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; @@ -9,6 +11,10 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +@OpenAPIDefinition(security = { + @SecurityRequirement(name = "Basic Authentication"), + @SecurityRequirement(name = "Bearer Token Authentication") +}) @Path(IndexResource.INDEX_PATH_V2) @AllowAnonymousAccess public class IndexResource { From 61d7d5e6dd97d399fa23e79bdca2a789f64cd3e2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 13:10:37 +0100 Subject: [PATCH 04/33] Add footer extension points for links and avatar --- CHANGELOG.md | 4 ++ .../ui-components/src/avatar/AvatarImage.tsx | 15 +++--- scm-ui/ui-components/src/layout/Footer.tsx | 51 +++++++++++++++++-- scm-ui/ui-styles/src/scm.scss | 11 ++-- scm-ui/ui-webapp/src/containers/App.tsx | 10 ++-- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dcc480d1..4bb851e876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added +- Added footer extension points for links and avatar + ## 2.0.0-rc4 - 2020-02-14 ### Added - Support for Java versions > 8 diff --git a/scm-ui/ui-components/src/avatar/AvatarImage.tsx b/scm-ui/ui-components/src/avatar/AvatarImage.tsx index 48808b582f..a30a999d18 100644 --- a/scm-ui/ui-components/src/avatar/AvatarImage.tsx +++ b/scm-ui/ui-components/src/avatar/AvatarImage.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {FC} from "react"; import { binder } from "@scm-manager/ui-extensions"; import { Image } from ".."; import { Person } from "./Avatar"; @@ -6,21 +6,20 @@ import { EXTENSION_POINT } from "./Avatar"; type Props = { person: Person; + representation?: "rounded" | "rounded-border"; }; -class AvatarImage extends React.Component { - render() { - const { person } = this.props; - +const AvatarImage:FC = ({person, representation = "rounded-border"}) => { const avatarFactory = binder.getExtension(EXTENSION_POINT); if (avatarFactory) { const avatar = avatarFactory(person); - return {person.name}; + const className = representation === "rounded" ? "is-rounded" : "has-rounded-border"; + + return {person.name}; } return null; - } -} +}; export default AvatarImage; diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 6c56061a71..2761268a1c 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -1,23 +1,66 @@ import React from "react"; -import { Me } from "@scm-manager/ui-types"; +import { Me, Links } from "@scm-manager/ui-types"; import { Link } from "react-router-dom"; +import { binder } from "@scm-manager/ui-extensions"; +import { AvatarWrapper, AvatarImage } from "../avatar"; type Props = { me?: Me; + version: string; + links: Links; }; +const RestDocLink = () => { + return Rest Documentation; +}; + +binder.bind("footer.links", RestDocLink); + class Footer extends React.Component { render() { - const { me } = this.props; + const { me, version, links } = this.props; if (!me) { return ""; } + + const extensions = binder.getExtensions("footer.links", { me, links }); + return ( ); diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index cca6a484ed..dffa4cb00e 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -67,8 +67,10 @@ hr.header-with-actions { } } -.footer { - height: 50px; +footer.footer { + //height: 100px; + background-color: whitesmoke; + padding: 2rem 1.5rem 1rem; } // 6. Import the rest of Bulma @@ -691,11 +693,6 @@ form .field:not(.is-grouped) { } } -// footer -.footer { - background-color: whitesmoke; -} - // aside .aside-background { bottom: 0; diff --git a/scm-ui/ui-webapp/src/containers/App.tsx b/scm-ui/ui-webapp/src/containers/App.tsx index 281e47e920..6170621c65 100644 --- a/scm-ui/ui-webapp/src/containers/App.tsx +++ b/scm-ui/ui-webapp/src/containers/App.tsx @@ -8,6 +8,7 @@ import { fetchMe, getFetchMeFailure, getMe, isAuthenticated, isFetchMePending } import { ErrorPage, Footer, Header, Loading, PrimaryNavigation } from "@scm-manager/ui-components"; import { Links, Me } from "@scm-manager/ui-types"; import { + getAppVersion, getFetchIndexResourcesFailure, getLinks, getMeLink, @@ -21,6 +22,7 @@ type Props = WithTranslation & { loading: boolean; links: Links; meLink: string; + version: string; // dispatcher functions fetchMe: (link: string) => void; @@ -34,7 +36,7 @@ class App extends Component { } render() { - const { me, loading, error, authenticated, links, t } = this.props; + const { me, loading, error, authenticated, links, version, t } = this.props; let content; const navigation = authenticated ? : ""; @@ -50,7 +52,7 @@ class App extends Component {
{navigation}
{content} - {authenticated &&
} + {authenticated &&
}
); } @@ -69,13 +71,15 @@ const mapStateToProps = (state: any) => { const error = getFetchMeFailure(state) || getFetchIndexResourcesFailure(state); const links = getLinks(state); const meLink = getMeLink(state); + const version = getAppVersion(state); return { authenticated, me, loading, error, links, - meLink + meLink, + version }; }; From 6431f19b3089e21335353a725c6c7d313a46d6af Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 18 Feb 2020 14:03:33 +0100 Subject: [PATCH 05/33] rm link mock --- scm-ui/ui-components/src/layout/Footer.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 2761268a1c..63e294bb01 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -10,12 +10,6 @@ type Props = { links: Links; }; -const RestDocLink = () => { - return Rest Documentation; -}; - -binder.bind("footer.links", RestDocLink); - class Footer extends React.Component { render() { const { me, version, links } = this.props; @@ -56,7 +50,7 @@ class Footer extends React.Component { Cloudogu GmbH {" "} - | Learn more at{" "} + | Learn more about{" "} SCM-Manager From 5364e8682ddd54fbaa8f51c673deca7464a0994a Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 18 Feb 2020 17:19:35 +0100 Subject: [PATCH 06/33] Create openapi docs for hg/svn plugin --- .../api/v2/resources/GitConfigResource.java | 9 --- .../HgConfigAutoConfigurationResource.java | 50 ++++++++++------ .../HgConfigInstallationsResource.java | 58 ++++++++++++------ .../v2/resources/HgConfigPackageResource.java | 60 +++++++++++++------ .../api/v2/resources/HgConfigResource.java | 60 +++++++++++++------ .../api/v2/resources/SvnConfigResource.java | 59 ++++++++++++------ 6 files changed, 199 insertions(+), 97 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index d3332d5a37..098396098f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -1,8 +1,5 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -94,12 +91,6 @@ public class GitConfigResource { @PUT @Path("") @Consumes(GitVndMediaType.GIT_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git") @ApiResponse( responseCode = "204", diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java index b265f2929d..3507c41da3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java @@ -1,13 +1,15 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.ws.rs.Consumes; import javax.ws.rs.PUT; @@ -31,13 +33,20 @@ public class HgConfigAutoConfigurationResource { */ @PUT @Path("") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Sets hg configuration and installs hg binary", description = "Sets the default mercurial config and installs the mercurial binary.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response autoConfiguration() { return autoConfiguration(null); } @@ -50,13 +59,20 @@ public class HgConfigAutoConfigurationResource { @PUT @Path("") @Consumes(HgVndMediaType.CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modifies hg configuration and installs hg binary", description = "Modifies the mercurial config and installs the mercurial binary.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response autoConfiguration(HgConfigDto configDto) { HgConfig config; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java index 8842d07569..795d0c87a6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java @@ -1,13 +1,15 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import de.otto.edison.hal.HalRepresentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.repository.HgConfig; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.GET; @@ -31,13 +33,24 @@ public class HgConfigInstallationsResource { @GET @Path(PATH_HG) @Produces(HgVndMediaType.INSTALLATIONS) - @TypeHint(HalRepresentation.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Hg installations", description = "Returns the mercurial installations.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.INSTALLATIONS, + schema = @Schema(implementation = HgConfigInstallationsDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getHgInstallations() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -52,13 +65,24 @@ public class HgConfigInstallationsResource { @GET @Path(PATH_PYTHON) @Produces(HgVndMediaType.INSTALLATIONS) - @TypeHint(HalRepresentation.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Python installations", description = "Returns the python installations.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.INSTALLATIONS, + schema = @Schema(implementation = HgConfigInstallationsDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getPythonInstallations() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java index 88a7de7ea0..2e185f152d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java @@ -1,9 +1,10 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; import de.otto.edison.hal.HalRepresentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.SCMContext; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.installer.HgInstallerFactory; @@ -13,6 +14,7 @@ import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.GET; @@ -44,13 +46,20 @@ public class HgConfigPackageResource { @GET @Path("") @Produces(HgVndMediaType.PACKAGES) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(HalRepresentation.class) + @Operation(summary = "Hg configuration packages", description = "Returns all mercurial packages.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public HalRepresentation getPackages() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -65,14 +74,27 @@ public class HgConfigPackageResource { */ @PUT @Path("{pkgId}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 404, condition = "no package found for id"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modifies hg configuration package", description = "Installs a mercurial package.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "404", + description = "no package found for id", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response installPackage(@PathParam("pkgId") String pkgId) { Response response; @@ -82,7 +104,7 @@ public class HgConfigPackageResource { if (pkg != null) { if (HgInstallerFactory.createInstaller() - .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { + .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { response = Response.noContent().build(); } else { response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java index e6a8f01238..be534f4345 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java @@ -1,12 +1,16 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.web.HgVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; @@ -20,11 +24,13 @@ import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the hg plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Mercurial", description = "Configuration for the mercurial repository type") +}) @Path(HgConfigResource.HG_CONFIG_PATH_V2) public class HgConfigResource { static final String HG_CONFIG_PATH_V2 = "v2/config/hg"; - private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; private final HgConfigToHgConfigDtoMapper configToDtoMapper; private final HgRepositoryHandler repositoryHandler; @@ -51,13 +57,24 @@ public class HgConfigResource { @GET @Path("") @Produces(HgVndMediaType.CONFIG) - @TypeHint(HgConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Hg configuration", description = "Returns the global mercurial configuration.", tags = "Mercurial") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = HgVndMediaType.CONFIG, + schema = @Schema(implementation = HgConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { ConfigurationPermissions.read(HgConfig.PERMISSION).check(); @@ -80,13 +97,20 @@ public class HgConfigResource { @PUT @Path("") @Consumes(HgVndMediaType.CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify hg configuration", description = "Modifies the global mercurial configuration.", tags = "Mercurial") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(HgConfigDto configDto) { HgConfig config = dtoToConfigMapper.map(configDto); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java index b12785dca9..9ff13ffb46 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java @@ -1,12 +1,16 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.repository.SvnConfig; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.web.SvnVndMediaType; +import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.Consumes; @@ -19,6 +23,9 @@ import javax.ws.rs.core.Response; /** * RESTful Web Service Resource to manage the configuration of the svn plugin. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Subversion", description = "Configuration for the subversion repository type") +}) @Path(SvnConfigResource.SVN_CONFIG_PATH_V2) public class SvnConfigResource { @@ -41,13 +48,24 @@ public class SvnConfigResource { @GET @Path("") @Produces(SvnVndMediaType.SVN_CONFIG) - @TypeHint(SvnConfigDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:svn\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Svn configuration", description = "Returns the global subversion configuration.", tags = "Subversion") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = SvnVndMediaType.SVN_CONFIG, + schema = @Schema(implementation = SvnConfigDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:svn\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get() { SvnConfig config = repositoryHandler.getConfig(); @@ -70,13 +88,20 @@ public class SvnConfigResource { @PUT @Path("") @Consumes(SvnVndMediaType.SVN_CONFIG) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:svn\" privilege"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @Operation(summary = "Modify svn configuration", description = "Modifies the global subversion configuration.", tags = "Subversion") + @ApiResponse( + responseCode = "204", + description = "update success" + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:svn\" privilege") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response update(SvnConfigDto configDto) { SvnConfig config = dtoToConfigMapper.map(configDto); From dc83b50095d844e89af78f75eef7494fbb747a0c Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 18 Feb 2020 17:20:23 +0100 Subject: [PATCH 07/33] Create openapi docs for me endpoint and changesets --- .../v2/resources/ChangesetRootResource.java | 119 +++++++++++------- .../v2/resources/FileHistoryRootResource.java | 40 ++++-- .../scm/api/v2/resources/MeResource.java | 49 ++++++-- 3 files changed, 142 insertions(+), 66 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java index 0766816d4d..e7338b42f2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -42,17 +43,82 @@ public class ChangesetRootResource { this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; } + @GET + @Path("{id}") + @Produces(VndMediaType.CHANGESET) + @Operation(summary = "Collection of changesets", description = "Returns a collection of changesets.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET, + schema = @Schema(implementation = ChangesetDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changeset with the specified id is available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setStartChangeset(id) + .setEndChangeset(id) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { + Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); + if (!changeset.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + } + @GET @Path("") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Specific changeset", description = "Returns a specific changeset.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { @@ -75,35 +141,4 @@ public class ChangesetRootResource { } } } - - @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.CHANGESET) - @TypeHint(ChangesetDto.class) - @Path("{id}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { - try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - Repository repository = repositoryService.getRepository(); - RepositoryPermissions.read(repository).check(); - ChangesetPagingResult changesets = repositoryService.getLogCommand() - .setStartChangeset(id) - .setEndChangeset(id) - .getChangesets(); - if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { - Optional changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); - if (!changeset.isPresent()) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java index 5e087dc7ca..3a66f9c7a1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -1,8 +1,9 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; @@ -54,15 +55,32 @@ public class FileHistoryRootResource { */ @GET @Path("{revision}/{path: .*}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) + @Operation(summary = "Changesets to given file", description = "Get all changesets related to the given file starting with the given revision.", tags = "Repository") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.CHANGESET_COLLECTION, + schema = @Schema(implementation = CollectionDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the changeset") + @ApiResponse( + responseCode = "404", + description = "not found, no changesets available in the repository", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index 2c2e208893..eae947c5d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -3,6 +3,12 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -23,11 +29,13 @@ import javax.ws.rs.core.UriInfo; /** * RESTful Web Service Resource to get currently logged in users. */ +@OpenAPIDefinition(tags = { + @Tag(name = "Me", description = "Me related endpoints") +}) @Path(MeResource.ME_PATH_V2) public class MeResource { static final String ME_PATH_V2 = "v2/me/"; - private final MeDtoFactory meDtoFactory; private final UserManager userManager; private final PasswordService passwordService; @@ -45,12 +53,23 @@ public class MeResource { @GET @Path("") @Produces(VndMediaType.ME) - @TypeHint(MeDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 500, condition = "internal server error") - }) + @Operation(summary = "Current user", description = "Returns the currently logged in user or a 401 if user is not logged in.", tags = "Me") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.ME, + schema = @Schema(implementation = MeDto.class) + ) + ) + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response get(@Context Request request, @Context UriInfo uriInfo) { return Response.ok(meDtoFactory.create()).build(); } @@ -60,13 +79,17 @@ public class MeResource { */ @PUT @Path("password") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PASSWORD_CHANGE) + @Operation(summary = "Change password", description = "Change password of the current user.", tags = "Me") + @ApiResponse(responseCode = "204", description = "update success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) public Response changePassword(@Valid PasswordChangeDto passwordChange) { userManager.changePasswordForLoggedInUser( passwordService.encryptPassword(passwordChange.getOldPassword()), From 3f9d61ca99488f6b47a8568a667cc543a26e6a58 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 19 Feb 2020 08:48:51 +0100 Subject: [PATCH 08/33] inject props to each extension --- scm-ui/ui-components/src/layout/Footer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 63e294bb01..a74551198a 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -17,7 +17,8 @@ class Footer extends React.Component { return ""; } - const extensions = binder.getExtensions("footer.links", { me, links }); + const extensionProps = { me, links }; + const extensions = binder.getExtensions("footer.links", extensionProps); return (