feat(front+backend): various swagger improvements

This commit is contained in:
MiniDigger | Martin 2023-01-23 23:55:39 +01:00
parent 52eaf1e56b
commit 1ebbd816e5
9 changed files with 70 additions and 62 deletions

View File

@ -1,11 +1,14 @@
package io.papermc.hangar.config;
import io.papermc.hangar.controller.extras.pagination.FilterRegistry;
import io.papermc.hangar.controller.extras.pagination.SorterRegistry;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableSorters;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
@ -21,40 +24,40 @@ public class SwaggerConfig {
@Bean
OpenApiCustomizer apiInfo() {
return (openApi) -> openApi
.info(new Info().title("Hangar API")
.description("""
This page describes the format for the current Hangar REST API as well as general usage guidelines.<br>
Note that all routes **not** listed here should be considered **internal**, and can change at a moment's notice. **Do not use them**.
return openApi -> {
openApi.info(new Info().title("Hangar API").version("1.0").description("""
This page describes the format for the current Hangar REST API as well as general usage guidelines.<br>
Note that all routes **not** listed here should be considered **internal**, and can change at a moment's notice. **Do not use them**.
<h2>Authentication and Authorization</h2>
There are two ways to consume the API: Authenticated or anonymous.
## Authentication and Authorization
There are two ways to consume the API: Authenticated or anonymous.
<h3>Anonymous</h3>
When using anonymous authentication, you only have access to public information, but you don't need to worry about creating and storing an API key or handing JWTs.
### Anonymous
When using anonymous authentication, you only have access to public information, but you don't need to worry about creating and storing an API key or handing JWTs.
<h3>Authenticated</h3>
If you need access to non-public content or actions, you need to create and use API keys.
These can be created by going to the API keys page via the profile dropdown or by going to your user page and clicking on the key icon.<br><br>
### Authenticated
If you need access to non-public content or actions, you need to create and use API keys.
These can be created by going to the API keys page via the profile dropdown or by going to your user page and clicking on the key icon.
API keys allow you to impersonate yourself, so they should be handled like passwords. **Do not share them with anyone else!**
API keys allow you to impersonate yourself, so they should be handled like passwords. **Do not share them with anyone else!**
<h4>Getting and Using a JWT</h4>
Once you have an API key, you need to authenticate yourself: Send a `POST` request with your API key identifier (a UUID) to `/api/v1/authenticate?apiKey=yourKey`. The response will contain your JWT as well as an expiration time.
Put this JWT into the `Authentication` header of every request and make sure to request a new JWT after the expiration time has passed.<br><br>
#### Getting and Using a JWT
Once you have an API key, you need to authenticate yourself: Send a `POST` request with your API key identifier (a UUID) to `/api/v1/authenticate?apiKey=yourKey`. The response will contain your JWT as well as an expiration time.
Put this JWT into the `Authentication` header of every request and make sure to request a new JWT after the expiration time has passed.
Please also set a meaningful `User-Agent` header. This allows us to better identify loads and needs for potentially new endpoints.
Please also set a meaningful `User-Agent` header. This allows us to better identify loads and needs for potentially new endpoints.
<h2>Misc</h2>
<h3>Date Formats</h3>
Standard ISO types. Where possible, we use the [OpenAPI format modifier](https://swagger.io/docs/specification/data-models/data-types/#format).
## Misc
### Date Formats
Standard ISO types. Where possible, we use the [OpenAPI format modifier](https://swagger.io/docs/specification/data-models/data-types/#format).
<h3>Rate Limits and Caching</h3>
The default rate limit is set at 20 requests every 5 seconds with an initial overdraft for extra leniency.
Individual endpoints, such as version creation, may have stricter rate limiting.<br><br>
### Rate Limits and Caching
The default rate limit is set at 20 requests every 5 seconds with an initial overdraft for extra leniency.
Individual endpoints, such as version creation, may have stricter rate limiting.
If applicable, always cache responses. The Hangar API itself is cached by CloudFlare and internally.""")
.version("1.0"));
If applicable, always cache responses. The Hangar API itself is cached by CloudFlare and internally."""));
openApi.getComponents().addSecuritySchemes("HangarAuth", new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"));
};
}
// TODO fix
@ -79,12 +82,17 @@ public class SwaggerConfig {
public Operation customize(final Operation operation, final HandlerMethod handlerMethod) {
final ApplicableSorters sorters = handlerMethod.getMethodAnnotation(ApplicableSorters.class);
if (sorters != null) {
final StringSchema allowedValues = new StringSchema();
for (final SorterRegistry sorterRegistry : sorters.value()) {
allowedValues.addEnumItem(sorterRegistry.getName());
}
operation.addParametersItem(new Parameter()
.name("sort")
.in("query")
.description("Used to sort the result"));
// TODO fix
//.query(q -> q.style(ParameterStyle.SIMPLE).model(m -> m.scalarModel(ScalarType.STRING)).enumerationFacet(e -> e.allowedValues(Arrays.asList(sorters.value()).stream().map(SorterRegistry::getName).collect(Collectors.toSet())))).build());
.description("Used to sort the result")
.style(Parameter.StyleEnum.SIMPLE)
.schema(allowedValues)
);
}
final ApplicableFilters filters = handlerMethod.getMethodAnnotation(ApplicableFilters.class);
if (filters != null) {
@ -94,9 +102,9 @@ public class SwaggerConfig {
operation.addParametersItem(new Parameter()
.name(filter.getSingleQueryParam()) // TODO multi-param filters
.in("query")
.description(filter.getDescription()));
// TODO fix
// .query(q -> q.style(ParameterStyle.SIMPLE).model(m -> m.scalarModel(ScalarType.STRING))).build());
.description(filter.getDescription())
.style(Parameter.StyleEnum.SIMPLE)
.schema(new StringSchema()));
}
}

View File

@ -28,7 +28,7 @@ public interface IApiKeysController {
summary = "Creates an API key",
operationId = "createKey",
description = "Creates an API key. Requires the `edit_api_keys` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "edit_api_keys"),
tags = "API Keys"
)
@ApiResponses({
@ -42,7 +42,7 @@ public interface IApiKeysController {
summary = "Fetches a list of API Keys",
operationId = "getKeys",
description = "Fetches a list of API Keys. Requires the `edit_api_keys` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "edit_api_keys"),
tags = "API Keys"
)
@ApiResponses({
@ -56,7 +56,7 @@ public interface IApiKeysController {
summary = "Deletes an API key",
operationId = "deleteKey",
description = "Deletes an API key. Requires the `edit_api_keys` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "edit_api_keys"),
tags = "API Keys"
)
@ApiResponses({

View File

@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@ -21,7 +20,6 @@ public interface IAuthenticationController {
summary = "Creates an API JWT",
operationId = "authenticate",
description = "`Log-in` with your API key in order to be able to call other endpoints authenticated. The returned JWT should be specified as a header in all following requests: `Authorization: HangarAuth your.jwt`",
security = @SecurityRequirement(name = "Key"),
tags = "Authentication"
)
@ApiResponses({

View File

@ -25,7 +25,7 @@ public interface IPermissionsController {
summary = "Checks whether you have all the provided permissions",
operationId = "hasAll",
description = "Checks whether you have all the provided permissions in the given context",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth"),
tags = "Permissions"
)
@ApiResponses({
@ -44,7 +44,7 @@ public interface IPermissionsController {
summary = "Checks whether you have at least one of the provided permissions",
operationId = "hasAny",
description = "Checks whether you have at least one of the provided permissions in the given context",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth"),
tags = "Permissions"
)
@ApiResponses({
@ -63,7 +63,7 @@ public interface IPermissionsController {
summary = "Returns your permissions",
operationId = "showPermissions",
description = "Returns a list of permissions you have in the given context",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth"),
tags = "Permissions"
)
@ApiResponses({

View File

@ -31,7 +31,7 @@ public interface IProjectsController {
summary = "Returns info on a specific project",
operationId = "getProject",
description = "Returns info on a specific project. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Projects"
)
@ApiResponses({
@ -47,7 +47,7 @@ public interface IProjectsController {
summary = "Returns the members of a project",
operationId = "getProjectMembers",
description = "Returns the members of a project. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Projects"
)
@ApiResponses({
@ -66,7 +66,7 @@ public interface IProjectsController {
summary = "Searches the projects on Hangar",
operationId = "getProjects",
description = "Searches all the projects on Hangar, or for a single user. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Projects"
)
@ApiResponses({
@ -85,7 +85,7 @@ public interface IProjectsController {
summary = "Returns the stats for a project",
operationId = "showProjectStats",
description = "Returns the stats (downloads and views) for a project per day for a certain date range. Requires the `is_subject_member` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "is_subject_member"),
tags = "Projects"
)
@ApiResponses({
@ -104,7 +104,7 @@ public interface IProjectsController {
summary = "Returns the stargazers of a project",
operationId = "getProjectStargazers",
description = "Returns the stargazers of a project. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Projects"
)
@ApiResponses({
@ -123,7 +123,7 @@ public interface IProjectsController {
summary = "Returns the watchers of a project",
operationId = "getProjectWatchers",
description = "Returns the watchers of a project. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Projects"
)
@ApiResponses({

View File

@ -29,7 +29,7 @@ public interface IUsersController {
summary = "Returns a specific user",
operationId = "getUser",
description = "Returns a specific user. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -43,8 +43,8 @@ public interface IUsersController {
@Operation(
summary = "Searches for users",
operationId = "showUsers",
description = "Returns a list of users based on a search query",
security = @SecurityRequirement(name = "Session"),
description = "Returns a list of users based on a search query. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -60,7 +60,7 @@ public interface IUsersController {
summary = "Returns the starred projects for a specific user",
operationId = "showStarred",
description = "Returns the starred projects for a specific user. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -77,7 +77,7 @@ public interface IUsersController {
summary = "Returns the watched projects for a specific user",
operationId = "getUserWatching",
description = "Returns the watched projects for a specific user. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -94,7 +94,7 @@ public interface IUsersController {
summary = "Returns the pinned projects for a specific user",
operationId = "getUserPinnedProjects",
description = "Returns the pinned projects for a specific user. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -109,7 +109,7 @@ public interface IUsersController {
summary = "Returns all users with at least one public project",
operationId = "getAuthors",
description = "Returns all users that have at least one public project. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({
@ -124,7 +124,7 @@ public interface IUsersController {
summary = "Returns Hangar staff",
operationId = "getStaff",
description = "Returns Hanagr staff. Requires the `view_public_info` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Users"
)
@ApiResponses({

View File

@ -36,7 +36,7 @@ public interface IVersionsController {
summary ="Creates a new version",
operationId = "uploadVersion",
description = "Creates a new version for a project. Requires the `create_version` permission in the project or owning organization.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "create_version"),
tags = "Versions"
)
@ApiResponses({
@ -54,8 +54,8 @@ public interface IVersionsController {
summary = "Returns a specific version of a project",
operationId = "showVersion",
description = "Returns a specific version of a project. Requires the `view_public_info` permission in the project or owning organization.",
security = @SecurityRequirement(name = "Session"),
tags = "Version"
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Versions"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Ok"),
@ -71,7 +71,7 @@ public interface IVersionsController {
summary = "Returns all versions of a project",
operationId = "listVersions",
description = "Returns all versions of a project. Requires the `view_public_info` permission in the project or owning organization.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "view_public_info"),
tags = "Versions"
)
@ApiResponses({
@ -88,7 +88,7 @@ public interface IVersionsController {
summary = "Returns the stats for a version",
operationId = "showVersionStats",
description = "Returns the stats (downloads) for a version per day for a certain date range. Requires the `is_subject_member` permission.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth", scopes = "is_subject_member"),
tags = "Versions"
)
@ApiResponses({
@ -108,7 +108,7 @@ public interface IVersionsController {
summary = "Downloads a version",
operationId = "downloadVersion",
description = "Downloads the file for a specific platform of a version. Requires visibility of the project and version.",
security = @SecurityRequirement(name = "Session"),
security = @SecurityRequirement(name = "HangarAuth"),
tags = "Versions"
)
@ApiResponses({

View File

@ -94,7 +94,7 @@ async function deleteKey(key: ApiKey) {
{{ i18n.t("apiKeys.createKey") }}
</Button>
</div>
<InputGroup v-model="selectedPerms" :label="i18n.t('apiKeys.permissions')" class="w-full mt-2">
<InputGroup v-model="selectedPerms" :label="i18n.t('apiKeys.permissions')" class="w-full mt-2 text-xl font-bold" full-width>
<div class="grid autofix mt-2">
<InputCheckbox v-for="perm in possiblePerms" :key="perm" v-model="selectedPerms" :label="perm" :value="perm" />
</div>

View File

@ -37,6 +37,10 @@ onMounted(() => {
if (req.url.startsWith("http://localhost:8080")) {
req.url = req.url.replace("http://localhost:8080", "http://localhost:3333");
}
if (req.headers?.Authorization) {
req.headers.Authorization = req.headers?.Authorization.replace("Bearer", "HangarAuth")
}
console.log(req)
}
return req;
}
@ -86,12 +90,10 @@ useHead(useSeo(i18n.t("apiDocs.title"), "API Docs for the Hangar REST API", rout
}
.description h3 {
padding-top: 1rem;
font-size: 25px;
}
.description h4 {
padding-top: 0.5rem;
font-size: 20px;
}