diff --git a/OWNERS b/OWNERS index a707d8b421..5691d610b4 100644 --- a/OWNERS +++ b/OWNERS @@ -9,3 +9,4 @@ approvers: - ruibaby - guqing - JohnNiang +- LIlGG diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json index b605b3f4fb..bd88ac814e 100644 --- a/api-docs/openapi/v3_0/aggregated.json +++ b/api-docs/openapi/v3_0/aggregated.json @@ -14701,87 +14701,6 @@ ] } }, - "/apis/uc.api.content.halo.run/v1alpha1/attachments": { - "post": { - "description": "Create attachment for the given post.", - "operationId": "CreateAttachmentForPost", - "parameters": [ - { - "description": "Wait for permalink.", - "in": "query", - "name": "waitForPermalink", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/PostAttachmentRequest" - } - } - } - }, - "responses": { - "default": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/Attachment" - } - } - }, - "description": "default response" - } - }, - "tags": [ - "AttachmentV1alpha1Uc" - ] - } - }, - "/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url": { - "post": { - "description": "Upload attachment from the given URL.", - "operationId": "ExternalTransferAttachment_1", - "parameters": [ - { - "description": "Wait for permalink.", - "in": "query", - "name": "waitForPermalink", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadFromUrlRequest" - } - } - }, - "required": true - }, - "responses": { - "default": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/Attachment" - } - } - }, - "description": "default response" - } - }, - "tags": [ - "AttachmentV1alpha1Uc" - ] - } - }, "/apis/uc.api.content.halo.run/v1alpha1/posts": { "get": { "description": "List posts owned by the current user.", @@ -15549,6 +15468,217 @@ "PersonalAccessTokenV1alpha1Uc" ] } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments": { + "get": { + "description": "List attachments of the current user uploaded.", + "operationId": "ListMyAttachments", + "parameters": [ + { + "description": "Page number. Default is 0.", + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "description": "Size number. Default is 0.", + "in": "query", + "name": "size", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "description": "Label selector. e.g.: hidden!\u003dtrue", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Field selector. e.g.: metadata.name\u003d\u003dhalo", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "in": "query", + "name": "sort", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Filter attachments without group. This parameter will ignore group parameter.", + "in": "query", + "name": "ungrouped", + "schema": { + "type": "boolean" + } + }, + { + "description": "Keyword for searching.", + "in": "query", + "name": "keyword", + "schema": { + "type": "string" + } + }, + { + "description": "Acceptable media types.", + "in": "query", + "name": "accepts", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AttachmentList" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + }, + "post": { + "description": "Create attachment for the given post.", + "operationId": "CreateAttachmentForPost", + "parameters": [ + { + "description": "Wait for permalink.", + "in": "query", + "name": "waitForPermalink", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PostAttachmentRequest" + } + } + } + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload": { + "post": { + "description": "Upload attachment to user center storage.", + "operationId": "UploadUcAttachment", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UcUploadRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url": { + "post": { + "description": "Upload attachment from the given URL.", + "operationId": "ExternalTransferAttachment_1", + "parameters": [ + { + "description": "Wait for permalink.", + "in": "query", + "name": "waitForPermalink", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadFromUrlRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } } }, "components": { @@ -19719,6 +19849,9 @@ } } }, + "Part": { + "type": "object" + }, "PasswordRequest": { "required": [ "password" @@ -20558,12 +20691,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, @@ -22403,12 +22536,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, @@ -23426,6 +23559,39 @@ } } }, + "UcUploadRequest": { + "required": [ + "file" + ], + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "formData": { + "type": "object", + "properties": { + "all": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Part" + }, + "writeOnly": true + }, + "empty": { + "type": "boolean" + } + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + } + } + }, "UpgradeFromUriRequest": { "required": [ "uri" diff --git a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json index bee0b0f624..df4144e8b8 100644 --- a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json +++ b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json @@ -5420,12 +5420,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, @@ -5949,12 +5949,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, diff --git a/api-docs/openapi/v3_0/apis_extension.api_v1alpha1.json b/api-docs/openapi/v3_0/apis_extension.api_v1alpha1.json index 90a36eaf33..2d9cecbfd2 100644 --- a/api-docs/openapi/v3_0/apis_extension.api_v1alpha1.json +++ b/api-docs/openapi/v3_0/apis_extension.api_v1alpha1.json @@ -11300,12 +11300,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, @@ -12484,12 +12484,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, diff --git a/api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json b/api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json index be52ece490..84820d4197 100644 --- a/api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json +++ b/api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json @@ -52,87 +52,6 @@ ] } }, - "/apis/uc.api.content.halo.run/v1alpha1/attachments": { - "post": { - "description": "Create attachment for the given post.", - "operationId": "CreateAttachmentForPost", - "parameters": [ - { - "description": "Wait for permalink.", - "in": "query", - "name": "waitForPermalink", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/PostAttachmentRequest" - } - } - } - }, - "responses": { - "default": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/Attachment" - } - } - }, - "description": "default response" - } - }, - "tags": [ - "AttachmentV1alpha1Uc" - ] - } - }, - "/apis/uc.api.content.halo.run/v1alpha1/attachments/-/upload-from-url": { - "post": { - "description": "Upload attachment from the given URL.", - "operationId": "ExternalTransferAttachment", - "parameters": [ - { - "description": "Wait for permalink.", - "in": "query", - "name": "waitForPermalink", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadFromUrlRequest" - } - } - }, - "required": true - }, - "responses": { - "default": { - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/Attachment" - } - } - }, - "description": "default response" - } - }, - "tags": [ - "AttachmentV1alpha1Uc" - ] - } - }, "/apis/uc.api.content.halo.run/v1alpha1/posts": { "get": { "description": "List posts owned by the current user.", @@ -900,6 +819,217 @@ "PersonalAccessTokenV1alpha1Uc" ] } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments": { + "get": { + "description": "List attachments of the current user uploaded.", + "operationId": "ListMyAttachments", + "parameters": [ + { + "description": "Page number. Default is 0.", + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "description": "Size number. Default is 0.", + "in": "query", + "name": "size", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "description": "Label selector. e.g.: hidden!\u003dtrue", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Field selector. e.g.: metadata.name\u003d\u003dhalo", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "in": "query", + "name": "sort", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Filter attachments without group. This parameter will ignore group parameter.", + "in": "query", + "name": "ungrouped", + "schema": { + "type": "boolean" + } + }, + { + "description": "Keyword for searching.", + "in": "query", + "name": "keyword", + "schema": { + "type": "string" + } + }, + { + "description": "Acceptable media types.", + "in": "query", + "name": "accepts", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AttachmentList" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + }, + "post": { + "description": "Create attachment for the given post.", + "operationId": "CreateAttachmentForPost", + "parameters": [ + { + "description": "Wait for permalink.", + "in": "query", + "name": "waitForPermalink", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PostAttachmentRequest" + } + } + } + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload": { + "post": { + "description": "Upload attachment to user center storage.", + "operationId": "UploadUcAttachment", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UcUploadRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } + }, + "/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url": { + "post": { + "description": "Upload attachment from the given URL.", + "operationId": "ExternalTransferAttachment", + "parameters": [ + { + "description": "Wait for permalink.", + "in": "query", + "name": "waitForPermalink", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadFromUrlRequest" + } + } + }, + "required": true + }, + "responses": { + "default": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Attachment" + } + } + }, + "description": "default response" + } + }, + "tags": [ + "AttachmentV1alpha1Uc" + ] + } } }, "components": { @@ -955,6 +1085,65 @@ } } }, + "AttachmentList": { + "required": [ + "first", + "hasNext", + "hasPrevious", + "items", + "last", + "page", + "size", + "total", + "totalPages" + ], + "type": "object", + "properties": { + "first": { + "type": "boolean", + "description": "Indicates whether current page is the first page." + }, + "hasNext": { + "type": "boolean", + "description": "Indicates whether current page has previous page." + }, + "hasPrevious": { + "type": "boolean", + "description": "Indicates whether current page has previous page." + }, + "items": { + "type": "array", + "description": "A chunk of items.", + "items": { + "$ref": "#/components/schemas/Attachment" + } + }, + "last": { + "type": "boolean", + "description": "Indicates whether current page is the last page." + }, + "page": { + "type": "integer", + "description": "Page number, starts from 1. If not set or equal to 0, it means no pagination.", + "format": "int32" + }, + "size": { + "type": "integer", + "description": "Size of each page. If not set or equal to 0, it means no pagination.", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "Total elements.", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "description": "Indicates total pages.", + "format": "int64" + } + } + }, "AttachmentSpec": { "type": "object", "properties": { @@ -1477,6 +1666,9 @@ } } }, + "Part": { + "type": "object" + }, "PasswordRequest": { "required": [ "password" @@ -1696,12 +1888,12 @@ }, "visible": { "type": "string", - "default": "PUBLIC", "enum": [ "PUBLIC", "INTERNAL", "PRIVATE" - ] + ], + "default": "PUBLIC" } } }, @@ -2054,6 +2246,39 @@ } } }, + "UcUploadRequest": { + "required": [ + "file" + ], + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "formData": { + "type": "object", + "properties": { + "all": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Part" + }, + "writeOnly": true + }, + "empty": { + "type": "boolean" + } + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + } + } + }, "UploadFromUrlRequest": { "required": [ "url" diff --git a/api/src/main/java/run/halo/app/infra/SystemSetting.java b/api/src/main/java/run/halo/app/infra/SystemSetting.java index 9beabb6c4f..8475cc76eb 100644 --- a/api/src/main/java/run/halo/app/infra/SystemSetting.java +++ b/api/src/main/java/run/halo/app/infra/SystemSetting.java @@ -71,6 +71,7 @@ public static class User { boolean mustVerifyEmailOnRegistration; String defaultRole; String avatarPolicy; + String ucAttachmentPolicy; } @Data diff --git a/application/build.gradle b/application/build.gradle index 2103485192..47c9d9168c 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -128,7 +128,7 @@ ext.presetPluginUrls = [ // Currently, plugin-app-store is not open source, so we need to download it from the official website. // Please see https://github.com/halo-dev/plugin-app-store/issues/55 // https://www.halo.run/store/apps/app-VYJbF - 'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-BpYuv/assets/app-release-BpYuv-LTHgb': 'appstore.jar', + 'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-CZSAd/assets/app-release-CZSAd-KZEqI': 'appstore.jar', ] tasks.register('downloadPluginPresets', Download) { diff --git a/application/src/main/java/run/halo/app/core/attachment/AttachmentLister.java b/application/src/main/java/run/halo/app/core/attachment/AttachmentLister.java new file mode 100644 index 0000000000..ed57380428 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/attachment/AttachmentLister.java @@ -0,0 +1,10 @@ +package run.halo.app.core.attachment; + +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.attachment.Attachment; +import run.halo.app.extension.ListResult; + +public interface AttachmentLister { + + Mono> listBy(SearchRequest searchRequest); +} diff --git a/application/src/main/java/run/halo/app/core/attachment/SearchRequest.java b/application/src/main/java/run/halo/app/core/attachment/SearchRequest.java new file mode 100644 index 0000000000..05ead06ba5 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/attachment/SearchRequest.java @@ -0,0 +1,110 @@ +package run.halo.app.core.attachment; + +import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance; +import static run.halo.app.extension.index.query.QueryFactory.contains; +import static run.halo.app.extension.index.query.QueryFactory.in; +import static run.halo.app.extension.index.query.QueryFactory.isNull; +import static run.halo.app.extension.index.query.QueryFactory.not; +import static run.halo.app.extension.index.query.QueryFactory.startsWith; + +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import java.util.List; +import java.util.Optional; +import org.springdoc.core.fn.builders.operation.Builder; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.server.ServerRequest; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.index.query.QueryFactory; +import run.halo.app.extension.router.IListRequest; +import run.halo.app.extension.router.QueryParamBuildUtil; +import run.halo.app.extension.router.SortableRequest; + +public class SearchRequest extends SortableRequest { + + public SearchRequest(ServerRequest request) { + super(request.exchange()); + } + + public Optional getKeyword() { + return Optional.ofNullable(queryParams.getFirst("keyword")) + .filter(StringUtils::hasText); + } + + public Optional getUngrouped() { + return Optional.ofNullable(queryParams.getFirst("ungrouped")) + .map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class)); + } + + public Optional> getAccepts() { + return Optional.ofNullable(queryParams.get("accepts")) + .filter(accepts -> !accepts.isEmpty() + && !accepts.contains("*") + && !accepts.contains("*/*") + ); + } + + public ListOptions toListOptions(List hiddenGroups) { + var builder = ListOptions.builder(super.toListOptions()); + + getKeyword().ifPresent(keyword -> { + builder.andQuery(contains("spec.displayName", keyword)); + }); + + getUngrouped() + .filter(ungrouped -> ungrouped) + .ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName"))); + + if (!CollectionUtils.isEmpty(hiddenGroups)) { + builder.andQuery(not(in("spec.groupName", hiddenGroups))); + } + + getAccepts().flatMap(accepts -> accepts.stream() + .filter(StringUtils::hasText) + .map(accept -> accept.replace("/*", "/").toLowerCase()) + .distinct() + .map(accept -> startsWith("spec.mediaType", accept)) + .reduce(QueryFactory::or) + ) + .ifPresent(builder::andQuery); + + return builder.build(); + } + + public static void buildParameters(Builder builder) { + IListRequest.buildParameters(builder); + builder.parameter(QueryParamBuildUtil.sortParameter()) + .parameter(parameterBuilder() + .in(ParameterIn.QUERY) + .name("ungrouped") + .required(false) + .description(""" + Filter attachments without group. This parameter will ignore group \ + parameter.\ + """) + .implementation(Boolean.class)) + .parameter(parameterBuilder() + .in(ParameterIn.QUERY) + .name("keyword") + .required(false) + .description("Keyword for searching.") + .implementation(String.class)) + .parameter(parameterBuilder() + .in(ParameterIn.QUERY) + .name("accepts") + .required(false) + .description("Acceptable media types.") + .array( + arraySchemaBuilder() + .uniqueItems(true) + .schema(schemaBuilder() + .implementation(String.class) + .example("image/*")) + ) + .implementationArray(String.class) + ); + } +} diff --git a/application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java b/application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java index a79e0f3268..6ecdd19e9a 100644 --- a/application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java +++ b/application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java @@ -2,37 +2,23 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; -import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; -import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; -import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; import static run.halo.app.extension.ListResult.generateGenericClass; -import static run.halo.app.extension.index.query.QueryFactory.contains; -import static run.halo.app.extension.index.query.QueryFactory.in; -import static run.halo.app.extension.index.query.QueryFactory.isNull; -import static run.halo.app.extension.index.query.QueryFactory.not; -import static run.halo.app.extension.index.query.QueryFactory.startsWith; -import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import java.net.URL; -import java.util.List; import java.util.Objects; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springdoc.core.fn.builders.operation.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; -import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.FormFieldPart; import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyExtractors; @@ -41,18 +27,11 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; +import run.halo.app.core.attachment.AttachmentLister; +import run.halo.app.core.attachment.SearchRequest; import run.halo.app.core.extension.attachment.Attachment; -import run.halo.app.core.extension.attachment.Group; import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.service.AttachmentService; -import run.halo.app.extension.ListOptions; -import run.halo.app.extension.PageRequestImpl; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.index.query.QueryFactory; -import run.halo.app.extension.router.IListRequest; -import run.halo.app.extension.router.QueryParamBuildUtil; -import run.halo.app.extension.router.SortableRequest; -import run.halo.app.extension.router.selector.LabelSelector; @Slf4j @Component @@ -61,7 +40,7 @@ public class AttachmentEndpoint implements CustomEndpoint { private final AttachmentService attachmentService; - private final ReactiveExtensionClient client; + private final AttachmentLister attachmentLister; @Override public RouterFunction endpoint() { @@ -131,112 +110,13 @@ public RouterFunction endpoint() { Mono search(ServerRequest request) { var searchRequest = new SearchRequest(request); - var groupListOptions = new ListOptions(); - groupListOptions.setLabelSelector(LabelSelector.builder() - .exists(Group.HIDDEN_LABEL) - .build()); - return client.listAll(Group.class, groupListOptions, Sort.unsorted()) - .map(group -> group.getMetadata().getName()) - .collectList() - .defaultIfEmpty(List.of()) - .flatMap(hiddenGroups -> client.listBy(Attachment.class, - searchRequest.toListOptions(hiddenGroups), - PageRequestImpl.of(searchRequest.getPage(), searchRequest.getSize(), - searchRequest.getSort()) - ) - .flatMap(listResult -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(listResult) - ) + return attachmentLister.listBy(searchRequest) + .flatMap(listResult -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listResult) ); } - public static class SearchRequest extends SortableRequest { - - public SearchRequest(ServerRequest request) { - super(request.exchange()); - } - - public Optional getKeyword() { - return Optional.ofNullable(queryParams.getFirst("keyword")) - .filter(StringUtils::hasText); - } - - public Optional getUngrouped() { - return Optional.ofNullable(queryParams.getFirst("ungrouped")) - .map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class)); - } - - public Optional> getAccepts() { - return Optional.ofNullable(queryParams.get("accepts")) - .filter(accepts -> !accepts.isEmpty() - && !accepts.contains("*") - && !accepts.contains("*/*") - ); - } - - public ListOptions toListOptions(List hiddenGroups) { - var builder = ListOptions.builder(super.toListOptions()); - - getKeyword().ifPresent(keyword -> { - builder.andQuery(contains("spec.displayName", keyword)); - }); - - getUngrouped() - .filter(ungrouped -> ungrouped) - .ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName"))); - - if (!CollectionUtils.isEmpty(hiddenGroups)) { - builder.andQuery(not(in("spec.groupName", hiddenGroups))); - } - - getAccepts().flatMap(accepts -> accepts.stream() - .filter(StringUtils::hasText) - .map(accept -> accept.replace("/*", "/").toLowerCase()) - .distinct() - .map(accept -> startsWith("spec.mediaType", accept)) - .reduce(QueryFactory::or) - ) - .ifPresent(builder::andQuery); - - return builder.build(); - } - - public static void buildParameters(Builder builder) { - IListRequest.buildParameters(builder); - builder.parameter(QueryParamBuildUtil.sortParameter()) - .parameter(parameterBuilder() - .in(ParameterIn.QUERY) - .name("ungrouped") - .required(false) - .description(""" - Filter attachments without group. This parameter will ignore group \ - parameter.\ - """) - .implementation(Boolean.class)) - .parameter(parameterBuilder() - .in(ParameterIn.QUERY) - .name("keyword") - .required(false) - .description("Keyword for searching.") - .implementation(String.class)) - .parameter(parameterBuilder() - .in(ParameterIn.QUERY) - .name("accepts") - .required(false) - .description("Acceptable media types.") - .array( - arraySchemaBuilder() - .uniqueItems(true) - .schema(schemaBuilder() - .implementation(String.class) - .example("image/*")) - ) - .implementationArray(String.class) - ); - } - } - public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url, @Schema(requiredMode = REQUIRED) String policyName, String groupName, diff --git a/application/src/main/java/run/halo/app/core/attachment/impl/AttachmentListerImpl.java b/application/src/main/java/run/halo/app/core/attachment/impl/AttachmentListerImpl.java new file mode 100644 index 0000000000..3f0511d5d6 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/attachment/impl/AttachmentListerImpl.java @@ -0,0 +1,37 @@ +package run.halo.app.core.attachment.impl; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.attachment.AttachmentLister; +import run.halo.app.core.attachment.SearchRequest; +import run.halo.app.core.extension.attachment.Attachment; +import run.halo.app.core.extension.attachment.Group; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; +import run.halo.app.extension.ReactiveExtensionClient; + +@Component +@RequiredArgsConstructor +public class AttachmentListerImpl implements AttachmentLister { + private final ReactiveExtensionClient client; + + @Override + public Mono> listBy(SearchRequest searchRequest) { + var groupListOptions = ListOptions.builder() + .labelSelector() + .exists(Group.HIDDEN_LABEL) + .end() + .build(); + return client.listAll(Group.class, groupListOptions, Sort.unsorted()) + .map(group -> group.getMetadata().getName()) + .collectList() + .defaultIfEmpty(List.of()) + .flatMap(hiddenGroups -> client.listBy(Attachment.class, + searchRequest.toListOptions(hiddenGroups), + searchRequest.toPageRequest() + )); + } +} diff --git a/application/src/main/java/run/halo/app/core/endpoint/uc/UcPostAttachmentEndpoint.java b/application/src/main/java/run/halo/app/core/endpoint/uc/UcAttachmentEndpoint.java similarity index 69% rename from application/src/main/java/run/halo/app/core/endpoint/uc/UcPostAttachmentEndpoint.java rename to application/src/main/java/run/halo/app/core/endpoint/uc/UcAttachmentEndpoint.java index 0e04701a77..48f5cbdd0c 100644 --- a/application/src/main/java/run/halo/app/core/endpoint/uc/UcPostAttachmentEndpoint.java +++ b/application/src/main/java/run/halo/app/core/endpoint/uc/UcAttachmentEndpoint.java @@ -9,13 +9,17 @@ import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; +import static run.halo.app.extension.index.query.QueryFactory.equal; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import java.net.URL; import java.util.HashMap; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import lombok.Builder; +import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; @@ -25,6 +29,7 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.server.RouterFunction; @@ -32,31 +37,40 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import run.halo.app.content.PostService; +import run.halo.app.core.attachment.AttachmentLister; +import run.halo.app.core.attachment.SearchRequest; import run.halo.app.core.extension.attachment.Attachment; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.service.AttachmentService; import run.halo.app.extension.GroupVersion; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.exception.NotFoundException; @Component -public class UcPostAttachmentEndpoint implements CustomEndpoint { +public class UcAttachmentEndpoint implements CustomEndpoint { public static final String POST_NAME_LABEL = "content.halo.run/post-name"; public static final String SINGLE_PAGE_NAME_LABEL = "content.halo.run/single-page-name"; private final AttachmentService attachmentService; + private final AttachmentLister attachmentLister; + private final PostService postService; private final SystemConfigurableEnvironmentFetcher systemSettingFetcher; - public UcPostAttachmentEndpoint(AttachmentService attachmentService, PostService postService, + public UcAttachmentEndpoint(AttachmentService attachmentService, + AttachmentLister attachmentLister, PostService postService, SystemConfigurableEnvironmentFetcher systemSettingFetcher) { this.attachmentService = attachmentService; + this.attachmentLister = attachmentLister; this.postService = postService; this.systemSettingFetcher = systemSettingFetcher; } @@ -82,6 +96,19 @@ public RouterFunction endpoint() { ) .response(responseBuilder().implementation(Attachment.class)) ) + .POST("/attachments/-/upload", contentType(MediaType.MULTIPART_FORM_DATA), + this::uploadAttachment, builder -> builder + .operationId("UploadUcAttachment") + .description("Upload attachment to user center storage.") + .tag(tag) + .requestBody(requestBodyBuilder() + .required(true) + .content(contentBuilder() + .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE) + .schema(schemaBuilder().implementation(UcUploadRequest.class)) + )) + .response(responseBuilder().implementation(Attachment.class)) + ) .POST("/attachments/-/upload-from-url", contentType(MediaType.APPLICATION_JSON), this::uploadFromUrlForPost, builder -> builder @@ -103,9 +130,94 @@ public RouterFunction endpoint() { .response(responseBuilder().implementation(Attachment.class)) .build() ) + .GET("/attachments", this::listMyAttachments, builder -> { + builder.operationId("ListMyAttachments") + .description("List attachments of the current user uploaded.") + .tag(tag) + .response(responseBuilder() + .implementation(ListResult.generateGenericClass(Attachment.class)) + ); + SearchRequest.buildParameters(builder); + }) .build(); } + private Mono uploadAttachment(ServerRequest request) { + var builder = UploadContext.builder(); + var filePartMono = request.body(BodyExtractors.toMultipartData()) + .map(UcUploadRequest::new) + .switchIfEmpty( + Mono.error(new ServerWebInputException("Required request body is missing."))) + .doOnNext(uploadRequest -> builder.filePart(uploadRequest.getFile())) + .subscribeOn(Schedulers.boundedElastic()); + + var ownerMono = getCurrentUser() + .doOnNext(builder::owner); + + var storagePolicyMono = + systemSettingFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class) + .mapNotNull(SystemSetting.User::getUcAttachmentPolicy) + .filter(StringUtils::isNotBlank) + .switchIfEmpty(Mono.error(new ServerWebInputException( + "Please contact the administrator to configure the storage policy.")) + ) + .doOnNext(builder::storagePolicy) + .subscribeOn(Schedulers.boundedElastic()); + + return Mono.when(filePartMono, storagePolicyMono, ownerMono) + .then(Mono.fromSupplier(builder::build)) + .flatMap(context -> attachmentService.upload(context.owner(), + context.storagePolicy(), null, context.filePart(), null) + ) + .flatMap(attachment -> ServerResponse.ok().bodyValue(attachment)); + } + + private Mono listMyAttachments(ServerRequest request) { + return getCurrentUser() + .flatMap(username -> { + var searchRequest = new UcSearchRequest(request, username); + return attachmentLister.listBy(searchRequest) + .flatMap(listResult -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listResult) + ); + }); + } + + @Builder + record UploadContext(String owner, String storagePolicy, FilePart filePart) { + } + + public record UcUploadRequest(MultiValueMap formData) { + + @Schema(description = "The file to upload.", requiredMode = REQUIRED) + public FilePart getFile() { + if (formData.getFirst("file") instanceof FilePart file) { + return file; + } + throw new ServerWebInputException("Invalid part of file"); + } + } + + @Getter + public static class UcSearchRequest extends SearchRequest { + private final String owner; + + public UcSearchRequest(ServerRequest request, String owner) { + super(request); + Assert.state(StringUtils.isNotBlank(owner), "Owner must not be blank."); + this.owner = owner; + } + + @Override + public ListOptions toListOptions(List hiddenGroups) { + var listOptions = super.toListOptions(hiddenGroups); + return ListOptions.builder(listOptions) + .andQuery((equal("spec.ownerName", owner))) + .build(); + } + } + private Mono uploadFromUrlForPost(ServerRequest request) { var uploadFromUrlRequestMono = request.bodyToMono(UploadFromUrlRequest.class); @@ -218,7 +330,7 @@ private Mono getCurrentUser() { @Override public GroupVersion groupVersion() { - return GroupVersion.parseAPIVersion("uc.api.content.halo.run/v1alpha1"); + return GroupVersion.parseAPIVersion("uc.api.storage.halo.run/v1alpha1"); } public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url, diff --git a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java index a04a90208a..9fb3443529 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java @@ -46,7 +46,7 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl private RememberMeRequestCache rememberMeRequestCache = new WebSessionRememberMeRequestCache(); private final ServerAuthenticationSuccessHandler defaultSuccessHandler = - new RedirectServerAuthenticationSuccessHandler("/console/"); + new RedirectServerAuthenticationSuccessHandler("/uc"); public UsernamePasswordHandler(ServerResponse.Context context, MessageSource messageSource, LoginHandlerEnhancer loginHandlerEnhancer) { diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index b2869a3e11..810ed858ed 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -1,6 +1,6 @@ server: port: 8090 - forward-headers-strategy: framework + forward-headers-strategy: native compression: enabled: true error: diff --git a/application/src/main/resources/extensions/authproviders.yaml b/application/src/main/resources/extensions/authproviders.yaml index 87cad00e27..82080199b1 100644 --- a/application/src/main/resources/extensions/authproviders.yaml +++ b/application/src/main/resources/extensions/authproviders.yaml @@ -10,9 +10,9 @@ metadata: spec: displayName: Local description: Built-in authentication for Halo. - logo: /images/logo.png + logo: /images/login-methods/login-with-credentials.svg website: https://www.halo.run authenticationUrl: /login method: post rememberMeSupport: true - authType: form + authType: form \ No newline at end of file diff --git a/application/src/main/resources/extensions/role-template-uc-attachment.yaml b/application/src/main/resources/extensions/role-template-uc-attachment.yaml new file mode 100644 index 0000000000..132b358aef --- /dev/null +++ b/application/src/main/resources/extensions/role-template-uc-attachment.yaml @@ -0,0 +1,15 @@ +apiVersion: v1alpha1 +kind: Role +metadata: + name: role-template-uc-attachment-manager + labels: + halo.run/role-template: "true" + annotations: + rbac.authorization.halo.run/module: "Attachments Management" + rbac.authorization.halo.run/display-name: "UC Attachment Manage" + rbac.authorization.halo.run/ui-permissions: | + [ "uc:attachments:manage" ] +rules: + - apiGroups: [ "uc.api.storage.halo.run" ] + resources: [ "attachments", "attachments/upload", "attachments/upload-from-url" ] + verbs: [ "create", "list" ] diff --git a/application/src/main/resources/extensions/role-template-uc-content.yaml b/application/src/main/resources/extensions/role-template-uc-content.yaml index 575d68e68e..60c20d7e0c 100644 --- a/application/src/main/resources/extensions/role-template-uc-content.yaml +++ b/application/src/main/resources/extensions/role-template-uc-content.yaml @@ -40,7 +40,7 @@ metadata: rbac.authorization.halo.run/display-name: "Post Author" rbac.authorization.halo.run/dependencies: | [ "role-template-post-contributor", "role-template-post-publisher", "role-template-recycle-my-post", - "role-template-post-attachment-manager" ] + "role-template-uc-attachment-manager" ] rules: [ ] --- @@ -100,6 +100,7 @@ rules: verbs: [ "update" ] --- +# TODO remove this in next major version apiVersion: v1alpha1 kind: Role metadata: @@ -121,17 +122,10 @@ apiVersion: v1alpha1 kind: Role metadata: name: role-template-post-attachment-manager + deletionTimestamp: 2024-09-30T14:00:41.813954138Z labels: halo.run/role-template: "true" annotations: rbac.authorization.halo.run/module: "Posts Management" rbac.authorization.halo.run/display-name: "Post Attachment Manager" - rbac.authorization.halo.run/ui-permissions: | - [ "uc:attachments:manage" ] -rules: - - apiGroups: [ "uc.api.content.halo.run" ] - resources: [ "attachments" ] - verbs: [ "create", "update", "delete" ] - - apiGroups: [ "uc.api.content.halo.run" ] - resources: [ "attachments/upload-from-url" ] - verbs: [ "create" ] +rules: [ ] diff --git a/application/src/main/resources/extensions/system-setting.yaml b/application/src/main/resources/extensions/system-setting.yaml index eaf4414840..60626a1243 100644 --- a/application/src/main/resources/extensions/system-setting.yaml +++ b/application/src/main/resources/extensions/system-setting.yaml @@ -118,6 +118,11 @@ spec: label: "头像存储位置" value: "default-policy" help: 指定用户上传头像的存储策略 + - $formkit: attachmentPolicySelect + name: ucAttachmentPolicy + label: "个人中心附件存储位置" + value: "default-policy" + help: 指定用户在个人中心上传的附件的存储位置 - group: comment label: 评论设置 formSchema: diff --git a/application/src/main/resources/static/images/login-methods/login-with-credentials.svg b/application/src/main/resources/static/images/login-methods/login-with-credentials.svg new file mode 100644 index 0000000000..2e323aa68a --- /dev/null +++ b/application/src/main/resources/static/images/login-methods/login-with-credentials.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/application/src/main/resources/templates/gateway_modules/common_fragments.html b/application/src/main/resources/templates/gateway_modules/common_fragments.html index 6ce5f4a414..cb0bd48b5e 100644 --- a/application/src/main/resources/templates/gateway_modules/common_fragments.html +++ b/application/src/main/resources/templates/gateway_modules/common_fragments.html @@ -119,3 +119,73 @@ + +
+ + +
+ +
+ + +
+ +
+ + +
diff --git a/application/src/main/resources/templates/gateway_modules/common_fragments.properties b/application/src/main/resources/templates/gateway_modules/common_fragments.properties index f079f83e50..36f5946b88 100644 --- a/application/src/main/resources/templates/gateway_modules/common_fragments.properties +++ b/application/src/main/resources/templates/gateway_modules/common_fragments.properties @@ -1,3 +1,9 @@ socialLogin.label=社交登录 js.sendVerificationCode.success=发送成功 -js.sendVerificationCode.failed=发送失败,请稍后再试 \ No newline at end of file +js.sendVerificationCode.failed=发送失败,请稍后再试 + +signupNotice.description=没有账号? +signupNotice.link=立即注册 +loginNotice.description=已有账号, +loginNotice.link=立即登录 +returnToSite=返回网站 \ No newline at end of file diff --git a/application/src/main/resources/templates/gateway_modules/common_fragments_en.properties b/application/src/main/resources/templates/gateway_modules/common_fragments_en.properties index cb299c93a5..77019abe17 100644 --- a/application/src/main/resources/templates/gateway_modules/common_fragments_en.properties +++ b/application/src/main/resources/templates/gateway_modules/common_fragments_en.properties @@ -1,3 +1,9 @@ socialLogin.label=Social Login js.sendVerificationCode.success=Sent Successfully -js.sendVerificationCode.failed=Sending Failed, Please Try Again Later \ No newline at end of file +js.sendVerificationCode.failed=Sending Failed, Please Try Again Later + +signupNotice.description=Don't have an account? +signupNotice.link=Sign up +loginNotice.description=Already have an account, +loginNotice.link=Login now +returnToSite=Return to site \ No newline at end of file diff --git a/application/src/main/resources/templates/gateway_modules/common_fragments_es.properties b/application/src/main/resources/templates/gateway_modules/common_fragments_es.properties index cbda753fe4..e79af38312 100644 --- a/application/src/main/resources/templates/gateway_modules/common_fragments_es.properties +++ b/application/src/main/resources/templates/gateway_modules/common_fragments_es.properties @@ -1,3 +1,9 @@ socialLogin.label=Inicio de Sesión Social js.sendVerificationCode.success=Enviado con éxito -js.sendVerificationCode.failed=Error al enviar, por favor intente nuevamente más tarde \ No newline at end of file +js.sendVerificationCode.failed=Error al enviar, por favor intente nuevamente más tarde + +signupNotice.description=¿No tienes una cuenta? +signupNotice.link=Regístrate ahora +loginNotice.description=Ya tienes una cuenta, +loginNotice.link=Inicia sesión ahora +returnToSite=Volver al sitio \ No newline at end of file diff --git a/application/src/main/resources/templates/gateway_modules/common_fragments_zh_TW.properties b/application/src/main/resources/templates/gateway_modules/common_fragments_zh_TW.properties index d55ad22de9..ddc0d3f899 100644 --- a/application/src/main/resources/templates/gateway_modules/common_fragments_zh_TW.properties +++ b/application/src/main/resources/templates/gateway_modules/common_fragments_zh_TW.properties @@ -1,3 +1,9 @@ socialLogin.label=社交登入 js.sendVerificationCode.success=發送成功 -js.sendVerificationCode.failed=發送失敗,請稍後再試 \ No newline at end of file +js.sendVerificationCode.failed=發送失敗,請稍後再試 + +signupNotice.description=沒有帳號? +signupNotice.link=立即註冊 +loginNotice.description=已有帳號, +loginNotice.link=立即登入 +returnToSite=返回網站 \ No newline at end of file diff --git a/application/src/main/resources/templates/gateway_modules/form_fragments.html b/application/src/main/resources/templates/gateway_modules/form_fragments.html index 1a80002818..1533e1cfa1 100644 --- a/application/src/main/resources/templates/gateway_modules/form_fragments.html +++ b/application/src/main/resources/templates/gateway_modules/form_fragments.html @@ -55,9 +55,9 @@ - -
+
+
diff --git a/application/src/main/resources/templates/signup.html b/application/src/main/resources/templates/signup.html index 1aca0da135..e602bad92b 100644 --- a/application/src/main/resources/templates/signup.html +++ b/application/src/main/resources/templates/signup.html @@ -18,6 +18,8 @@

+
+
diff --git a/application/src/main/resources/themes/theme-earth.zip b/application/src/main/resources/themes/theme-earth.zip index aa2fcd9634..b636c95943 100644 Binary files a/application/src/main/resources/themes/theme-earth.zip and b/application/src/main/resources/themes/theme-earth.zip differ diff --git a/application/src/test/java/run/halo/app/XForwardHeaderTest.java b/application/src/test/java/run/halo/app/XForwardHeaderTest.java index e0e15e9be4..5237b27904 100644 --- a/application/src/test/java/run/halo/app/XForwardHeaderTest.java +++ b/application/src/test/java/run/halo/app/XForwardHeaderTest.java @@ -17,7 +17,7 @@ import reactor.test.StepVerifier; @SpringBootTest(webEnvironment = RANDOM_PORT, - properties = "server.forward-headers-strategy=framework") + properties = "server.forward-headers-strategy=native") class XForwardHeaderTest { @LocalServerPort diff --git a/application/src/test/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpointTest.java index fb0a1ba56a..bd0f77d2b0 100644 --- a/application/src/test/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpointTest.java @@ -1,8 +1,6 @@ package run.halo.app.core.extension.attachment.endpoint; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -19,7 +17,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -28,16 +25,15 @@ import org.springframework.web.reactive.function.BodyInserters; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.core.attachment.AttachmentLister; import run.halo.app.core.attachment.endpoint.AttachmentEndpoint; import run.halo.app.core.extension.attachment.Attachment; -import run.halo.app.core.extension.attachment.Group; import run.halo.app.core.extension.attachment.Policy; import run.halo.app.core.extension.attachment.Policy.PolicySpec; import run.halo.app.core.user.service.impl.DefaultAttachmentService; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; -import run.halo.app.extension.PageRequest; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.ReactiveUrlDataBufferFetcher; import run.halo.app.plugin.extensionpoint.ExtensionGetter; @@ -54,6 +50,9 @@ class AttachmentEndpointTest { @Mock ReactiveUrlDataBufferFetcher dataBufferFetcher; + @Mock + AttachmentLister attachmentLister; + AttachmentEndpoint endpoint; WebTestClient webClient; @@ -62,7 +61,7 @@ class AttachmentEndpointTest { void setUp() { var attachmentService = new DefaultAttachmentService(client, extensionGetter, dataBufferFetcher); - endpoint = new AttachmentEndpoint(attachmentService, client); + endpoint = new AttachmentEndpoint(attachmentService, attachmentLister); webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()) .apply(springSecurity()) .build(); @@ -240,11 +239,7 @@ class SearchTest { @Test void shouldListUngroupedAttachments() { - when(client.listAll(eq(Group.class), any(), any(Sort.class))) - .thenReturn(Flux.empty()); - - when(client.listBy(same(Attachment.class), any(), any(PageRequest.class))) - .thenReturn(Mono.just(ListResult.emptyResult())); + when(attachmentLister.listBy(any())).thenReturn(Mono.just(ListResult.emptyResult())); webClient .get() @@ -257,11 +252,7 @@ void shouldListUngroupedAttachments() { @Test void searchAttachmentWhenGroupIsEmpty() { - when(client.listAll(eq(Group.class), any(), any(Sort.class))) - .thenReturn(Flux.empty()); - - when(client.listBy(eq(Attachment.class), any(), any(PageRequest.class))) - .thenReturn(Mono.empty()); + when(attachmentLister.listBy(any())).thenReturn(Mono.just(ListResult.emptyResult())); webClient .get() @@ -269,7 +260,7 @@ void searchAttachmentWhenGroupIsEmpty() { .exchange() .expectStatus().isOk(); - verify(client).listBy(eq(Attachment.class), any(), any(PageRequest.class)); + verify(attachmentLister).listBy(any()); } } diff --git a/ui/console-src/modules/contents/attachments/components/AttachmentDetailModal.vue b/ui/console-src/modules/contents/attachments/components/AttachmentDetailModal.vue index 2111013cb7..c82fa25783 100644 --- a/ui/console-src/modules/contents/attachments/components/AttachmentDetailModal.vue +++ b/ui/console-src/modules/contents/attachments/components/AttachmentDetailModal.vue @@ -1,4 +1,5 @@