From 1ebbd816e5b7710b4787e9488c094dae8d8e2094 Mon Sep 17 00:00:00 2001 From: MiniDigger | Martin Date: Mon, 23 Jan 2023 23:55:39 +0100 Subject: [PATCH] feat(front+backend): various swagger improvements --- .../papermc/hangar/config/SwaggerConfig.java | 70 +++++++++++-------- .../api/v1/interfaces/IApiKeysController.java | 6 +- .../interfaces/IAuthenticationController.java | 2 - .../v1/interfaces/IPermissionsController.java | 6 +- .../v1/interfaces/IProjectsController.java | 12 ++-- .../api/v1/interfaces/IUsersController.java | 16 ++--- .../v1/interfaces/IVersionsController.java | 12 ++-- .../src/pages/[user]/settings/api-keys.vue | 2 +- frontend/src/pages/api-docs.vue | 6 +- 9 files changed, 70 insertions(+), 62 deletions(-) diff --git a/backend/src/main/java/io/papermc/hangar/config/SwaggerConfig.java b/backend/src/main/java/io/papermc/hangar/config/SwaggerConfig.java index 2c7677059..ca98966fd 100644 --- a/backend/src/main/java/io/papermc/hangar/config/SwaggerConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/SwaggerConfig.java @@ -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.
- 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.
+ Note that all routes **not** listed here should be considered **internal**, and can change at a moment's notice. **Do not use them**. -

Authentication and Authorization

- 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. -

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. + ### 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. -

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.

+ ### 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!** -

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.

+ #### 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. -

Misc

-

Date Formats

- 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). -

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.

+ ### 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())); } } diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IApiKeysController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IApiKeysController.java index ae47fbb85..45e860680 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IApiKeysController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IApiKeysController.java @@ -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({ diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IAuthenticationController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IAuthenticationController.java index 2e5e6162b..f974acb54 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IAuthenticationController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IAuthenticationController.java @@ -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({ diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IPermissionsController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IPermissionsController.java index 08c133bb3..6ab0a61b5 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IPermissionsController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IPermissionsController.java @@ -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({ diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IProjectsController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IProjectsController.java index aeeb42f5e..78e340140 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IProjectsController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IProjectsController.java @@ -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({ diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java index 373cfcdf6..0192bcbc9 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java @@ -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({ diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IVersionsController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IVersionsController.java index 4e431678c..66f3c9fd5 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IVersionsController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IVersionsController.java @@ -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({ diff --git a/frontend/src/pages/[user]/settings/api-keys.vue b/frontend/src/pages/[user]/settings/api-keys.vue index 40da96f31..3bb1fc6d2 100644 --- a/frontend/src/pages/[user]/settings/api-keys.vue +++ b/frontend/src/pages/[user]/settings/api-keys.vue @@ -94,7 +94,7 @@ async function deleteKey(key: ApiKey) { {{ i18n.t("apiKeys.createKey") }} - +
diff --git a/frontend/src/pages/api-docs.vue b/frontend/src/pages/api-docs.vue index 1ee6e832a..a6fbb5d83 100644 --- a/frontend/src/pages/api-docs.vue +++ b/frontend/src/pages/api-docs.vue @@ -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; }