mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-10-26 08:06:09 +01:00
Merge with default
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -5,12 +5,22 @@ 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
|
||||
- Create OpenAPI specification during build
|
||||
- Extension point entries with supplied extensionName are sorted ascending
|
||||
|
||||
### Changed
|
||||
- New footer design
|
||||
- Update svnkit to version 1.10.1-scm1
|
||||
|
||||
### Fixed
|
||||
- Modification for mercurial repositories with enabled XSRF protection
|
||||
- Does not throw NullPointerException when merge fails without normal merge conflicts
|
||||
- Keep file attributes on modification
|
||||
|
||||
### Removed
|
||||
- Enunciate rest documentation
|
||||
|
||||
## 2.0.0-rc4 - 2020-02-14
|
||||
### Added
|
||||
@@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Use icon only buttons for diff file controls
|
||||
- Upgrade [Legman](https://github.com/sdorra/legman) to v1.6.2 in order to fix execution on Java versions > 8
|
||||
- Upgrade [Lombok](https://projectlombok.org/) to version 1.18.10 in order to fix build on Java versions > 8
|
||||
- Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8
|
||||
- Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8
|
||||
- Upgrade smp-maven-plugin to version 1.0.0-rc3
|
||||
|
||||
### Fixed
|
||||
|
||||
10
Jenkinsfile
vendored
10
Jenkinsfile
vendored
@@ -29,11 +29,11 @@ node('docker') {
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
mvn 'clean install -Pdoc -DskipTests'
|
||||
mvn 'clean install -DskipTests'
|
||||
}
|
||||
|
||||
stage('Unit Test') {
|
||||
mvn 'test -Pcoverage -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true'
|
||||
mvn 'test -Pcoverage -Dmaven.test.failure.ignore=true'
|
||||
}
|
||||
|
||||
stage('Integration Test') {
|
||||
@@ -67,7 +67,6 @@ node('docker') {
|
||||
stage('Archive') {
|
||||
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
|
||||
archiveArtifacts 'scm-server/target/scm-server-app.*'
|
||||
archiveArtifacts 'scm-webapp/target/scm-webapp-restdocs.zip'
|
||||
}
|
||||
|
||||
stage('Docker') {
|
||||
@@ -97,9 +96,6 @@ node('docker') {
|
||||
// Archive Unit and integration test results, if any
|
||||
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
|
||||
|
||||
// Find maven warnings and visualize in job
|
||||
warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true
|
||||
|
||||
mailIfStatusChanged(commitAuthorEmail)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +104,7 @@ String mainBranch
|
||||
|
||||
Maven setupMavenBuild() {
|
||||
// Keep this version number in sync with .mvn/maven-wrapper.properties
|
||||
Maven mvn = new MavenInDocker(this, '3.6.3-jdk-11')
|
||||
Maven mvn = new MavenWrapper(this)
|
||||
|
||||
if (isMainBranch()) {
|
||||
// Release starts javadoc, which takes very long, so do only for certain branches
|
||||
|
||||
31
pom.xml
31
pom.xml
@@ -184,12 +184,6 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-core-annotations</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-jdk8</artifactId>
|
||||
@@ -266,6 +260,12 @@
|
||||
<version>${jaxrs.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
@@ -447,16 +447,10 @@
|
||||
<version>2.3</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-maven-plugin</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.0.0-rc3</version>
|
||||
<version>1.0.0-rc4</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
@@ -465,6 +459,12 @@
|
||||
<version>2.8.2</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>io.openapitools.swagger</groupId>
|
||||
<artifactId>swagger-maven-plugin</artifactId>
|
||||
<version>2.1.2</version>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
@@ -831,7 +831,6 @@
|
||||
<jaxrs.version>2.1.1</jaxrs.version>
|
||||
<resteasy.version>4.4.1.Final</resteasy.version>
|
||||
<jersey-client.version>1.19.4</jersey-client.version>
|
||||
<enunciate.version>2.11.1</enunciate.version>
|
||||
<jackson.version>2.10.0</jackson.version>
|
||||
<guice.version>4.0</guice.version>
|
||||
<jaxb.version>2.3.0</jaxb.version>
|
||||
@@ -856,8 +855,8 @@
|
||||
<guava.version>26.0-jre</guava.version>
|
||||
|
||||
<!-- frontend -->
|
||||
<nodejs.version>10.16.0</nodejs.version>
|
||||
<yarn.version>1.16.0</yarn.version>
|
||||
<nodejs.version>12.16.1</nodejs.version>
|
||||
<yarn.version>1.22.0</yarn.version>
|
||||
|
||||
<!-- build properties -->
|
||||
<project.build.javaLevel>8</project.build.javaLevel>
|
||||
|
||||
@@ -137,12 +137,6 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- rest documentation -->
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-core-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- event bus -->
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
@@ -22,6 +24,8 @@ import static sonia.scm.NotFoundException.notFound;
|
||||
*/
|
||||
public interface ModifyWorkerHelper extends ModifyCommand.Worker {
|
||||
|
||||
Logger LOG = LoggerFactory.getLogger(ModifyWorkerHelper.class);
|
||||
|
||||
@Override
|
||||
default void delete(String toBeDeleted) throws IOException {
|
||||
Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath();
|
||||
@@ -57,7 +61,11 @@ public interface ModifyWorkerHelper extends ModifyCommand.Worker {
|
||||
if (!targetFile.toFile().exists()) {
|
||||
throw notFound(createFileContext(path));
|
||||
}
|
||||
boolean executable = Files.isExecutable(targetFile);
|
||||
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
if (targetFile.toFile().setExecutable(executable)) {
|
||||
LOG.warn("could not set executable flag for file {}", targetFile);
|
||||
}
|
||||
addFileToScm(path, targetFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class ModifyWorkerHelperTest {
|
||||
|
||||
@Test
|
||||
void shouldKeepExecutableFlag(@TempDirectory.TempDir Path temp) throws IOException {
|
||||
|
||||
File target = createFile(temp, "executable.sh");
|
||||
File newFile = createFile(temp, "other");
|
||||
|
||||
target.setExecutable(true);
|
||||
|
||||
ModifyWorkerHelper helper = new MinimalModifyWorkerHelper(temp);
|
||||
|
||||
helper.modify("executable.sh", newFile);
|
||||
|
||||
assertThat(target.canExecute()).isTrue();
|
||||
}
|
||||
|
||||
private File createFile(Path temp, String fileName) throws IOException {
|
||||
File file = new File(temp.toFile(), fileName);
|
||||
FileWriter source = new FileWriter(file);
|
||||
source.write("something");
|
||||
source.close();
|
||||
return file;
|
||||
}
|
||||
|
||||
private static class MinimalModifyWorkerHelper implements ModifyWorkerHelper {
|
||||
|
||||
private final Path temp;
|
||||
|
||||
public MinimalModifyWorkerHelper(Path temp) {
|
||||
this.temp = temp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doScmDelete(String toBeDeleted) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFileToScm(String name, Path file) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getWorkDir() {
|
||||
return temp.toFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getRepository() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBranch() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- openapi documentation -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- test scope -->
|
||||
|
||||
<dependency>
|
||||
@@ -136,100 +143,37 @@
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>io.openapitools.swagger</groupId>
|
||||
<artifactId>swagger-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<resourcePackages>
|
||||
<resourcePackage>sonia.scm.api.v2.resources</resourcePackage>
|
||||
</resourcePackages>
|
||||
<outputDirectory>${basedir}/target/classes/META-INF/scm</outputDirectory>
|
||||
<outputFilename>openapi</outputFilename>
|
||||
<outputFormats>JSON,YAML</outputFormats>
|
||||
<prettyPrint>true</prettyPrint>
|
||||
<swaggerConfig>
|
||||
<info>
|
||||
<title>SCM-Manager Plugin REST-API</title>
|
||||
<version>${project.version}</version>
|
||||
<license>
|
||||
<url>http://www.opensource.org/licenses/bsd-license.php</url>
|
||||
<name>BSD</name>
|
||||
</license>
|
||||
</info>
|
||||
</swaggerConfig>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>generate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>plugin-doc</id>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-enunciate-configuration</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/doc</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>**/enunciate.xml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>docs</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<configFile>${project.build.directory}/enunciate.xml</configFile>
|
||||
<docsDir>${project.build.directory}</docsDir>
|
||||
<docsSubdir>restdocs</docsSubdir>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-top</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-swagger</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-lombok</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/main/doc/assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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.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 +18,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 +51,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();
|
||||
@@ -74,13 +91,20 @@ 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")
|
||||
})
|
||||
@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 +118,7 @@ public class GitConfigResource {
|
||||
}
|
||||
|
||||
@Path("{namespace}/{name}")
|
||||
public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
public GitRepositoryConfigResource getRepositoryConfig() {
|
||||
return gitRepositoryConfigResource.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 +13,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 +45,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 +82,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();
|
||||
|
||||
@@ -88,7 +88,14 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeC
|
||||
}
|
||||
|
||||
MergeCommandResult analyseFailure(MergeResult result) {
|
||||
logger.info("could not merge branch {} into {} due to conflict in paths {}", branchToMerge, targetBranch, result.getConflicts().keySet());
|
||||
logger.info("could not merge branch {} into {} with merge status '{}' due to ...", branchToMerge, targetBranch, result.getMergeStatus());
|
||||
logger.info("... conflicts: {}", result.getConflicts());
|
||||
logger.info("... checkout conflicts: {}", result.getCheckoutConflicts());
|
||||
logger.info("... failing paths: {}", result.getFailingPaths());
|
||||
logger.info("... message: {}", result);
|
||||
if (result.getConflicts() == null) {
|
||||
throw new UnexpectedMergeResultException(getRepository(), result);
|
||||
}
|
||||
return MergeCommandResult.failure(targetRevision.name(), revisionToMerge.name(), result.getConflicts().keySet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.MergeResult;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
class UnexpectedMergeResultException extends ExceptionWithContext {
|
||||
|
||||
public static final String CODE = "4GRrgkSC01";
|
||||
|
||||
public UnexpectedMergeResultException(Repository repository, MergeResult result) {
|
||||
super(ContextEntry.ContextBuilder.entity(repository).build(), createMessage(result));
|
||||
}
|
||||
|
||||
private static String createMessage(MergeResult result) {
|
||||
return "unexpected merge result: " + result
|
||||
+ "\nconflicts: " + result.getConflicts()
|
||||
+ "\ncheckout conflicts: " + result.getCheckoutConflicts()
|
||||
+ "\nfailing paths: " + result.getFailingPaths();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -113,5 +113,4 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
||||
|
||||
/** Field description */
|
||||
private GitContext context;
|
||||
private ScmTransportProtocol scmTransportProtocol;
|
||||
}
|
||||
|
||||
@@ -12,16 +12,23 @@ import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
import sonia.scm.repository.api.MergeStrategy;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -163,11 +170,34 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleUnexpectedMergeResults() {
|
||||
GitMergeCommand command = createCommand(git -> {
|
||||
try {
|
||||
FileWriter fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true);
|
||||
BufferedWriter bw = new BufferedWriter(fw);
|
||||
bw.write("change");
|
||||
bw.newLine();
|
||||
bw.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
MergeCommandRequest request = new MergeCommandRequest();
|
||||
request.setBranchToMerge("mergeable");
|
||||
request.setTargetBranch("master");
|
||||
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
request.setMessageTemplate("simple");
|
||||
|
||||
Assertions.assertThrows(UnexpectedMergeResultException.class, () -> command.merge(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
|
||||
SimplePrincipalCollection principals = new SimplePrincipalCollection();
|
||||
principals.add("admin", REALM);
|
||||
principals.add( new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM);
|
||||
principals.add(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM);
|
||||
shiro.setSubject(
|
||||
new Subject.Builder()
|
||||
.principals(principals)
|
||||
@@ -364,6 +394,20 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
private GitMergeCommand createCommand() {
|
||||
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
|
||||
return createCommand(git -> {
|
||||
});
|
||||
}
|
||||
|
||||
private GitMergeCommand createCommand(Consumer<Git> interceptor) {
|
||||
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())) {
|
||||
@Override
|
||||
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
|
||||
Function<Git, W> interceptedWorkerSupplier = git -> {
|
||||
interceptor.accept(git);
|
||||
return workerSupplier.apply(git);
|
||||
};
|
||||
return super.inClone(interceptedWorkerSupplier, workdirFactory, initialBranch);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
@@ -26,12 +24,6 @@ public class LegacyRepositoryService {
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@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 \"repository:read:global\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) {
|
||||
Repository repo = repositoryManager.get(repositoryId);
|
||||
if (repo == null) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
BIN
scm-ui/ui-components/src/__resources__/hitchhiker.png
Normal file
BIN
scm-ui/ui-components/src/__resources__/hitchhiker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
scm-ui/ui-components/src/__resources__/marvin.jpg
Normal file
BIN
scm-ui/ui-components/src/__resources__/marvin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -484,7 +484,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Binaries 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -753,7 +753,7 @@ exports[`Storyshots Diff Binaries 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Collapsed 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi ckdmuY panel is-size-6"
|
||||
@@ -1102,7 +1102,7 @@ exports[`Storyshots Diff Collapsed 1`] = `
|
||||
|
||||
exports[`Storyshots Diff CollapsingWithFunction 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi ckdmuY panel is-size-6"
|
||||
@@ -3028,7 +3028,7 @@ exports[`Storyshots Diff CollapsingWithFunction 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Default 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -6936,7 +6936,7 @@ exports[`Storyshots Diff Default 1`] = `
|
||||
|
||||
exports[`Storyshots Diff File Annotation 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -10868,7 +10868,7 @@ exports[`Storyshots Diff File Annotation 1`] = `
|
||||
|
||||
exports[`Storyshots Diff File Controls 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -14884,7 +14884,7 @@ exports[`Storyshots Diff File Controls 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Hunks 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -15721,7 +15721,7 @@ exports[`Storyshots Diff Hunks 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Line Annotation 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -19665,7 +19665,7 @@ exports[`Storyshots Diff Line Annotation 1`] = `
|
||||
|
||||
exports[`Storyshots Diff OnClick 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -23847,7 +23847,7 @@ exports[`Storyshots Diff OnClick 1`] = `
|
||||
|
||||
exports[`Storyshots Diff Side-By-Side 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -28284,7 +28284,7 @@ exports[`Storyshots Diff Side-By-Side 1`] = `
|
||||
|
||||
exports[`Storyshots Diff SyntaxHighlighting 1`] = `
|
||||
<div
|
||||
className="sc-TOsTZ flmUBf"
|
||||
className="sc-hmzhuo TypKC"
|
||||
>
|
||||
<div
|
||||
className="sc-gZMcBi iABzaT panel is-size-6"
|
||||
@@ -32192,7 +32192,7 @@ exports[`Storyshots Diff SyntaxHighlighting 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
<div
|
||||
className="sc-gisBJw jHakbY"
|
||||
className="sc-kgAjT khfRmZ"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -32237,7 +32237,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
<div
|
||||
className="sc-gisBJw jHakbY"
|
||||
className="sc-kgAjT khfRmZ"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -32265,7 +32265,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
<div
|
||||
className="sc-kjoXOD hVPZau"
|
||||
className="sc-cJSrbW hLoADP"
|
||||
>
|
||||
<label
|
||||
className="sc-cMljjf kOqpHe radio"
|
||||
@@ -32294,7 +32294,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||
<div
|
||||
className="sc-kjoXOD hVPZau"
|
||||
className="sc-cJSrbW hLoADP"
|
||||
>
|
||||
<label
|
||||
className="sc-cMljjf kOqpHe radio"
|
||||
@@ -32314,7 +32314,7 @@ exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Textarea OnCancel 1`] = `
|
||||
<div
|
||||
className="sc-cHGsZl klfJMr"
|
||||
className="sc-ksYbfQ ePXdiL"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -32337,7 +32337,7 @@ exports[`Storyshots Forms|Textarea OnCancel 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Textarea OnChange 1`] = `
|
||||
<div
|
||||
className="sc-cHGsZl klfJMr"
|
||||
className="sc-ksYbfQ ePXdiL"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -32364,7 +32364,7 @@ exports[`Storyshots Forms|Textarea OnChange 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
|
||||
<div
|
||||
className="sc-cHGsZl klfJMr"
|
||||
className="sc-ksYbfQ ePXdiL"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -32389,6 +32389,514 @@ exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Layout|Footer Default 1`] = `
|
||||
<footer
|
||||
className="footer"
|
||||
>
|
||||
<section
|
||||
className="section container"
|
||||
>
|
||||
<div
|
||||
className="columns is-size-7"
|
||||
>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-user-circle fa-fw"
|
||||
/>
|
||||
|
||||
Trillian McMillian
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me"
|
||||
onClick={[Function]}
|
||||
>
|
||||
footer.user.profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me/settings/password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
profile.changePasswordNavLink
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-info-circle fa-fw"
|
||||
/>
|
||||
|
||||
footer.information.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/"
|
||||
target="_blank"
|
||||
>
|
||||
SCM-Manager 2.0.0
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-life-ring fa-fw"
|
||||
/>
|
||||
|
||||
footer.support.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/support/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://cloudogu.com/en/scm-manager-enterprise/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.enterprise
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Layout|Footer Full 1`] = `
|
||||
<footer
|
||||
className="footer"
|
||||
>
|
||||
<section
|
||||
className="section container"
|
||||
>
|
||||
<div
|
||||
className="columns is-size-7"
|
||||
>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<span
|
||||
className="sc-fMiknA fyPpQQ image is-rounded"
|
||||
>
|
||||
<img
|
||||
alt="trillian"
|
||||
className="is-rounded sc-fBuWsC djJrAv"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
Trillian McMillian
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me"
|
||||
onClick={[Function]}
|
||||
>
|
||||
footer.user.profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me/settings/password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
profile.changePasswordNavLink
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Authorized Keys
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-info-circle fa-fw"
|
||||
/>
|
||||
|
||||
footer.information.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/"
|
||||
target="_blank"
|
||||
>
|
||||
SCM-Manager 2.0.0
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
REST API
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
CLI
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-life-ring fa-fw"
|
||||
/>
|
||||
|
||||
footer.support.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/support/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://cloudogu.com/en/scm-manager-enterprise/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.enterprise
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Layout|Footer With Avatar 1`] = `
|
||||
<footer
|
||||
className="footer"
|
||||
>
|
||||
<section
|
||||
className="section container"
|
||||
>
|
||||
<div
|
||||
className="columns is-size-7"
|
||||
>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<span
|
||||
className="sc-fMiknA fyPpQQ image is-rounded"
|
||||
>
|
||||
<img
|
||||
alt="trillian"
|
||||
className="is-rounded sc-fBuWsC djJrAv"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
Trillian McMillian
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me"
|
||||
onClick={[Function]}
|
||||
>
|
||||
footer.user.profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me/settings/password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
profile.changePasswordNavLink
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-info-circle fa-fw"
|
||||
/>
|
||||
|
||||
footer.information.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/"
|
||||
target="_blank"
|
||||
>
|
||||
SCM-Manager 2.0.0
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-life-ring fa-fw"
|
||||
/>
|
||||
|
||||
footer.support.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/support/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://cloudogu.com/en/scm-manager-enterprise/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.enterprise
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
|
||||
<footer
|
||||
className="footer"
|
||||
>
|
||||
<section
|
||||
className="section container"
|
||||
>
|
||||
<div
|
||||
className="columns is-size-7"
|
||||
>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-user-circle fa-fw"
|
||||
/>
|
||||
|
||||
Trillian McMillian
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me"
|
||||
onClick={[Function]}
|
||||
>
|
||||
footer.user.profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/me/settings/password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
profile.changePasswordNavLink
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className=""
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Authorized Keys
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-info-circle fa-fw"
|
||||
/>
|
||||
|
||||
footer.information.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/"
|
||||
target="_blank"
|
||||
>
|
||||
SCM-Manager 2.0.0
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
REST API
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
CLI
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section
|
||||
className="column is-one-third"
|
||||
>
|
||||
<div
|
||||
className="sc-hzDkRC jeksqW"
|
||||
>
|
||||
<i
|
||||
className="fas fa-life-ring fa-fw"
|
||||
/>
|
||||
|
||||
footer.support.title
|
||||
</div>
|
||||
<ul
|
||||
className="sc-jhAzac eoWThz"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.scm-manager.org/support/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://cloudogu.com/en/scm-manager-enterprise/"
|
||||
target="_blank"
|
||||
>
|
||||
footer.support.enterprise
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
>
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Loading Default 1`] = `
|
||||
<div>
|
||||
<div
|
||||
@@ -34243,7 +34751,7 @@ PORT_NUMBER =
|
||||
|
||||
exports[`Storyshots Table|Table Default 1`] = `
|
||||
<table
|
||||
className="sc-fBuWsC eeihxG table content is-hoverable"
|
||||
className="sc-fAjcbJ byigni table content is-hoverable"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -34261,7 +34769,7 @@ exports[`Storyshots Table|Table Default 1`] = `
|
||||
>
|
||||
Last Name
|
||||
<i
|
||||
className="fas fa-sort-amount-down has-text-grey-light sc-jhAzac gDbcZp"
|
||||
className="fas fa-sort-amount-down has-text-grey-light sc-eqIVtm jxAoDg"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
@@ -34334,7 +34842,7 @@ exports[`Storyshots Table|Table Empty 1`] = `
|
||||
|
||||
exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||
<table
|
||||
className="sc-fBuWsC eeihxG table content is-hoverable"
|
||||
className="sc-fAjcbJ byigni table content is-hoverable"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -34346,7 +34854,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||
>
|
||||
Id
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
@@ -34357,7 +34865,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||
>
|
||||
Name
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
@@ -34368,7 +34876,7 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||
>
|
||||
Description
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-jhAzac gDbcZp"
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-eqIVtm jxAoDg"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import React, { FC } from "react";
|
||||
import { Image } from "..";
|
||||
import { Person } from "./Avatar";
|
||||
import { EXTENSION_POINT } from "./Avatar";
|
||||
import { useBinder } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
person: Person;
|
||||
representation?: "rounded" | "rounded-border";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
class AvatarImage extends React.Component<Props> {
|
||||
render() {
|
||||
const { person } = this.props;
|
||||
const AvatarImage: FC<Props> = ({ person, representation = "rounded-border", className }) => {
|
||||
const binder = useBinder();
|
||||
const avatarFactory = binder.getExtension(EXTENSION_POINT);
|
||||
if (avatarFactory) {
|
||||
const avatar = avatarFactory(person);
|
||||
|
||||
const avatarFactory = binder.getExtension(EXTENSION_POINT);
|
||||
if (avatarFactory) {
|
||||
const avatar = avatarFactory(person);
|
||||
|
||||
return <Image className="has-rounded-border" src={avatar} alt={person.name} />;
|
||||
let classes = representation === "rounded" ? "is-rounded" : "has-rounded-border";
|
||||
if (className) {
|
||||
classes += " " + className;
|
||||
}
|
||||
|
||||
return null;
|
||||
return <Image className={classes} src={avatar} alt={person.name} />;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default AvatarImage;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import React, { FC } from "react";
|
||||
import { useBinder } from "@scm-manager/ui-extensions";
|
||||
import { EXTENSION_POINT } from "./Avatar";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
const AvatarWrapper: FC = ({ children }) => {
|
||||
const binder = useBinder();
|
||||
if (binder.hasExtension(EXTENSION_POINT)) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
class AvatarWrapper extends Component<Props> {
|
||||
render() {
|
||||
if (binder.hasExtension(EXTENSION_POINT)) {
|
||||
return <>{this.props.children}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarWrapper;
|
||||
|
||||
@@ -15,7 +15,7 @@ export const byKey = (key: string) => {
|
||||
}
|
||||
|
||||
if (isUndefined(b, key)) {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a[key] < b[key]) {
|
||||
@@ -35,7 +35,7 @@ export const byValueLength = (key: string) => {
|
||||
}
|
||||
|
||||
if (isUndefined(b, key)) {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a[key].length < b[key].length) {
|
||||
@@ -55,7 +55,7 @@ export const byNestedKeys = (key: string, nestedKey: string) => {
|
||||
}
|
||||
|
||||
if (isUndefined(b, key, nestedKey)) {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a[key][nestedKey] < b[key][nestedKey]) {
|
||||
|
||||
@@ -41,7 +41,7 @@ class ConfigurationBinder {
|
||||
});
|
||||
|
||||
// bind navigation link to extension point
|
||||
binder.bind("admin.setting", ConfigNavLink, configPredicate);
|
||||
binder.bind("admin.setting", ConfigNavLink, configPredicate, labelI18nKey);
|
||||
|
||||
// route for global configuration, passes the link from the index resource to component
|
||||
const ConfigRoute = ({ url, links, ...additionalProps }: GlobalRouteProps) => {
|
||||
|
||||
62
scm-ui/ui-components/src/layout/Footer.stories.tsx
Normal file
62
scm-ui/ui-components/src/layout/Footer.stories.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Footer from "./Footer";
|
||||
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
|
||||
import { Me } from "@scm-manager/ui-types";
|
||||
import { EXTENSION_POINT } from "../avatar/Avatar";
|
||||
// @ts-ignore ignore unknown png
|
||||
import hitchhiker from "../__resources__/hitchhiker.png";
|
||||
// @ts-ignore ignore unknown jpg
|
||||
import marvin from "../__resources__/marvin.jpg";
|
||||
import NavLink from "../navigation/NavLink";
|
||||
import ExternalLink from "../navigation/ExternalLink";
|
||||
|
||||
const trillian: Me = {
|
||||
name: "trillian",
|
||||
displayName: "Trillian McMillian",
|
||||
mail: "tricia@hitchhiker.com",
|
||||
groups: ["crew"],
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const bindAvatar = (binder: Binder, avatar: string) => {
|
||||
binder.bind(EXTENSION_POINT, () => {
|
||||
return avatar;
|
||||
});
|
||||
};
|
||||
|
||||
const bindLinks = (binder: Binder) => {
|
||||
binder.bind("footer.information", () => <ExternalLink to="#" label="REST API" />);
|
||||
binder.bind("footer.information", () => <ExternalLink to="#" label="CLI" />);
|
||||
binder.bind("footer.support", () => <ExternalLink to="#" label="FAQ" />);
|
||||
binder.bind("profile.setting", () => <NavLink label="Authorized Keys" to="#" />);
|
||||
};
|
||||
|
||||
const withBinder = (binder: Binder) => {
|
||||
return (
|
||||
<BinderContext.Provider value={binder}>
|
||||
<Footer me={trillian} version="2.0.0" links={{}} />
|
||||
</BinderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Layout|Footer", module)
|
||||
.add("Default", () => {
|
||||
return <Footer me={trillian} version="2.0.0" links={{}} />;
|
||||
})
|
||||
.add("With Avatar", () => {
|
||||
const binder = new Binder("avatar-story");
|
||||
bindAvatar(binder, hitchhiker);
|
||||
return withBinder(binder);
|
||||
})
|
||||
.add("With Plugin Links", () => {
|
||||
const binder = new Binder("link-story");
|
||||
bindLinks(binder);
|
||||
return withBinder(binder);
|
||||
})
|
||||
.add("Full", () => {
|
||||
const binder = new Binder("link-story");
|
||||
bindAvatar(binder, marvin);
|
||||
bindLinks(binder);
|
||||
return withBinder(binder);
|
||||
});
|
||||
@@ -1,27 +1,93 @@
|
||||
import React from "react";
|
||||
import { Me } from "@scm-manager/ui-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { FC } from "react";
|
||||
import { Me, Links } from "@scm-manager/ui-types";
|
||||
import { useBinder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { AvatarImage } from "../avatar";
|
||||
import NavLink from "../navigation/NavLink";
|
||||
import FooterSection from "./FooterSection";
|
||||
import styled from "styled-components";
|
||||
import { EXTENSION_POINT } from "../avatar/Avatar";
|
||||
import ExternalLink from "../navigation/ExternalLink";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
me?: Me;
|
||||
version: string;
|
||||
links: Links;
|
||||
};
|
||||
|
||||
class Footer extends React.Component<Props> {
|
||||
render() {
|
||||
const { me } = this.props;
|
||||
if (!me) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container is-centered">
|
||||
<p className="has-text-centered">
|
||||
<Link to={"/me"}>{me.displayName}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
type TitleWithIconsProps = {
|
||||
title: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
const TitleWithIcon: FC<TitleWithIconsProps> = ({ icon, title }) => (
|
||||
<>
|
||||
<i className={`fas fa-${icon} fa-fw`} /> {title}
|
||||
</>
|
||||
);
|
||||
|
||||
type TitleWithAvatarProps = {
|
||||
me: Me;
|
||||
};
|
||||
|
||||
const VCenteredAvatar = styled(AvatarImage)`
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
const AvatarContainer = styled.span`
|
||||
float: left;
|
||||
margin-right: 0.3em;
|
||||
padding-top: 0.2em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
`;
|
||||
|
||||
const TitleWithAvatar: FC<TitleWithAvatarProps> = ({ me }) => (
|
||||
<>
|
||||
<AvatarContainer className="image is-rounded">
|
||||
<VCenteredAvatar person={me} representation="rounded" />
|
||||
</AvatarContainer>
|
||||
{me.displayName}
|
||||
</>
|
||||
);
|
||||
|
||||
const Footer: FC<Props> = ({ me, version, links }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
const binder = useBinder();
|
||||
if (!me) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const extensionProps = { me, url: "/me", links };
|
||||
let meSectionTile;
|
||||
if (binder.hasExtension(EXTENSION_POINT)) {
|
||||
meSectionTile = <TitleWithAvatar me={me} />;
|
||||
} else {
|
||||
meSectionTile = <TitleWithIcon title={me.displayName} icon="user-circle" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="footer">
|
||||
<section className="section container">
|
||||
<div className="columns is-size-7">
|
||||
<FooterSection title={meSectionTile}>
|
||||
<NavLink to="/me" label={t("footer.user.profile")} />
|
||||
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
|
||||
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
||||
</FooterSection>
|
||||
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
|
||||
<ExternalLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
|
||||
<ExtensionPoint name="footer.information" props={extensionProps} renderAll={true} />
|
||||
</FooterSection>
|
||||
<FooterSection title={<TitleWithIcon title={t("footer.support.title")} icon="life-ring" />}>
|
||||
<ExternalLink to="https://www.scm-manager.org/support/" label={t("footer.support.community")} />
|
||||
<ExternalLink to="https://cloudogu.com/en/scm-manager-enterprise/" label={t("footer.support.enterprise")} />
|
||||
<ExtensionPoint name="footer.support" props={extensionProps} renderAll={true} />
|
||||
</FooterSection>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
||||
26
scm-ui/ui-components/src/layout/FooterSection.tsx
Normal file
26
scm-ui/ui-components/src/layout/FooterSection.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
title: ReactNode;
|
||||
};
|
||||
|
||||
const Title = styled.div`
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
`;
|
||||
|
||||
const Menu = styled.ul`
|
||||
padding-left: 1.1rem;
|
||||
`;
|
||||
|
||||
const FooterSection: FC<Props> = ({ title, children }) => {
|
||||
return (
|
||||
<section className="column is-one-third">
|
||||
<Title>{title}</Title>
|
||||
<Menu>{children}</Menu>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterSection;
|
||||
30
scm-ui/ui-components/src/navigation/ExternalLink.tsx
Normal file
30
scm-ui/ui-components/src/navigation/ExternalLink.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
to: string;
|
||||
icon?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const ExternalLink: FC<Props> = ({ to, icon, label }) => {
|
||||
let showIcon;
|
||||
if (icon) {
|
||||
showIcon = (
|
||||
<>
|
||||
<i className={classNames(icon, "fa-fw")} />{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a target="_blank" href={to}>
|
||||
{showIcon}
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalLink;
|
||||
@@ -33,12 +33,6 @@ describe("ExtensionPoint test", () => {
|
||||
expect(rendered.text()).toBe("Extension One");
|
||||
});
|
||||
|
||||
// We use this wrapper since Enzyme cannot handle React Fragments (see https://github.com/airbnb/enzyme/issues/1213)
|
||||
class ExtensionPointEnzymeFix extends ExtensionPoint {
|
||||
render() {
|
||||
return <div>{super.render()}</div>;
|
||||
}
|
||||
}
|
||||
it("should render the given components", () => {
|
||||
const labelOne = () => {
|
||||
return <label>Extension One</label>;
|
||||
@@ -50,7 +44,7 @@ describe("ExtensionPoint test", () => {
|
||||
mockedBinder.hasExtension.mockReturnValue(true);
|
||||
mockedBinder.getExtensions.mockReturnValue([labelOne, labelTwo]);
|
||||
|
||||
const rendered = mount(<ExtensionPointEnzymeFix name="something.special" renderAll={true} />);
|
||||
const rendered = mount(<ExtensionPoint name="something.special" renderAll={true} />);
|
||||
const text = rendered.text();
|
||||
expect(text).toContain("Extension One");
|
||||
expect(text).toContain("Extension Two");
|
||||
@@ -143,4 +137,12 @@ describe("ExtensionPoint test", () => {
|
||||
const text = rendered.text();
|
||||
expect(text).toBe("Hello Trillian");
|
||||
});
|
||||
|
||||
it("should not render nothing without extension and without default", () => {
|
||||
mockedBinder.hasExtension.mockReturnValue(false);
|
||||
|
||||
const rendered = mount(<ExtensionPoint name="something.special" />);
|
||||
const text = rendered.text();
|
||||
expect(text).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
import * as React from "react";
|
||||
import binder from "./binder";
|
||||
import { Binder } from "./binder";
|
||||
import { FC, ReactNode } from "react";
|
||||
import useBinder from "./useBinder";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
renderAll?: boolean;
|
||||
props?: object;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const renderAllExtensions = (binder: Binder, name: string, props?: object) => {
|
||||
const extensions = binder.getExtensions(name, props);
|
||||
return (
|
||||
<>
|
||||
{extensions.map((Component, index) => {
|
||||
return <Component key={index} {...props} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSingleExtension = (binder: Binder, name: string, props?: object) => {
|
||||
const Component = binder.getExtension(name, props);
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const renderDefault = (children: ReactNode) => {
|
||||
if (children) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* ExtensionPoint renders components which are bound to an extension point.
|
||||
*/
|
||||
class ExtensionPoint extends React.Component<Props> {
|
||||
renderAll(name: string, props?: object) {
|
||||
const extensions = binder.getExtensions(name, props);
|
||||
return (
|
||||
<>
|
||||
{extensions.map((Component, index) => {
|
||||
return <Component key={index} {...props} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
const ExtensionPoint: FC<Props> = ({ name, renderAll, props, children }) => {
|
||||
const binder = useBinder();
|
||||
if (!binder.hasExtension(name, props)) {
|
||||
return renderDefault(children);
|
||||
} else if (renderAll) {
|
||||
return renderAllExtensions(binder, name, props);
|
||||
}
|
||||
|
||||
renderSingle(name: string, props?: object) {
|
||||
const Component = binder.getExtension(name, props);
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
renderDefault() {
|
||||
const { children } = this.props;
|
||||
if (children) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, renderAll, props } = this.props;
|
||||
if (!binder.hasExtension(name, props)) {
|
||||
return this.renderDefault();
|
||||
} else if (renderAll) {
|
||||
return this.renderAll(name, props);
|
||||
}
|
||||
return this.renderSingle(name, props);
|
||||
}
|
||||
}
|
||||
return renderSingleExtension(binder, name, props);
|
||||
};
|
||||
|
||||
export default ExtensionPoint;
|
||||
|
||||
@@ -4,7 +4,7 @@ describe("binder tests", () => {
|
||||
let binder: Binder;
|
||||
|
||||
beforeEach(() => {
|
||||
binder = new Binder();
|
||||
binder = new Binder("testing");
|
||||
});
|
||||
|
||||
it("should return an empty array for non existing extension points", () => {
|
||||
@@ -13,31 +13,31 @@ describe("binder tests", () => {
|
||||
});
|
||||
|
||||
it("should return the binded extensions", () => {
|
||||
binder.bind("hitchhicker.trillian", "heartOfGold");
|
||||
binder.bind("hitchhicker.trillian", "earth");
|
||||
binder.bind("hitchhiker.trillian", "heartOfGold");
|
||||
binder.bind("hitchhiker.trillian", "earth");
|
||||
|
||||
const extensions = binder.getExtensions("hitchhicker.trillian");
|
||||
const extensions = binder.getExtensions("hitchhiker.trillian");
|
||||
expect(extensions).toEqual(["heartOfGold", "earth"]);
|
||||
});
|
||||
|
||||
it("should return the first bound extension", () => {
|
||||
binder.bind("hitchhicker.trillian", "heartOfGold");
|
||||
binder.bind("hitchhicker.trillian", "earth");
|
||||
binder.bind("hitchhiker.trillian", "heartOfGold");
|
||||
binder.bind("hitchhiker.trillian", "earth");
|
||||
|
||||
expect(binder.getExtension("hitchhicker.trillian")).toBe("heartOfGold");
|
||||
expect(binder.getExtension("hitchhiker.trillian")).toBe("heartOfGold");
|
||||
});
|
||||
|
||||
it("should return null if no extension was bound", () => {
|
||||
expect(binder.getExtension("hitchhicker.trillian")).toBe(null);
|
||||
expect(binder.getExtension("hitchhiker.trillian")).toBe(null);
|
||||
});
|
||||
|
||||
it("should return true, if an extension is bound", () => {
|
||||
binder.bind("hitchhicker.trillian", "heartOfGold");
|
||||
expect(binder.hasExtension("hitchhicker.trillian")).toBe(true);
|
||||
binder.bind("hitchhiker.trillian", "heartOfGold");
|
||||
expect(binder.hasExtension("hitchhiker.trillian")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false, if no extension is bound", () => {
|
||||
expect(binder.hasExtension("hitchhicker.trillian")).toBe(false);
|
||||
expect(binder.hasExtension("hitchhiker.trillian")).toBe(false);
|
||||
});
|
||||
|
||||
type Props = {
|
||||
@@ -45,13 +45,34 @@ describe("binder tests", () => {
|
||||
};
|
||||
|
||||
it("should return only extensions which predicates matches", () => {
|
||||
binder.bind("hitchhicker.trillian", "heartOfGold", (props: Props) => props.category === "a");
|
||||
binder.bind("hitchhicker.trillian", "earth", (props: Props) => props.category === "b");
|
||||
binder.bind("hitchhicker.trillian", "earth2", (props: Props) => props.category === "a");
|
||||
binder.bind("hitchhiker.trillian", "heartOfGold", (props: Props) => props.category === "a");
|
||||
binder.bind("hitchhiker.trillian", "earth", (props: Props) => props.category === "b");
|
||||
binder.bind("hitchhiker.trillian", "earth2", (props: Props) => props.category === "a");
|
||||
|
||||
const extensions = binder.getExtensions("hitchhicker.trillian", {
|
||||
const extensions = binder.getExtensions("hitchhiker.trillian", {
|
||||
category: "b"
|
||||
});
|
||||
expect(extensions).toEqual(["earth"]);
|
||||
});
|
||||
|
||||
it("should return extensions in ascending order", () => {
|
||||
binder.bind("hitchhiker.trillian", "planetA", () => true, "zeroWaste");
|
||||
binder.bind("hitchhiker.trillian", "planetB", () => true, "EPSILON");
|
||||
binder.bind("hitchhiker.trillian", "planetC", () => true, "emptyBin");
|
||||
binder.bind("hitchhiker.trillian", "planetD", () => true, "absolute");
|
||||
|
||||
const extensions = binder.getExtensions("hitchhiker.trillian");
|
||||
expect(extensions).toEqual(["planetD", "planetC", "planetB", "planetA"]);
|
||||
});
|
||||
|
||||
it("should return extensions starting with entries with specified extensionName", () => {
|
||||
binder.bind("hitchhiker.trillian", "planetA", () => true);
|
||||
binder.bind("hitchhiker.trillian", "planetB", () => true, "zeroWaste");
|
||||
binder.bind("hitchhiker.trillian", "planetC", () => true);
|
||||
binder.bind("hitchhiker.trillian", "planetD", () => true, "emptyBin");
|
||||
|
||||
const extensions = binder.getExtensions("hitchhiker.trillian");
|
||||
expect(extensions[0]).toEqual("planetD");
|
||||
expect(extensions[1]).toEqual("planetB");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ type Predicate = (props: any) => boolean;
|
||||
type ExtensionRegistration = {
|
||||
predicate: Predicate;
|
||||
extension: any;
|
||||
extensionName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -10,11 +11,13 @@ type ExtensionRegistration = {
|
||||
* The Binder class is mainly exported for testing, plugins should only use the default export.
|
||||
*/
|
||||
export class Binder {
|
||||
name: string;
|
||||
extensionPoints: {
|
||||
[key: string]: Array<ExtensionRegistration>;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.extensionPoints = {};
|
||||
}
|
||||
|
||||
@@ -25,13 +28,14 @@ export class Binder {
|
||||
* @param extension provided extension
|
||||
* @param predicate to decide if the extension gets rendered for the given props
|
||||
*/
|
||||
bind(extensionPoint: string, extension: any, predicate?: Predicate) {
|
||||
bind(extensionPoint: string, extension: any, predicate?: Predicate, extensionName?: string) {
|
||||
if (!this.extensionPoints[extensionPoint]) {
|
||||
this.extensionPoints[extensionPoint] = [];
|
||||
}
|
||||
const registration = {
|
||||
predicate: predicate ? predicate : () => true,
|
||||
extension
|
||||
extension,
|
||||
extensionName: extensionName ? extensionName : ""
|
||||
};
|
||||
this.extensionPoints[extensionPoint].push(registration);
|
||||
}
|
||||
@@ -61,6 +65,7 @@ export class Binder {
|
||||
if (props) {
|
||||
registrations = registrations.filter(reg => reg.predicate(props || {}));
|
||||
}
|
||||
registrations.sort(this.sortExtensions);
|
||||
return registrations.map(reg => reg.extension);
|
||||
}
|
||||
|
||||
@@ -70,9 +75,28 @@ export class Binder {
|
||||
hasExtension(extensionPoint: string, props?: object): boolean {
|
||||
return this.getExtensions(extensionPoint, props).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort extensions in ascending order, starting with entries with specified extensionName.
|
||||
*/
|
||||
sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => {
|
||||
const regA = a.extensionName ? a.extensionName.toUpperCase() : "";
|
||||
const regB = b.extensionName ? b.extensionName.toUpperCase() : "";
|
||||
|
||||
if (regA === "" && regB !== "") {
|
||||
return 1;
|
||||
} else if (regA !== "" && regB === "") {
|
||||
return -1;
|
||||
} else if (regA > regB) {
|
||||
return 1;
|
||||
} else if (regA < regB) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
// singleton binder
|
||||
const binder = new Binder();
|
||||
const binder = new Binder("default");
|
||||
|
||||
export default binder;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as binder } from "./binder";
|
||||
export { default as binder, Binder } from "./binder";
|
||||
export * from "./useBinder";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint";
|
||||
|
||||
29
scm-ui/ui-extensions/src/useBinder.test.tsx
Normal file
29
scm-ui/ui-extensions/src/useBinder.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import useBinder, { BinderContext } from "./useBinder";
|
||||
import { Binder } from "./binder";
|
||||
import { mount } from "enzyme";
|
||||
import "@scm-manager/ui-tests/enzyme";
|
||||
import React from "react";
|
||||
|
||||
describe("useBinder tests", () => {
|
||||
const BinderName = () => {
|
||||
const binder = useBinder();
|
||||
return <>{binder.name}</>;
|
||||
};
|
||||
|
||||
it("should return default binder", () => {
|
||||
const rendered = mount(<BinderName />);
|
||||
expect(rendered.text()).toBe("default");
|
||||
});
|
||||
|
||||
it("should return binder from context", () => {
|
||||
const binder = new Binder("from-context");
|
||||
const app = (
|
||||
<BinderContext.Provider value={binder}>
|
||||
<BinderName />
|
||||
</BinderContext.Provider>
|
||||
);
|
||||
|
||||
const rendered = mount(app);
|
||||
expect(rendered.text()).toBe("from-context");
|
||||
});
|
||||
});
|
||||
16
scm-ui/ui-extensions/src/useBinder.ts
Normal file
16
scm-ui/ui-extensions/src/useBinder.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import defaultBinder from "./binder";
|
||||
|
||||
/**
|
||||
* The BinderContext should only be used to override the default binder for testing purposes.
|
||||
*/
|
||||
export const BinderContext = createContext(defaultBinder);
|
||||
|
||||
/**
|
||||
* Hook to get the binder from context.
|
||||
*/
|
||||
export const useBinder = () => {
|
||||
return useContext(BinderContext);
|
||||
};
|
||||
|
||||
export default useBinder;
|
||||
@@ -67,8 +67,14 @@ hr.header-with-actions {
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 50px;
|
||||
footer.footer {
|
||||
//height: 100px;
|
||||
background-color: $white-ter;
|
||||
padding: inherit;
|
||||
|
||||
a {
|
||||
color: darken($blue, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Import the rest of Bulma
|
||||
@@ -691,11 +697,6 @@ form .field:not(.is-grouped) {
|
||||
}
|
||||
}
|
||||
|
||||
// footer
|
||||
.footer {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
// aside
|
||||
.aside-background {
|
||||
bottom: 0;
|
||||
|
||||
@@ -4,6 +4,6 @@ export type Me = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
mail: string;
|
||||
groups: [];
|
||||
groups: string[];
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
@@ -86,5 +86,18 @@
|
||||
"passwordConfirmFailed": "Passwörter müssen identisch sein!",
|
||||
"submit": "Speichern",
|
||||
"changedSuccessfully": "Passwort erfolgreich geändert!"
|
||||
},
|
||||
"footer": {
|
||||
"user": {
|
||||
"profile": "Profil"
|
||||
},
|
||||
"information": {
|
||||
"title": "Information"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"community": "Community",
|
||||
"enterprise": "Enterprise"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,5 +87,18 @@
|
||||
"passwordConfirmFailed": "Passwords have to be identical",
|
||||
"submit": "Submit",
|
||||
"changedSuccessfully": "Password changed successfully"
|
||||
},
|
||||
"footer": {
|
||||
"user": {
|
||||
"profile": "Profile"
|
||||
},
|
||||
"information": {
|
||||
"title": "Information"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"community": "Community",
|
||||
"enterprise": "Enterprise"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Props> {
|
||||
}
|
||||
|
||||
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 ? <PrimaryNavigation links={links} /> : "";
|
||||
@@ -50,7 +52,7 @@ class App extends Component<Props> {
|
||||
<div className="App">
|
||||
<Header>{navigation}</Header>
|
||||
{content}
|
||||
{authenticated && <Footer me={me} />}
|
||||
{authenticated && <Footer me={me} version={version} links={links} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -426,6 +426,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.1.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -471,6 +478,49 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>io.openapitools.swagger</groupId>
|
||||
<artifactId>swagger-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<resourcePackages>
|
||||
<resourcePackage>sonia.scm.api.v2.resources</resourcePackage>
|
||||
</resourcePackages>
|
||||
<outputDirectory>${basedir}/target/openapi/META-INF/scm</outputDirectory>
|
||||
<outputFilename>openapi</outputFilename>
|
||||
<outputFormats>JSON,YAML</outputFormats>
|
||||
<prettyPrint>true</prettyPrint>
|
||||
<swaggerConfig>
|
||||
<servers>
|
||||
<server>
|
||||
<url>http://localhost:8081/scm/api</url>
|
||||
<description>local endpoint url</description>
|
||||
</server>
|
||||
</servers>
|
||||
<info>
|
||||
<title>SCM-Manager REST-API</title>
|
||||
<version>${project.version}</version>
|
||||
<contact>
|
||||
<email>scmmanager@googlegroups.com</email>
|
||||
<name>SCM-Manager</name>
|
||||
<url>https://scm-manager.org</url>
|
||||
</contact>
|
||||
<license>
|
||||
<url>http://www.opensource.org/licenses/bsd-license.php</url>
|
||||
<name>BSD</name>
|
||||
</license>
|
||||
</info>
|
||||
<descriptionFile>src/main/doc/openapi.md</descriptionFile>
|
||||
</swaggerConfig>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>generate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
@@ -511,9 +561,15 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>2.2</version>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<filteringDeploymentDescriptors>true</filteringDeploymentDescriptors>
|
||||
<webResources>
|
||||
<resource>
|
||||
<directory>target/openapi</directory>
|
||||
<targetPath>WEB-INF/classes</targetPath>
|
||||
</resource>
|
||||
</webResources>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -860,107 +916,9 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>doc</id>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-enunciate-configuration</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/doc</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>**/enunciate.xml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>docs</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<configFile>${project.build.directory}/enunciate.xml</configFile>
|
||||
<docsDir>${project.build.directory}</docsDir>
|
||||
<docsSubdir>restdocs</docsSubdir>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-top</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-swagger</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-lombok</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/main/doc/assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Copyright (c) 2010, Sebastian Sdorra
|
||||
All rights reserved.
|
||||
|
||||
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.
|
||||
3. Neither the name of SCM-Manager; nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
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 REGENTS 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.
|
||||
|
||||
http://bitbucket.org/sdorra/scm-manager
|
||||
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
Document : enunciate.xml
|
||||
Created on : October 2, 2011, 12:02 PM
|
||||
Author : Sebastian Sdorra
|
||||
Description: Enunciate configuration
|
||||
-->
|
||||
|
||||
<enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.9.0.xsd"
|
||||
slug="scm-manager" version="${project.version}">
|
||||
|
||||
<title>SCM-Manager API</title>
|
||||
|
||||
<description>
|
||||
<![CDATA[
|
||||
<h1>SCM-Manager API</h1>
|
||||
<p>This page describes the RESTful Web Service API of <a href="https://www.scm-manager.org">SCM-Manager</a> ${project.version}.</p>
|
||||
]]>
|
||||
</description>
|
||||
|
||||
<api-classes/>
|
||||
|
||||
<modules>
|
||||
|
||||
<jaxrs datatype-detection="local">
|
||||
<application path="/api" />
|
||||
</jaxrs>
|
||||
|
||||
<docs disableResourceLinks="true" includeApplicationPath="true" />
|
||||
|
||||
</modules>
|
||||
|
||||
</enunciate>
|
||||
15
scm-webapp/src/main/doc/openapi.md
Normal file
15
scm-webapp/src/main/doc/openapi.md
Normal file
@@ -0,0 +1,15 @@
|
||||
The following REST documentation describes all public endpoints of your SCM-Manager instance.
|
||||
You can try the endpoints with or without authentication right on the swagger surface provided by the OpenAPI-Plugin.
|
||||
|
||||
For authenticated requests please login to the SCM-Manager. You can also use the "Authorize" button and insert your preferred authentication method.
|
||||
For basic authentication simply use your SCM-Manager credentials. If you want to use the bearer token authentication, you can generate an
|
||||
valid token using the authentication endpoint and copy the response body.
|
||||
|
||||
SCM-Manager defines a modern ["Level 3"-REST API](https://martinfowler.com/articles/richardsonMaturityModel.html).
|
||||
Using the HATEOAS architecture for REST allows us to provide discoverable and self explanatory endpoint definitions.
|
||||
The responses are build using the [HAL JSON format](http://stateless.co/hal_specification.html).
|
||||
HAL makes the API human-friendly and simplifies the communication between the frontend and the server using links and embedded resources.
|
||||
|
||||
We highly suggest using HAL links when creating new functions for SCM-Manager since they are consistent and are only
|
||||
appended to the response when user has the necessary permissions. The links and embedded resources can also be used by plugins, which can
|
||||
define new resources or enrich existing ones.
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* <p>
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* <p>
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 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 list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
* <p>
|
||||
* 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
|
||||
@@ -24,13 +24,11 @@
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.api.rest.resources;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
@@ -38,10 +36,6 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.FeatureNotSupportedException;
|
||||
@@ -100,8 +94,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
// @Path("import/repositories")
|
||||
public class RepositoryImportResource
|
||||
{
|
||||
public class RepositoryImportResource {
|
||||
|
||||
/**
|
||||
* the logger for RepositoryImportResource
|
||||
@@ -114,13 +107,12 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Constructs a new repository import resource.
|
||||
*
|
||||
* @param manager repository manager
|
||||
* @param manager repository manager
|
||||
* @param serviceFactory
|
||||
*/
|
||||
@Inject
|
||||
public RepositoryImportResource(RepositoryManager manager,
|
||||
RepositoryServiceFactory serviceFactory)
|
||||
{
|
||||
RepositoryServiceFactory serviceFactory) {
|
||||
this.manager = manager;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
@@ -133,37 +125,23 @@ public class RepositoryImportResource
|
||||
* bundle file is passed to the {@link UnbundleCommandBuilder}. <strong>Note:</strong> This method
|
||||
* requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream input bundle
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
*
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return empty response with location header which points to the imported repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the imported repository")
|
||||
}),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response importFromBundle(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, @FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed)
|
||||
{
|
||||
@PathParam("type") String type, @FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed) {
|
||||
Repository repository = doImportFromBundle(type, name, inputStream,
|
||||
compressed);
|
||||
compressed);
|
||||
|
||||
return buildResponse(uriInfo, repository);
|
||||
}
|
||||
@@ -175,43 +153,28 @@ public class RepositoryImportResource
|
||||
* workaround of the javascript ui extjs. <strong>Note:</strong> This method requires admin
|
||||
* privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream input bundle
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
*
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle.html")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(RestActionUploadResult.class)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response importFromBundleUI(@PathParam("type") String type,
|
||||
@FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed)
|
||||
{
|
||||
@FormParam("name") String name,
|
||||
@FormParam("bundle") InputStream inputStream, @QueryParam("compressed")
|
||||
@DefaultValue("false") boolean compressed) {
|
||||
Response response;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
doImportFromBundle(type, name, inputStream, compressed);
|
||||
response = Response.ok(new RestActionUploadResult(true)).build();
|
||||
}
|
||||
catch (WebApplicationException ex)
|
||||
{
|
||||
} catch (WebApplicationException ex) {
|
||||
logger.warn("error durring bundle import", ex);
|
||||
response = Response.fromResponse(ex.getResponse()).entity(
|
||||
new RestActionUploadResult(false)).build();
|
||||
@@ -227,31 +190,17 @@ public class RepositoryImportResource
|
||||
* repository. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param type repository type
|
||||
* @param request request object
|
||||
*
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/url")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the imported repository")
|
||||
}),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importFromUrl(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, UrlImportRequest request)
|
||||
{
|
||||
@PathParam("type") String type, UrlImportRequest request) {
|
||||
RepositoryPermissions.create().check();
|
||||
checkNotNull(request, "request is required");
|
||||
checkArgument(!Strings.isNullOrEmpty(request.getName()),
|
||||
@@ -268,17 +217,12 @@ public class RepositoryImportResource
|
||||
Repository repository = create(type, request.getName());
|
||||
RepositoryService service = null;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
service = serviceFactory.create(repository);
|
||||
service.getPullCommand().pull(request.getUrl());
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
} catch (IOException ex) {
|
||||
handleImportFailure(ex, repository);
|
||||
}
|
||||
finally
|
||||
{
|
||||
} finally {
|
||||
IOUtil.close(service);
|
||||
}
|
||||
|
||||
@@ -290,23 +234,12 @@ public class RepositoryImportResource
|
||||
* directory. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
*
|
||||
* @return imported repositories
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository[].class)
|
||||
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
public Response importRepositories(@PathParam("type") String type)
|
||||
{
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositories(@PathParam("type") String type) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
List<Repository> repositories = new ArrayList<Repository>();
|
||||
@@ -315,7 +248,8 @@ public class RepositoryImportResource
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Repository>>(repositories) {}
|
||||
new GenericEntity<List<Repository>>(repositories) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
@@ -327,32 +261,22 @@ public class RepositoryImportResource
|
||||
* @return imported repositories
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository[].class)
|
||||
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
public Response importRepositories()
|
||||
{
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositories() {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
logger.info("start directory import for all supported repository types");
|
||||
|
||||
List<Repository> repositories = new ArrayList<Repository>();
|
||||
|
||||
for (Type t : findImportableTypes())
|
||||
{
|
||||
for (Type t : findImportableTypes()) {
|
||||
importFromDirectory(repositories, t.getName());
|
||||
}
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Repository>>(repositories) {}
|
||||
new GenericEntity<List<Repository>>(repositories) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
@@ -363,72 +287,50 @@ public class RepositoryImportResource
|
||||
* of failed directories. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
*
|
||||
* @return imported repositories
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/directory")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(ImportResult.class)
|
||||
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response importRepositoriesFromDirectory(
|
||||
@PathParam("type") String type)
|
||||
{
|
||||
@PathParam("type") String type) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
Response response;
|
||||
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
if (handler != null) {
|
||||
logger.info("start directory import for repository type {}", type);
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
ImportResult result;
|
||||
ImportHandler importHandler = handler.getImportHandler();
|
||||
|
||||
if (importHandler instanceof AdvancedImportHandler)
|
||||
{
|
||||
if (importHandler instanceof AdvancedImportHandler) {
|
||||
logger.debug("start directory import, using advanced import handler");
|
||||
result =
|
||||
((AdvancedImportHandler) importHandler)
|
||||
.importRepositoriesFromDirectory(manager);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
logger.debug("start directory import, using normal import handler");
|
||||
result = new ImportResult(importHandler.importRepositories(manager),
|
||||
ImmutableList.<String>of());
|
||||
}
|
||||
|
||||
response = Response.ok(result).build();
|
||||
}
|
||||
catch (FeatureNotSupportedException ex)
|
||||
{
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
logger
|
||||
.warn(
|
||||
"import feature is not supported by repository handler for type "
|
||||
.concat(type), ex);
|
||||
response = Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
} catch (IOException ex) {
|
||||
logger.warn("exception occured durring directory import", ex);
|
||||
response = Response.serverError().build();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
logger.warn("could not find reposiotry handler for type {}", type);
|
||||
response = Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
@@ -445,25 +347,16 @@ public class RepositoryImportResource
|
||||
* @return list of repository types
|
||||
*/
|
||||
@GET
|
||||
@TypeHint(Type[].class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
public Response getImportableTypes()
|
||||
{
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||
public Response getImportableTypes() {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
List<Type> types = findImportableTypes();
|
||||
|
||||
//J-
|
||||
return Response.ok(
|
||||
new GenericEntity<List<Type>>(types) {}
|
||||
new GenericEntity<List<Type>>(types) {
|
||||
}
|
||||
).build();
|
||||
//J+
|
||||
}
|
||||
@@ -473,16 +366,13 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Build rest response for repository.
|
||||
*
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param uriInfo uri info
|
||||
* @param repository imported repository
|
||||
*
|
||||
* @return rest response
|
||||
*/
|
||||
private Response buildResponse(UriInfo uriInfo, Repository repository)
|
||||
{
|
||||
private Response buildResponse(UriInfo uriInfo, Repository repository) {
|
||||
URI location = uriInfo.getBaseUriBuilder().path(
|
||||
RepositoryResource.class).path(repository.getId()).build();
|
||||
RepositoryResource.class).path(repository.getId()).build();
|
||||
|
||||
return Response.created(location).build();
|
||||
}
|
||||
@@ -490,15 +380,12 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Check repository type for support for the given command.
|
||||
*
|
||||
*
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
* @param request request object
|
||||
*/
|
||||
private void checkSupport(Type type, Command cmd, Object request)
|
||||
{
|
||||
if (!(type instanceof RepositoryType))
|
||||
{
|
||||
private void checkSupport(Type type, Command cmd, Object request) {
|
||||
if (!(type instanceof RepositoryType)) {
|
||||
logger.warn("type {} is not a repository type", type.getName());
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
@@ -506,8 +393,7 @@ public class RepositoryImportResource
|
||||
|
||||
Set<Command> cmds = ((RepositoryType) type).getSupportedCommands();
|
||||
|
||||
if (!cmds.contains(cmd))
|
||||
{
|
||||
if (!cmds.contains(cmd)) {
|
||||
logger.warn("type {} does not support this type of import: {}",
|
||||
type.getName(), request);
|
||||
|
||||
@@ -518,24 +404,18 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Creates a new repository with the given name and type.
|
||||
*
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name repository name
|
||||
*
|
||||
* @return newly created repository
|
||||
*/
|
||||
private Repository create(String type, String name)
|
||||
{
|
||||
private Repository create(String type, String name) {
|
||||
Repository repository = null;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
// TODO #8783
|
||||
// repository = new Repository(null, type, name);
|
||||
manager.create(repository);
|
||||
}
|
||||
catch (InternalRepositoryException ex)
|
||||
{
|
||||
} catch (InternalRepositoryException ex) {
|
||||
handleGenericCreationFailure(ex, type, name);
|
||||
}
|
||||
|
||||
@@ -545,17 +425,14 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Start bundle import.
|
||||
*
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
* @param inputStream bundle stream
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
*
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
* @return imported repository
|
||||
*/
|
||||
private Repository doImportFromBundle(String type, String name,
|
||||
InputStream inputStream, boolean compressed)
|
||||
{
|
||||
InputStream inputStream, boolean compressed) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
checkArgument(!Strings.isNullOrEmpty(name),
|
||||
@@ -564,8 +441,7 @@ public class RepositoryImportResource
|
||||
|
||||
Repository repository;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
Type t = type(type);
|
||||
|
||||
checkSupport(t, Command.UNBUNDLE, "bundle");
|
||||
@@ -576,26 +452,19 @@ public class RepositoryImportResource
|
||||
|
||||
File file = File.createTempFile("scm-import-", ".bundle");
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
long length = Files.asByteSink(file).writeFrom(inputStream);
|
||||
|
||||
logger.info("copied {} bytes to temp, start bundle import", length);
|
||||
service = serviceFactory.create(repository);
|
||||
service.getUnbundleCommand().setCompressed(compressed).unbundle(file);
|
||||
}
|
||||
catch (InternalRepositoryException ex)
|
||||
{
|
||||
} catch (InternalRepositoryException ex) {
|
||||
handleImportFailure(ex, repository);
|
||||
}
|
||||
finally
|
||||
{
|
||||
} finally {
|
||||
IOUtil.close(service);
|
||||
IOUtil.delete(file);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
} catch (IOException ex) {
|
||||
logger.warn("could not create temporary file", ex);
|
||||
|
||||
throw new WebApplicationException(ex);
|
||||
@@ -607,42 +476,29 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<Type> findImportableTypes()
|
||||
{
|
||||
private List<Type> findImportableTypes() {
|
||||
List<Type> types = new ArrayList<Type>();
|
||||
Collection<Type> handlerTypes = manager.getTypes();
|
||||
|
||||
for (Type t : handlerTypes)
|
||||
{
|
||||
for (Type t : handlerTypes) {
|
||||
RepositoryHandler handler = manager.getHandler(t.getName());
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (handler.getImportHandler() != null)
|
||||
{
|
||||
if (handler != null) {
|
||||
try {
|
||||
if (handler.getImportHandler() != null) {
|
||||
types.add(t);
|
||||
}
|
||||
}
|
||||
catch (FeatureNotSupportedException ex)
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("import handler is not supported", ex);
|
||||
}
|
||||
else if (logger.isInfoEnabled())
|
||||
{
|
||||
} else if (logger.isInfoEnabled()) {
|
||||
logger.info("{} handler does not support import of repositories",
|
||||
t.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (logger.isWarnEnabled())
|
||||
{
|
||||
} else if (logger.isWarnEnabled()) {
|
||||
logger.warn("could not find handler for type {}", t.getName());
|
||||
}
|
||||
}
|
||||
@@ -653,14 +509,12 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Handle creation failures.
|
||||
*
|
||||
*
|
||||
* @param ex exception
|
||||
* @param ex exception
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
*/
|
||||
private void handleGenericCreationFailure(Exception ex, String type,
|
||||
String name)
|
||||
{
|
||||
String name) {
|
||||
logger.error(String.format("could not create repository %s with type %s",
|
||||
type, name), ex);
|
||||
|
||||
@@ -670,20 +524,15 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Handle import failures.
|
||||
*
|
||||
*
|
||||
* @param ex exception
|
||||
* @param ex exception
|
||||
* @param repository repository
|
||||
*/
|
||||
private void handleImportFailure(Exception ex, Repository repository)
|
||||
{
|
||||
private void handleImportFailure(Exception ex, Repository repository) {
|
||||
logger.error("import for repository failed, delete repository", ex);
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
manager.delete(repository);
|
||||
}
|
||||
catch (InternalRepositoryException | NotFoundException e)
|
||||
{
|
||||
} catch (InternalRepositoryException | NotFoundException e) {
|
||||
logger.error("can not delete repository after import failure", e);
|
||||
}
|
||||
|
||||
@@ -694,27 +543,21 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Import repositories from a specific type.
|
||||
*
|
||||
*
|
||||
* @param repositories repository list
|
||||
* @param type type of repository
|
||||
* @param type type of repository
|
||||
*/
|
||||
private void importFromDirectory(List<Repository> repositories, String type)
|
||||
{
|
||||
private void importFromDirectory(List<Repository> repositories, String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
if (handler != null) {
|
||||
logger.info("start directory import for repository type {}", type);
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
List<String> repositoryNames =
|
||||
handler.getImportHandler().importRepositories(manager);
|
||||
|
||||
if (repositoryNames != null)
|
||||
{
|
||||
for (String repositoryName : repositoryNames)
|
||||
{
|
||||
if (repositoryNames != null) {
|
||||
for (String repositoryName : repositoryNames) {
|
||||
// TODO #8783
|
||||
/*Repository repository = null; //manager.get(type, repositoryName);
|
||||
|
||||
@@ -729,22 +572,14 @@ public class RepositoryImportResource
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FeatureNotSupportedException ex)
|
||||
{
|
||||
} catch (FeatureNotSupportedException ex) {
|
||||
throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
} catch (IOException ex) {
|
||||
throw new WebApplicationException(ex);
|
||||
} catch (InternalRepositoryException ex) {
|
||||
throw new WebApplicationException(ex);
|
||||
}
|
||||
catch (InternalRepositoryException ex)
|
||||
{
|
||||
throw new WebApplicationException(ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -752,17 +587,13 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param type
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Type type(String type)
|
||||
{
|
||||
private Type type(String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler == null)
|
||||
{
|
||||
if (handler == null) {
|
||||
logger.warn("no handler for type {} found", type);
|
||||
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
@@ -778,24 +609,21 @@ public class RepositoryImportResource
|
||||
*/
|
||||
@XmlRootElement(name = "import")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class UrlImportRequest
|
||||
{
|
||||
public static class UrlImportRequest {
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
public UrlImportRequest() {}
|
||||
public UrlImportRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link UrlImportRequest}
|
||||
*
|
||||
*
|
||||
* @param name name of the repository
|
||||
* @param url external url of the repository
|
||||
* @param url external url of the repository
|
||||
*/
|
||||
public UrlImportRequest(String name, String url)
|
||||
{
|
||||
public UrlImportRequest(String name, String url) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
@@ -806,13 +634,12 @@ public class RepositoryImportResource
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
public String toString() {
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("url", url)
|
||||
.toString();
|
||||
.add("name", name)
|
||||
.add("url", url)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
@@ -821,40 +648,44 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Returns name of the repository.
|
||||
*
|
||||
*
|
||||
* @return name of the repository
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns external url of the repository.
|
||||
*
|
||||
*
|
||||
* @return external url of the repository
|
||||
*/
|
||||
public String getUrl()
|
||||
{
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** name of the repository */
|
||||
/**
|
||||
* name of the repository
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/** external url of the repository */
|
||||
/**
|
||||
* external url of the repository
|
||||
*/
|
||||
private String url;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** repository manager */
|
||||
/**
|
||||
* repository manager
|
||||
*/
|
||||
private final RepositoryManager manager;
|
||||
|
||||
/** repository service factory */
|
||||
/**
|
||||
* repository service factory
|
||||
*/
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,63 @@
|
||||
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.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.security.*;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilder;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.AllowAnonymousAccess;
|
||||
import sonia.scm.security.Scope;
|
||||
import sonia.scm.security.Tokens;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.BeanParam;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
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",
|
||||
description = "Authentication with a jwt bearer token",
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT",
|
||||
type = SecuritySchemeType.HTTP
|
||||
)
|
||||
})
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Authentication", description = "Authentication related endpoints")
|
||||
})
|
||||
@Path(AuthenticationResource.PATH)
|
||||
@AllowAnonymousAccess
|
||||
public class AuthenticationResource {
|
||||
@@ -34,21 +73,33 @@ public class AuthenticationResource {
|
||||
private LogoutRedirection logoutRedirection;
|
||||
|
||||
@Inject
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
||||
{
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) {
|
||||
this.tokenBuilderFactory = tokenBuilderFactory;
|
||||
this.cookieIssuer = cookieIssuer;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Login via Form",
|
||||
description = "Form-based authentication.",
|
||||
tags = "Authentication",
|
||||
hidden = true
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "204", description = "success without content")
|
||||
@ApiResponse(responseCode = "400", description = "bad request, required parameter is missing")
|
||||
@ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response authenticateViaForm(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
@@ -59,18 +110,41 @@ public class AuthenticationResource {
|
||||
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Login via JSON",
|
||||
description = "JSON-based authentication.",
|
||||
tags = "Authentication",
|
||||
requestBody = @RequestBody(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = AuthenticationRequestDto.class),
|
||||
examples = @ExampleObject(
|
||||
name = "Simple login",
|
||||
value = "{\n \"username\":\"scmadmin\",\n \"password\":\"scmadmin\",\n \"cookie\":false,\n \"grant_type\":\"password\"\n}",
|
||||
summary = "Authenticate with username and password"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "204", description = "success without content")
|
||||
@ApiResponse(responseCode = "400", description = "bad request, required parameter is missing")
|
||||
@ApiResponse(responseCode = "401", description = "unauthorized, the specified username or password is wrong")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response authenticateViaJSONBody(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
AuthenticationRequestDto authentication
|
||||
) {
|
||||
) {
|
||||
return authenticate(request, response, authentication);
|
||||
}
|
||||
|
||||
@@ -86,12 +160,11 @@ public class AuthenticationResource {
|
||||
Response res;
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
subject.login(Tokens.createAuthenticationToken(request, authentication.getUsername(), authentication.getPassword()));
|
||||
|
||||
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
|
||||
if ( authentication.getScope() != null ) {
|
||||
if (authentication.getScope() != null) {
|
||||
tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
|
||||
}
|
||||
|
||||
@@ -101,17 +174,12 @@ public class AuthenticationResource {
|
||||
cookieIssuer.authenticate(request, response, token);
|
||||
res = Response.noContent().build();
|
||||
} else {
|
||||
res = Response.ok( token.compact() ).build();
|
||||
res = Response.ok(token.compact()).build();
|
||||
}
|
||||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
{
|
||||
} catch (AuthenticationException ex) {
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
LOG.warn("authentication failed for user {}", authentication.getUsername());
|
||||
}
|
||||
|
||||
@@ -126,12 +194,10 @@ public class AuthenticationResource {
|
||||
@DELETE
|
||||
@Path("access_token")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
|
||||
{
|
||||
@Operation(summary = "Logout", description = "Removes the access token.", tags = "Authentication")
|
||||
@ApiResponse(responseCode = "204", description = "success")
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.logout();
|
||||
@@ -139,7 +205,6 @@ public class AuthenticationResource {
|
||||
// remove authentication cookie
|
||||
cookieIssuer.invalidate(request, response);
|
||||
|
||||
// TODO anonymous access ??
|
||||
if (logoutRedirection == null) {
|
||||
return Response.noContent().build();
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
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.ReducedModelObject;
|
||||
import sonia.scm.group.GroupDisplayManager;
|
||||
import sonia.scm.user.UserDisplayManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Size;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
@@ -18,7 +22,9 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Autocomplete", description = "Autocomplete related endpoints")
|
||||
})
|
||||
@Path(AutoCompleteResource.PATH)
|
||||
public class AutoCompleteResource {
|
||||
public static final String PATH = "v2/autocomplete/";
|
||||
@@ -43,13 +49,26 @@ public class AutoCompleteResource {
|
||||
@GET
|
||||
@Path("users")
|
||||
@Produces(VndMediaType.AUTOCOMPLETE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Search user", description = "Returns matching users.", tags = "Autocomplete")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.AUTOCOMPLETE,
|
||||
schema = @Schema(implementation = ReducedObjectModelDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "if the searched string contains less than 2 characters")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user:autocomplete\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public List<ReducedObjectModelDto> searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
|
||||
return map(userDisplayManager.autocomplete(filter));
|
||||
}
|
||||
@@ -57,13 +76,25 @@ public class AutoCompleteResource {
|
||||
@GET
|
||||
@Path("groups")
|
||||
@Produces(VndMediaType.AUTOCOMPLETE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Search groups", description = "Returns matching groups.", tags = "Autocomplete")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.AUTOCOMPLETE,
|
||||
schema = @Schema(implementation = ReducedObjectModelDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "400", description = "if the searched string contains less than 2 characters")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group:autocomplete\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public List<ReducedObjectModelDto> searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
|
||||
return map(groupDisplayManager.autocomplete(filter));
|
||||
}
|
||||
|
||||
@@ -1,8 +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.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
@@ -44,11 +46,29 @@ public class AvailablePluginResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(
|
||||
summary = "Find all available plugins",
|
||||
description = "Returns a collection of available plugins.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PLUGIN_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||
public Response getAvailablePlugins() {
|
||||
PluginPermissions.read().check();
|
||||
@@ -68,13 +88,37 @@ public class AvailablePluginResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("/{name}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(PluginDto.class)
|
||||
@Produces(VndMediaType.PLUGIN)
|
||||
@Operation(
|
||||
summary = "Find single available plugin",
|
||||
description = "Returns an available plugins.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PLUGIN,
|
||||
schema = @Schema(implementation = PluginDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no plugin with the specified name found",
|
||||
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 getAvailablePlugin(@PathParam("name") String name) {
|
||||
PluginPermissions.read().check();
|
||||
Optional<AvailablePlugin> plugin = pluginManager.getAvailable(name);
|
||||
@@ -87,15 +131,28 @@ public class AvailablePluginResource {
|
||||
|
||||
/**
|
||||
* Triggers plugin installation.
|
||||
*
|
||||
* @param name plugin name
|
||||
* @return HTTP Status.
|
||||
*/
|
||||
@POST
|
||||
@Path("/{name}/install")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Triggers plugin installation",
|
||||
description = "Put single plugin in installation queue. Plugin will be installed after restart.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response installPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) {
|
||||
PluginPermissions.manage().check();
|
||||
pluginManager.install(name, restartAfterInstallation);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
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.headers.Header;
|
||||
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.PageResult;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.Branches;
|
||||
@@ -69,15 +69,33 @@ public class BranchRootResource {
|
||||
@GET
|
||||
@Path("{branch}")
|
||||
@Produces(VndMediaType.BRANCH)
|
||||
@TypeHint(BranchDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the branch"),
|
||||
@ResponseCode(code = 404, condition = "not found, no branch with the specified name for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Get single branch", description = "Returns a branch for a repository.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.BRANCH,
|
||||
schema = @Schema(implementation = BranchDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "branches not supported for given repository")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the branch")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no branch with the specified name for the repository available or repository found",
|
||||
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("branch") String branchName) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
@@ -95,24 +113,42 @@ public class BranchRootResource {
|
||||
}
|
||||
}
|
||||
|
||||
@Path("{branch}/changesets/")
|
||||
@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 changesets available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Path("{branch}/changesets/")
|
||||
@Produces(VndMediaType.CHANGESET_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(summary = "Collection of changesets", description = "Returns a collection of changesets for specific branch.", 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 history(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("branch") String branchName,
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
if (!branchExists(branchName, repositoryService)){
|
||||
if (!branchExists(branchName, repositoryService)) {
|
||||
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
@@ -143,15 +179,25 @@ public class BranchRootResource {
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.BRANCH_REQUEST)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
|
||||
@Operation(summary = "Create branch", description = "Creates a new branch.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created branch"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"push\" privilege")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a branch with this name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response create(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@Valid BranchRequestDto branchRequest) throws IOException {
|
||||
@@ -195,15 +241,33 @@ public class BranchRootResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.BRANCH_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "List of branches", description = "Returns all branches for a repository.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.BRANCH_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "branches not supported for given repository")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"read repository\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository found for the given namespace and name",
|
||||
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) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
|
||||
@@ -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;
|
||||
@@ -44,15 +45,32 @@ public class ChangesetRootResource {
|
||||
|
||||
@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 = "Collection of changesets", description = "Returns a collection of changesets.", 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))) {
|
||||
@@ -77,16 +95,33 @@ 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}")
|
||||
@Produces(VndMediaType.CHANGESET)
|
||||
@Operation(summary = "Specific changeset", description = "Returns a specific changeset.", 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();
|
||||
|
||||
@@ -1,8 +1,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.Parameter;
|
||||
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.config.ScmConfiguration;
|
||||
import sonia.scm.repository.NamespaceStrategyValidator;
|
||||
@@ -21,6 +25,9 @@ import javax.ws.rs.core.Response;
|
||||
/**
|
||||
* RESTful Web Service Resource to manage the configuration.
|
||||
*/
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Instance configuration", description = "Global SCM-Manager instance configuration")
|
||||
})
|
||||
@Path(ConfigResource.CONFIG_PATH_V2)
|
||||
public class ConfigResource {
|
||||
|
||||
@@ -46,13 +53,25 @@ public class ConfigResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.CONFIG)
|
||||
@TypeHint(UserDto.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:global\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Instance configuration", description = "Returns the instance configuration.", tags = "Instance configuration")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.CONFIG,
|
||||
schema = @Schema(implementation = ConfigDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response get() {
|
||||
|
||||
// We do this permission check in Resource and not in ScmConfiguration, because it must be available for reading
|
||||
@@ -70,13 +89,18 @@ public class ConfigResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.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:global\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update instance configuration", description = "Modifies the instance configuration.", tags = "Instance configuration")
|
||||
@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\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response update(@Valid ConfigDto configDto) {
|
||||
|
||||
// This *could* be moved to ScmConfiguration or ScmConfigurationUtil classes.
|
||||
|
||||
@@ -2,8 +2,10 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.spotter.ContentType;
|
||||
import com.github.sdorra.spotter.ContentTypes;
|
||||
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.NotFoundException;
|
||||
@@ -11,6 +13,7 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -43,20 +46,30 @@ public class ContentResource {
|
||||
* recognized, this will be given in the header <code>Language</code>.
|
||||
*
|
||||
* @param namespace the namespace of the repository
|
||||
* @param name the name of the repository
|
||||
* @param revision the revision
|
||||
* @param path The path of the file
|
||||
*
|
||||
* @param name the name of the repository
|
||||
* @param revision the revision
|
||||
* @param path The path of the file
|
||||
*/
|
||||
@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 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 = "File content by revision", description = "Returns the content of a file for the given revision in the repository.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@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, @PathParam("revision") String revision, @PathParam("path") String path) {
|
||||
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
@@ -85,20 +98,34 @@ public class ContentResource {
|
||||
* the repository. The programming language will be given in the header <code>Language</code>.
|
||||
*
|
||||
* @param namespace the namespace of the repository
|
||||
* @param name the name of the repository
|
||||
* @param revision the revision
|
||||
* @param path The path of the file
|
||||
*
|
||||
* @param name the name of the repository
|
||||
* @param revision the revision
|
||||
* @param path The path of the file
|
||||
*/
|
||||
@HEAD
|
||||
@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 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 = "File metadata by revision",
|
||||
description = "Returns the content type and the programming language (if it can be detected) of a file for the given revision in the repository.",
|
||||
tags = "Repository"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@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 metadata(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Response.ResponseBuilder responseBuilder = Response.ok();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
@@ -44,23 +46,35 @@ public class DiffRootResource {
|
||||
* Get the repository diff of a revision
|
||||
*
|
||||
* @param namespace repository namespace
|
||||
* @param name repository name
|
||||
* @param revision the revision
|
||||
* @param name repository name
|
||||
* @param revision the revision
|
||||
* @return the dif of the revision
|
||||
* @throws NotFoundException if the repository is not found
|
||||
*/
|
||||
@GET
|
||||
@Path("{revision}")
|
||||
@Produces(VndMediaType.DIFF)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "Bad Request"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
|
||||
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ) throws IOException {
|
||||
@Operation(summary = "Diff by revision", description = "Get the repository diff of a revision.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "400", description = "bad request")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no revision with the specified param for the repository available or repository found",
|
||||
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("revision") String revision, @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format) throws IOException {
|
||||
HttpUtil.checkForCRLFInjection(revision);
|
||||
DiffFormat diffFormat = DiffFormat.valueOf(format);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
@@ -77,14 +91,33 @@ public class DiffRootResource {
|
||||
@GET
|
||||
@Path("{revision}/parsed")
|
||||
@Produces(VndMediaType.DIFF_PARSED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "Bad Request"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
|
||||
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Parsed diff by revision", description = "Get the parsed repository diff of a revision.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.DIFF_PARSED,
|
||||
schema = @Schema(implementation = DiffResultDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "bad request")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no revision with the specified param for the repository available or repository not found",
|
||||
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 DiffResultDto getParsed(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
|
||||
HttpUtil.checkForCRLFInjection(revision);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
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.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -10,6 +16,9 @@ import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Permissions", description = "Permission related endpoints")
|
||||
})
|
||||
@Path("v2/permissions")
|
||||
public class GlobalPermissionResource {
|
||||
|
||||
@@ -22,6 +31,25 @@ public class GlobalPermissionResource {
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.PERMISSION_COLLECTION)
|
||||
@Operation(summary = "List of permissions", description = "Returns all available permissions.", tags = "Permissions")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PERMISSION_COLLECTION,
|
||||
schema = @Schema(implementation = PermissionListDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the permissions")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
@Path("")
|
||||
public Response getAll() {
|
||||
String[] permissions = permissionAssigner.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
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.headers.Header;
|
||||
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.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.search.SearchRequest;
|
||||
@@ -25,9 +25,8 @@ import java.util.function.Predicate;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
|
||||
public class GroupCollectionResource {
|
||||
|
||||
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private final GroupDtoToGroupMapper dtoToGroupMapper;
|
||||
private final GroupCollectionToDtoMapper groupCollectionToDtoMapper;
|
||||
@@ -56,46 +55,70 @@ public class GroupCollectionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.GROUP_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "\"sortBy\" field unknown"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "List of groups", description = "Returns all groups for a given page number with a given page size.", tags = "Group")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.GROUP_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false")
|
||||
@QueryParam("desc") boolean desc,
|
||||
@DefaultValue("") @QueryParam("q") String search
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false")
|
||||
@QueryParam("desc") boolean desc,
|
||||
@DefaultValue("") @QueryParam("q") String search
|
||||
) {
|
||||
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
|
||||
pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group.
|
||||
*
|
||||
* @param group The group to be created.
|
||||
* @return A response with the link to the new group (if created successfully).
|
||||
*/
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.GROUP)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a group with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group"))
|
||||
@Operation(summary = "Create group", description = "Creates a new group.", tags = "Group")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created group"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a group with this name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response create(@Valid GroupDto group) {
|
||||
return adapter.create(group,
|
||||
() -> dtoToGroupMapper.map(group),
|
||||
g -> resourceLinks.group().self(g.getName()));
|
||||
() -> dtoToGroupMapper.map(group),
|
||||
g -> resourceLinks.group().self(g.getName()));
|
||||
}
|
||||
|
||||
private Predicate<Group> createSearchPredicate(String search) {
|
||||
|
||||
@@ -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 sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.security.PermissionPermissions;
|
||||
@@ -39,14 +40,31 @@ public class GroupPermissionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.PERMISSION_COLLECTION)
|
||||
@TypeHint(PermissionListDto.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 group"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Group permission", description = "Returns permissions for a group.", tags = {"Group", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PERMISSION_COLLECTION,
|
||||
schema = @Schema(implementation = PermissionListDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no group with the specified id/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 getPermissions(@PathParam("id") String id) {
|
||||
PermissionPermissions.read().check();
|
||||
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForGroup(id);
|
||||
@@ -62,15 +80,26 @@ public class GroupPermissionResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.PERMISSION_COLLECTION)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current group does not have the correct privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update Group permissions", description = "Sets permissions for a group. Overwrites all existing permissions.", tags = {"Group", "Permissions"})
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current group does not have the correct privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no group with the specified id/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 overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
|
||||
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
|
||||
.map(PermissionDescriptor::new)
|
||||
|
||||
@@ -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 sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -40,20 +41,37 @@ public class GroupResource {
|
||||
* <strong>Note:</strong> This method requires "group" privilege.
|
||||
*
|
||||
* @param id the id/name of the group
|
||||
*
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.GROUP)
|
||||
@TypeHint(GroupDto.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 group"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("id") String id){
|
||||
@Operation(summary = "Get single group", description = "Returns a group.", tags = "Group")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.GROUP,
|
||||
schema = @Schema(implementation = GroupDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no group with the specified id/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 get(@PathParam("id") String id) {
|
||||
return adapter.get(id, groupToGroupDtoMapper::map);
|
||||
}
|
||||
|
||||
@@ -63,17 +81,21 @@ public class GroupResource {
|
||||
* <strong>Note:</strong> This method requires "group" privilege.
|
||||
*
|
||||
* @param name the name of the group to delete.
|
||||
*
|
||||
*/
|
||||
@DELETE
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Delete group", description = "Deletes the group with the given id.", tags = "Group")
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the group")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response delete(@PathParam("id") String name) {
|
||||
return adapter.delete(name);
|
||||
}
|
||||
@@ -83,21 +105,32 @@ public class GroupResource {
|
||||
*
|
||||
* <strong>Note:</strong> This method requires "group" privilege.
|
||||
*
|
||||
* @param name name of the group to be modified
|
||||
* @param name name of the group to be modified
|
||||
* @param group group object to modify
|
||||
*/
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.GROUP)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of id/group name"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update group", description = "Modifies a group.", tags = "Group")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/group name")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"group\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no group with the specified id/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 update(@PathParam("id") String name, @Valid GroupDto group) {
|
||||
return adapter.update(name, existing -> dtoToGroupMapper.map(group));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
@@ -7,6 +10,9 @@ import javax.ws.rs.Path;
|
||||
/**
|
||||
* RESTful Web Service Resource to manage groups and their members.
|
||||
*/
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Group", description = "Group related endpoints")
|
||||
})
|
||||
@Path(GroupRootResource.GROUPS_PATH_V2)
|
||||
public class GroupRootResource {
|
||||
|
||||
@@ -17,7 +23,7 @@ public class GroupRootResource {
|
||||
|
||||
@Inject
|
||||
public GroupRootResource(Provider<GroupCollectionResource> groupCollectionResource,
|
||||
Provider<GroupResource> groupResource) {
|
||||
Provider<GroupResource> groupResource) {
|
||||
this.groupCollectionResource = groupCollectionResource;
|
||||
this.groupResource = groupResource;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,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 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.PageResult;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
@@ -81,17 +82,34 @@ public class IncomingRootResource {
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Path("{source}/{target}/changesets")
|
||||
@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 changesets available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Path("{source}/{target}/changesets")
|
||||
@Produces(VndMediaType.CHANGESET_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(summary = "Incoming changesets", description = "Get the incoming changesets from source to target.", 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 incomingChangesets(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("source") String source,
|
||||
@@ -117,18 +135,34 @@ public class IncomingRootResource {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Path("{source}/{target}/diff")
|
||||
@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 changesets available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Path("{source}/{target}/diff")
|
||||
@Produces(VndMediaType.DIFF)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(summary = "Incoming diff", description = "Get the incoming diff from source to target.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.DIFF,
|
||||
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 incomingDiff(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("source") String source,
|
||||
@@ -155,14 +189,32 @@ public class IncomingRootResource {
|
||||
@GET
|
||||
@Path("{source}/{target}/diff/parsed")
|
||||
@Produces(VndMediaType.DIFF_PARSED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "Bad Request"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"),
|
||||
@ResponseCode(code = 404, condition = "not found, source or target branch for the repository not available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Incoming parsed diff", description = "Get the incoming parsed diff from source to target.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.DIFF_PARSED,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "bad request")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the diff")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, source or target branch for the repository not available or repository not found",
|
||||
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 incomingDiffParsed(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("source") String source,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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.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.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import sonia.scm.security.AllowAnonymousAccess;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -9,6 +15,15 @@ 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")
|
||||
},
|
||||
tags = {
|
||||
@Tag(name = "Index", description = "SCM-Manager Index")
|
||||
}
|
||||
)
|
||||
@Path(IndexResource.INDEX_PATH_V2)
|
||||
@AllowAnonymousAccess
|
||||
public class IndexResource {
|
||||
@@ -24,7 +39,23 @@ public class IndexResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.INDEX)
|
||||
@TypeHint(IndexDto.class)
|
||||
@Operation(summary = "Get index", description = "Returns the index for the scm-manager instance.", tags = "Index")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.INDEX,
|
||||
schema = @Schema(implementation = IndexDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public IndexDto getIndex() {
|
||||
return indexDtoGenerator.generate();
|
||||
}
|
||||
|
||||
@@ -1,8 +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.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
@@ -43,12 +45,30 @@ public class InstalledPluginResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Find all installed plugins",
|
||||
description = "Returns a collection of installed plugins.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PLUGIN_COLLECTION,
|
||||
schema = @Schema(implementation = HalRepresentation.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response getInstalledPlugins() {
|
||||
PluginPermissions.read().check();
|
||||
List<InstalledPlugin> plugins = pluginManager.getInstalled();
|
||||
@@ -61,11 +81,22 @@ public class InstalledPluginResource {
|
||||
*/
|
||||
@POST
|
||||
@Path("/update")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(
|
||||
summary = "Update all installed plugins",
|
||||
description = "Updates all installed plugins to the latest compatible version.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response updateAll() {
|
||||
pluginManager.updateAll();
|
||||
return Response.ok().build();
|
||||
@@ -75,18 +106,41 @@ public class InstalledPluginResource {
|
||||
* Returns the installed plugin with the given id.
|
||||
*
|
||||
* @param name name of plugin
|
||||
*
|
||||
* @return installed plugin with specified id
|
||||
*/
|
||||
@GET
|
||||
@Path("/{name}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(PluginDto.class)
|
||||
@Produces(VndMediaType.PLUGIN)
|
||||
@Operation(
|
||||
summary = "Get installed plugin by name",
|
||||
description = "Returns the installed plugin with the given id",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PLUGIN,
|
||||
schema = @Schema(implementation = PluginDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, plugin by given id could not be found",
|
||||
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 getInstalledPlugin(@PathParam("name") String name) {
|
||||
PluginPermissions.read().check();
|
||||
Optional<InstalledPlugin> pluginDto = pluginManager.getInstalled(name);
|
||||
@@ -100,17 +154,29 @@ public class InstalledPluginResource {
|
||||
|
||||
/**
|
||||
* Triggers plugin uninstall.
|
||||
*
|
||||
* @param name plugin name
|
||||
* @return HTTP Status.
|
||||
*/
|
||||
@POST
|
||||
@Path("/{name}/uninstall")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Uninstall plugin",
|
||||
description = "Add plugin uninstall to pending queue. The plugin will be removed on restart.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response uninstallPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) {
|
||||
PluginPermissions.manage().check();
|
||||
pluginManager.uninstall(name, restartAfterInstallation);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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 +26,13 @@ import javax.ws.rs.core.UriInfo;
|
||||
/**
|
||||
* RESTful Web Service Resource to get currently logged in users.
|
||||
*/
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Current user", description = "Current user 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 +50,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 = "Current user")
|
||||
@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 +76,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 = "Current user")
|
||||
@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()),
|
||||
|
||||
@@ -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 sonia.scm.repository.Modifications;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
@@ -33,16 +34,27 @@ public class ModificationsRootResource {
|
||||
* file modifications are for example: Modified, Added or Removed.
|
||||
*/
|
||||
@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 modifications"),
|
||||
@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.MODIFICATIONS)
|
||||
@TypeHint(ModificationsDto.class)
|
||||
@Path("{revision}")
|
||||
@Produces(VndMediaType.MODIFICATIONS)
|
||||
@Operation(summary = "File modifications", description = "Get the file modifications related to a revision.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.MODIFICATIONS,
|
||||
schema = @Schema(implementation = ModificationsDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the modifications")
|
||||
@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")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Modifications modifications = repositoryService.getModificationsCommand()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -42,6 +43,7 @@ public class NamespaceStrategyResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.NAMESPACE_STRATEGIES)
|
||||
@Operation(summary = "List of namespace strategies", description = "Returns all available namespace strategies and the current selected.", tags = "Repository")
|
||||
public NamespaceStrategiesDto get(@Context UriInfo uriInfo) {
|
||||
String currentStrategy = strategyAsString(namespaceStrategyProvider.get());
|
||||
List<String> availableStrategies = collectStrategyNames();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
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.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
@@ -40,11 +42,30 @@ public class PendingPluginResource {
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Find all pending plugins",
|
||||
description = "Returns a collection of pending plugins.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PLUGIN_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response getPending() {
|
||||
List<AvailablePlugin> pending = pluginManager
|
||||
.getAvailable()
|
||||
@@ -71,7 +92,7 @@ public class PendingPluginResource {
|
||||
|
||||
if (
|
||||
PluginPermissions.manage().isPermitted() &&
|
||||
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
||||
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
||||
) {
|
||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
||||
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
|
||||
@@ -103,10 +124,22 @@ public class PendingPluginResource {
|
||||
|
||||
@POST
|
||||
@Path("/execute")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Execute pending",
|
||||
description = "Executes all pending plugin changes. The server will be restarted on this action.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response executePending() {
|
||||
pluginManager.executePendingAndRestart();
|
||||
return Response.ok().build();
|
||||
@@ -114,10 +147,22 @@ public class PendingPluginResource {
|
||||
|
||||
@POST
|
||||
@Path("/cancel")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(
|
||||
summary = "Cancel pending",
|
||||
description = "Cancels all pending plugin changes and clear the pending queue.",
|
||||
tags = "Plugin Management"
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response cancelPending() {
|
||||
pluginManager.cancelPending();
|
||||
return Response.ok().build();
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
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;
|
||||
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Plugin Management", description = "Plugin management related endpoints")
|
||||
})
|
||||
@Path("v2/plugins")
|
||||
public class PluginRootResource {
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
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.headers.Header;
|
||||
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.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryInitializer;
|
||||
@@ -63,13 +63,24 @@ public class RepositoryCollectionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_COLLECTION)
|
||||
@TypeHint(CollectionDto.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 \"repository\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "List of repositories", description = "Returns all repositories for a given page number with a given page size.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@@ -92,15 +103,25 @@ public class RepositoryCollectionResource {
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
|
||||
@Operation(summary = "Create repository", description = "Creates a new repository.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created repository"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a repository with this name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) {
|
||||
AtomicReference<Repository> reference = new AtomicReference<>();
|
||||
Response response = adapter.create(repository,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
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.headers.Header;
|
||||
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.AlreadyExistsException;
|
||||
import sonia.scm.NotFoundException;
|
||||
@@ -64,17 +65,30 @@ public class RepositoryPermissionRootResource {
|
||||
* @return a web response with the status code 201 and the url to GET the added permission
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "creates", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri of the created permission")
|
||||
}),
|
||||
@ResponseCode(code = 500, condition = "internal server error"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 409, condition = "conflict")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Create repository-specific permission", description = "Adds a new permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "creates",
|
||||
headers = @Header(name = "Location", description = "uri of the created permission")
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "409", description = "conflict")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryPermissionDto permission) {
|
||||
log.info("try to add new permission: {}", permission);
|
||||
Repository repository = load(namespace, name);
|
||||
@@ -95,14 +109,32 @@ public class RepositoryPermissionRootResource {
|
||||
* @throws NotFoundException if the repository does not exists
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "ok"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@TypeHint(RepositoryPermissionDto.class)
|
||||
@Path("{permission-name}")
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Get single repository-specific permission", description = "Get the searched permission with permission name related to a repository.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_PERMISSION,
|
||||
schema = @Schema(implementation = RepositoryPermissionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
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("permission-name") String permissionName) {
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
@@ -125,14 +157,32 @@ public class RepositoryPermissionRootResource {
|
||||
* @throws NotFoundException if the repository does not exists
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "ok"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@TypeHint(RepositoryPermissionDto.class)
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "List of repository-specific permissions", description = "Get all permissions related to a repository.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_PERMISSION,
|
||||
schema = @Schema(implementation = RepositoryPermissionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
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) {
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
@@ -148,14 +198,19 @@ public class RepositoryPermissionRootResource {
|
||||
* @return a web response with the status code 204
|
||||
*/
|
||||
@PUT
|
||||
@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.REPOSITORY_PERMISSION)
|
||||
@Path("{permission-name}")
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Update repository-specific permission", description = "Update a permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"})
|
||||
@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 update(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("permission-name") String permissionName,
|
||||
@@ -194,14 +249,19 @@ public class RepositoryPermissionRootResource {
|
||||
* @return a web response with the status code 204
|
||||
*/
|
||||
@DELETE
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Path("{permission-name}")
|
||||
@Operation(summary = "Delete repository-specific permission", description = "Delete a permission with the given name.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response delete(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("permission-name") String permissionName) {
|
||||
|
||||
@@ -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 sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
@@ -87,14 +88,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 = "Get 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);
|
||||
}
|
||||
@@ -110,13 +136,11 @@ public class RepositoryResource {
|
||||
*/
|
||||
@DELETE
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Delete repository", description = "Deletes the repository with the given namespace and name.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege")
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
return adapter.delete(loadBy(namespace, name));
|
||||
}
|
||||
@@ -133,15 +157,19 @@ public class RepositoryResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of namespace or name"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update repository", description = "Modifies the repository for the given namespace and name.", tags = "Repository")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of namespace or name")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repository\" privilege")
|
||||
@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")
|
||||
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repository) {
|
||||
return adapter.update(
|
||||
loadBy(namespace, name),
|
||||
@@ -168,7 +196,7 @@ public class RepositoryResource {
|
||||
}
|
||||
|
||||
@Path("branches/")
|
||||
public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
public BranchRootResource branches() {
|
||||
return branchRootResource.get();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
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.headers.Header;
|
||||
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.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -51,21 +51,32 @@ public class RepositoryRoleCollectionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_ROLE_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "\"sortBy\" field unknown"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current repositoryRole does not have the \"repositoryRole\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "List of repository roles", description = "Returns all repository roles for a given page number with a given page size.", tags = "Repository role")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_ROLE_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current repositoryRole does not have the \"repositoryRole\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false") @QueryParam("desc") boolean desc
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false") @QueryParam("desc") boolean desc
|
||||
) {
|
||||
return adapter.getAll(page, pageSize, x -> true, sortBy, desc,
|
||||
pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
pageResult -> repositoryRoleCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,15 +90,25 @@ public class RepositoryRoleCollectionResource {
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY_ROLE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository role with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repositoryRole"))
|
||||
@Operation(summary = "Create repository role", description = "Creates a new repository role.", tags = "Repository role")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created repository role"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a repository role with this name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response create(@Valid RepositoryRoleDto repositoryRole) {
|
||||
return adapter.create(repositoryRole, () -> dtoToRepositoryRoleMapper.map(repositoryRole), u -> resourceLinks.repositoryRole().self(u.getName()));
|
||||
}
|
||||
|
||||
@@ -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 sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -45,14 +46,31 @@ public class RepositoryRoleResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_ROLE)
|
||||
@TypeHint(RepositoryRoleDto.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 role"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Get single repository role", description = "Returns the repository role for the given name.", tags = "Repository role")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_ROLE,
|
||||
schema = @Schema(implementation = RepositoryRoleDto.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 role")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository role with the specified 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 get(@PathParam("name") String name) {
|
||||
return adapter.get(name, repositoryRoleToDtoMapper::map);
|
||||
}
|
||||
@@ -66,13 +84,11 @@ public class RepositoryRoleResource {
|
||||
*/
|
||||
@DELETE
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Delete repository role", description = "Deletes the repository role with the given name.", tags = "Repository role")
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege")
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response delete(@PathParam("name") String name) {
|
||||
return adapter.delete(name);
|
||||
}
|
||||
@@ -88,15 +104,19 @@ public class RepositoryRoleResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY_ROLE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of repository role name"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repositoryRole\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository role with the specified name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update repository role", description = "Modifies the repository role for the given name.", tags = "Repository role")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of repository role name")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"repositoryRole\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository role with the specified name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response update(@PathParam("name") String name, @Valid RepositoryRoleDto repositoryRole) {
|
||||
return adapter.update(name, existing -> dtoToRepositoryRoleMapper.map(repositoryRole));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
@@ -7,6 +10,9 @@ import javax.ws.rs.Path;
|
||||
/**
|
||||
* RESTful web service resource to manage repository roles.
|
||||
*/
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Repository role", description = "Repository role related endpoints")
|
||||
})
|
||||
@Path(RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2)
|
||||
public class RepositoryRoleRootResource {
|
||||
|
||||
|
||||
@@ -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/";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
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.repository.RepositoryManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -24,11 +26,25 @@ public class RepositoryTypeCollectionResource {
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.REPOSITORY_TYPE_COLLECTION)
|
||||
@Operation(summary = "List of repository types", description = "Returns all repository types.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_TYPE_COLLECTION,
|
||||
schema = @Schema(implementation = HalRepresentation.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public HalRepresentation getAll() {
|
||||
return mapper.map(repositoryManager.getConfiguredTypes());
|
||||
}
|
||||
|
||||
@@ -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 sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -35,12 +36,30 @@ public class RepositoryTypeResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_TYPE)
|
||||
@TypeHint(RepositoryTypeDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository type with the specified name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Get single repository type", description = "Returns the specified repository type for the given name.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_TYPE,
|
||||
schema = @Schema(implementation = RepositoryTypeDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no repository type with the specified 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 get(@PathParam("name") String name) {
|
||||
for (RepositoryType type : repositoryManager.getConfiguredTypes()) {
|
||||
if (name.equalsIgnoreCase(type.getName())) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import de.otto.edison.hal.Links;
|
||||
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.security.RepositoryPermissionProvider;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -30,11 +32,23 @@ public class RepositoryVerbResource {
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.REPOSITORY_VERB_COLLECTION)
|
||||
@Operation(summary = "List of repository verbs", description = "Returns all repository-specific permissions.", hidden = true)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_VERB_COLLECTION,
|
||||
schema = @Schema(implementation = RepositoryVerbsDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public RepositoryVerbsDto getAll() {
|
||||
return new RepositoryVerbsDto(
|
||||
Links.linkingTo().self(resourceLinks.repositoryVerbs().self()).build(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.BrowseCommandBuilder;
|
||||
@@ -32,22 +33,25 @@ public class SourceRootResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources", description = "Returns all sources for repository head.", tags = "Repository")
|
||||
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
|
||||
return getSource(namespace, name, "/", null);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("{revision}")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources by revision", description = "Returns all sources for the given revision.", tags = "Repository")
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException {
|
||||
return getSource(namespace, name, "/", revision);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("{revision}/{path: .*}")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources by revision in path", description = "Returns all sources for the given revision in a specific path.", tags = "Repository")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws IOException {
|
||||
return getSource(namespace, name, path, revision);
|
||||
}
|
||||
|
||||
@@ -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 sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -39,17 +40,27 @@ public class TagRootResource {
|
||||
this.tagToTagDtoMapper = tagToTagDtoMapper;
|
||||
}
|
||||
|
||||
|
||||
@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 tags"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.TAG_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Operation(summary = "List of tags", description = "Returns the tags for the given namespace and name.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.TAG_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 tags")
|
||||
@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) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Tags tags = getTags(repositoryService);
|
||||
@@ -65,16 +76,33 @@ public class TagRootResource {
|
||||
|
||||
|
||||
@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 tags"),
|
||||
@ResponseCode(code = 404, condition = "not found, no tag available in the repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.TAG)
|
||||
@TypeHint(TagDto.class)
|
||||
@Path("{tagName}")
|
||||
@Produces(VndMediaType.TAG)
|
||||
@Operation(summary = "Get tag", description = "Returns the tag for the given name.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.TAG,
|
||||
schema = @Schema(implementation = TagDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no tag with the specified name 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("tagName") String tagName) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
|
||||
@@ -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 sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.security.AllowAnonymousAccess;
|
||||
@@ -39,12 +40,23 @@ public class UIPluginResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(CollectionDto.class)
|
||||
@Produces(VndMediaType.UI_PLUGIN_COLLECTION)
|
||||
@Operation(summary = "Collection of ui plugin bundles", description = "Returns a collection of installed plugins and their ui bundles.", hidden = true)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.UI_PLUGIN_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response getInstalledPlugins() {
|
||||
List<InstalledPlugin> plugins = pluginLoader.getInstalledPlugins()
|
||||
.stream()
|
||||
@@ -63,13 +75,30 @@ public class UIPluginResource {
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(UIPluginDto.class)
|
||||
@Produces(VndMediaType.UI_PLUGIN)
|
||||
@Operation(summary = "Get single ui plugin bundle", description = "Returns the installed plugin with the given id.", hidden = true)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.UI_PLUGIN,
|
||||
schema = @Schema(implementation = UIPluginDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
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 getInstalledPlugin(@PathParam("id") String id) {
|
||||
Optional<UIPluginDto> uiPluginDto = pluginLoader.getInstalledPlugins()
|
||||
.stream()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
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.headers.Header;
|
||||
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.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.search.SearchRequest;
|
||||
import sonia.scm.search.SearchUtil;
|
||||
@@ -59,22 +59,33 @@ public class UserCollectionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.USER_COLLECTION)
|
||||
@TypeHint(CollectionDto.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "\"sortBy\" field unknown"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "List of users", description = "Returns all users for a given page number with a given page size.", tags = "User")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.USER_COLLECTION,
|
||||
schema = @Schema(implementation = CollectionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "400", description = "\"sortBy\" field unknown")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false") @QueryParam("desc") boolean desc,
|
||||
@DefaultValue("") @QueryParam("q") String search
|
||||
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
|
||||
@QueryParam("sortBy") String sortBy,
|
||||
@DefaultValue("false") @QueryParam("desc") boolean desc,
|
||||
@DefaultValue("") @QueryParam("q") String search
|
||||
) {
|
||||
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
|
||||
pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,15 +99,25 @@ public class UserCollectionResource {
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.USER)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
|
||||
@Operation(summary = "Create user", description = "Creates a new user.", tags = "User")
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "create success",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri to the created user"
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
|
||||
@ApiResponse(responseCode = "409", description = "conflict, a user with this name already exists")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response create(@Valid UserDto user) {
|
||||
return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName()));
|
||||
}
|
||||
|
||||
@@ -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 sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.security.PermissionPermissions;
|
||||
@@ -40,14 +41,32 @@ public class UserPermissionResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.PERMISSION_COLLECTION)
|
||||
@TypeHint(PermissionListDto.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 user"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "User permission", description = "Returns the global git configuration.", tags = {"User", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.PERMISSION_COLLECTION,
|
||||
schema = @Schema(implementation = PermissionListDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the user")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no user with the specified id/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 getPermissions(@PathParam("id") String id) {
|
||||
PermissionPermissions.read().check();
|
||||
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForUser(id);
|
||||
@@ -63,15 +82,26 @@ public class UserPermissionResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.PERMISSION_COLLECTION)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update user permissions", description = "Sets permissions for a user. Overwrites all existing permissions.", tags = {"User", "Permissions"})
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the correct privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no user with the specified id/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 overwritePermissions(@PathParam("id") String id, @Valid PermissionListDto newPermissions) {
|
||||
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
|
||||
.map(PermissionDescriptor::new)
|
||||
|
||||
@@ -1,12 +1,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.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.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -54,14 +54,31 @@ public class UserResource {
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.USER)
|
||||
@TypeHint(UserDto.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 user"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Operation(summary = "Get single user", description = "Returns the user for the given id.", tags = "User")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.USER,
|
||||
schema = @Schema(implementation = UserDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the user")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no user with the specified id/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 get(@PathParam("id") String id) {
|
||||
return adapter.get(id, userToDtoMapper::map);
|
||||
}
|
||||
@@ -75,13 +92,11 @@ public class UserResource {
|
||||
*/
|
||||
@DELETE
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Delete user", description = "Deletes the user with the given id.", tags = "User")
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response delete(@PathParam("id") String name) {
|
||||
return adapter.delete(name);
|
||||
}
|
||||
@@ -98,15 +113,19 @@ public class UserResource {
|
||||
@PUT
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.USER)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of id/user name"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Update user", description = "Modifies the user for the given id.", tags = "User")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body, e.g. illegal change of id/user name")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no user with the specified id/name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response update(@PathParam("id") String name, @Valid UserDto user) {
|
||||
return adapter.update(name, existing -> dtoToUserMapper.map(user, existing.getPassword()));
|
||||
}
|
||||
@@ -125,15 +144,19 @@ public class UserResource {
|
||||
@PUT
|
||||
@Path("password")
|
||||
@Consumes(VndMediaType.PASSWORD_OVERWRITE)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Operation(summary = "Modifies a user password", description = "Lets admins modifies the user password for the given id.", tags = "User")
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "400", description = "invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found, no user with the specified id/name available",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwrite) {
|
||||
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword()));
|
||||
return Response.noContent().build();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
@@ -7,6 +10,9 @@ import javax.ws.rs.Path;
|
||||
/**
|
||||
* RESTful Web Service Resource to manage users.
|
||||
*/
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "User", description = "User related endpoints")
|
||||
})
|
||||
@Path(UserRootResource.USERS_PATH_V2)
|
||||
public class UserRootResource {
|
||||
|
||||
|
||||
@@ -199,6 +199,10 @@
|
||||
"8LRncum0S1": {
|
||||
"displayName": "Interner Fehler im Repository",
|
||||
"description": "Bei der Bearbeitung des internen Repositories ist ein Fehler oder ein unerwarteter Zustand aufgetreten. Bitte prüfen Sie die Logs für weitere Informationen."
|
||||
},
|
||||
"4GRrgkSC01": {
|
||||
"displayName": "Unerwartetes Merge-Ergebnis",
|
||||
"description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -199,6 +199,10 @@
|
||||
"8LRncum0S1": {
|
||||
"displayName": "Internal repository error",
|
||||
"description": "There was an error or an unexpected condition while handling the native repository. Please consult the logs for further information."
|
||||
},
|
||||
"4GRrgkSC01": {
|
||||
"displayName": "Unexpected merge result",
|
||||
"description": "The merge led to an unexpected result, that could not be handled automatically. More details could be found in the log. Please merge the branches manually."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
Reference in New Issue
Block a user