Skip to content

Commit

Permalink
Merge branch 'main' into refactor/4049
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Jun 28, 2023
2 parents 9fa8e3b + 8db4cec commit 3f62e2a
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 44 deletions.
2 changes: 1 addition & 1 deletion application/build.gradle
@@ -1,5 +1,5 @@
plugins {
id 'org.springframework.boot' version '3.1.0'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
id "com.gorylenko.gradle-git-properties" version "2.3.2"
id "checkstyle"
Expand Down
Expand Up @@ -2,6 +2,7 @@

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.ServerRequest;

/**
Expand All @@ -14,6 +15,7 @@ public class IpAddressUtils {

private static final String[] IP_HEADER_NAMES = {
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"CF-Connecting-IP",
Expand All @@ -36,17 +38,18 @@ public class IpAddressUtils {
public static String getClientIp(ServerHttpRequest request) {
for (String header : IP_HEADER_NAMES) {
String ipList = request.getHeaders().getFirst(header);
if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) {
if (StringUtils.hasText(ipList) && !UNKNOWN.equalsIgnoreCase(ipList)) {
String[] ips = ipList.trim().split("[,;]");
for (String ip : ips) {
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
if (StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
return ip;
}
}
}
}
var remoteAddress = request.getRemoteAddress();
return remoteAddress == null ? UNKNOWN : remoteAddress.getAddress().getHostAddress();
return remoteAddress == null || remoteAddress.isUnresolved()
? UNKNOWN : remoteAddress.getAddress().getHostAddress();
}


Expand Down
Expand Up @@ -198,7 +198,9 @@ Mono<ServerResponse> createReply(ServerRequest request) {
.defaultIfEmpty(reply);
})
.flatMap(reply -> replyService.create(commentName, reply))
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
.flatMap(comment -> ServerResponse.ok().bodyValue(comment))
.transformDeferred(createIpBasedRateLimiter(request))
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
}

private boolean checkReplyOwner(Reply reply, Boolean onlySystemUser) {
Expand Down
2 changes: 1 addition & 1 deletion application/src/main/resources/application.yaml
Expand Up @@ -80,5 +80,5 @@ resilience4j.ratelimiter:
comment-creation:
limitForPeriod: 10
limitRefreshPeriod: 1m
timeoutDuration: 10s
timeoutDuration: 0s

Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.InetSocketAddress;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
Expand All @@ -19,12 +20,31 @@ void testGetIPAddressFromCloudflareProxy() {
}

@Test
void testGetUnknownIPAddress() {
void testGetIPAddressFromXRealIpHeader() {
var request = MockServerHttpRequest.get("/")
.header("X-Real-IP", "127.0.0.1")
.build();
var expected = "127.0.0.1";
var actual = IpAddressUtils.getClientIp(request);
assertEquals(expected, actual);
}

@Test
void testGetUnknownIPAddressWhenRemoteAddressIsNull() {
var request = MockServerHttpRequest.get("/").build();
var actual = IpAddressUtils.getClientIp(request);
assertEquals(IpAddressUtils.UNKNOWN, actual);
}

@Test
void testGetUnknownIPAddressWhenRemoteAddressIsUnresolved() {
var request = MockServerHttpRequest.get("/")
.remoteAddress(InetSocketAddress.createUnresolved("localhost", 8090))
.build();
var actual = IpAddressUtils.getClientIp(request);
assertEquals(IpAddressUtils.UNKNOWN, actual);
}

@Test
void testGetIPAddressWithMultipleHeaders() {
var headers = new HttpHeaders();
Expand Down
Expand Up @@ -145,7 +145,7 @@ void createComment() {
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ofSeconds(10))
.build();
RateLimiter rateLimiter = RateLimiter.of("comment-creation-from-ip-" + "0:0:0:0:0:0:0:0",
RateLimiter rateLimiter = RateLimiter.of("comment-creation-from-ip-" + "0:0:0:0:0:0:0:0",
config);
when(rateLimiterRegistry.rateLimiter(anyString(), anyString())).thenReturn(rateLimiter);

Expand Down Expand Up @@ -183,8 +183,13 @@ void createReply() {
replyRequest.setContent("content");
replyRequest.setAllowNotification(true);

when(rateLimiterRegistry.rateLimiter("comment-creation-from-ip-127.0.0.1",
"comment-creation"))
.thenReturn(RateLimiter.ofDefaults("comment-creation"));

webTestClient.post()
.uri("/comments/test-comment/reply")
.header("X-Forwarded-For", "127.0.0.1")
.bodyValue(replyRequest)
.exchange()
.expectStatus()
Expand All @@ -196,5 +201,8 @@ void createReply() {
assertThat(value.getSpec().getIpAddress()).isNotNull();
assertThat(value.getSpec().getUserAgent()).isNotNull();
assertThat(value.getSpec().getQuoteReply()).isNull();

verify(rateLimiterRegistry).rateLimiter("comment-creation-from-ip-127.0.0.1",
"comment-creation");
}
}
66 changes: 65 additions & 1 deletion console/src/components/editor/DefaultEditor.vue
Expand Up @@ -36,10 +36,12 @@ import {
lowlight,
type AnyExtension,
Editor,
ToolbarSubItem,
} from "@halo-dev/richtext-editor";
import {
IconCalendar,
IconCharacterRecognition,
IconFolder,
IconLink,
IconUserFollow,
Toast,
Expand All @@ -49,6 +51,9 @@ import {
import AttachmentSelectorModal from "@/modules/contents/attachments/components/AttachmentSelectorModal.vue";
import ExtensionCharacterCount from "@tiptap/extension-character-count";
import MdiFileImageBox from "~icons/mdi/file-image-box";
import MdiVideoPlusOutline from "~icons/mdi/video-plus-outline";
import MdiImagePlusOutline from "~icons/mdi/image-plus-outline";
import MdiVolume from "~icons/mdi/volume";
import MdiFormatHeader1 from "~icons/mdi/format-header-1";
import MdiFormatHeader2 from "~icons/mdi/format-header-2";
import MdiFormatHeader3 from "~icons/mdi/format-header-3";
Expand Down Expand Up @@ -221,8 +226,67 @@ onMounted(() => {
title: i18n.global.t(
"core.components.default_editor.toolbar.attachment"
),
action: () => (attachmentSelectorModal.value = true),
},
children: [
{
priority: 10,
component: ToolbarSubItem,
props: {
editor,
isActive: false,
icon: markRaw(IconFolder),
title: i18n.global.t(
"core.components.default_editor.toolbar.select_attachment"
),
action: () => (attachmentSelectorModal.value = true),
},
},
{
priority: 20,
component: ToolbarSubItem,
props: {
editor,
isActive: false,
icon: markRaw(MdiImagePlusOutline),
title: i18n.global.t(
"core.components.default_editor.toolbar.insert_image"
),
action: () => {
editor.chain().focus().setImage({ src: "" }).run();
},
},
},
{
priority: 30,
component: ToolbarSubItem,
props: {
editor,
isActive: false,
icon: markRaw(MdiVideoPlusOutline),
title: i18n.global.t(
"core.components.default_editor.toolbar.insert_video"
),
action: () => {
editor.chain().focus().setVideo({ src: "" }).run();
},
},
},
{
priority: 40,
component: ToolbarSubItem,
props: {
editor,
isActive: false,
icon: markRaw(MdiVolume),
title: i18n.global.t(
"core.components.default_editor.toolbar.insert_audio"
),
action: () => {
editor.chain().focus().setAudio({ src: "" }).run();
},
},
},
],
};
},
};
Expand Down
2 changes: 1 addition & 1 deletion console/src/components/login/LoginForm.vue
Expand Up @@ -95,7 +95,7 @@ const handleLogin = async () => {
const { title: errorTitle, detail: errorDetail } = e.response?.data || {};
if (errorTitle || errorDetail) {
Toast.error([errorTitle, errorDetail].filter(Boolean).join(" - "));
Toast.error(errorDetail || errorTitle);
} else {
Toast.error(t("core.common.toast.unknown_error"));
}
Expand Down
7 changes: 4 additions & 3 deletions console/src/locales/en.yaml
Expand Up @@ -1076,6 +1076,10 @@ core:
placeholder: "Enter / to select input type."
toolbar:
attachment: Insert attachment
select_attachment: Attachments library
insert_image: Insert image
insert_video: Insert video
insert_audio: Insert audio
upload_attachment:
toast:
no_available_policy: There is currently no available storage policy
Expand Down Expand Up @@ -1168,13 +1172,10 @@ core:
save_failed_and_retry: "Failed to save, please retry"
publish_failed_and_retry: "Failed to publish, please retry"
network_error: "Network error, please check your connection"
request_parameter_error: "Request parameter error: {title}"
login_expired: "Login expired, please log in again"
forbidden: Access denied
not_found: Resource not found
server_internal_error_with_title: "Internal server error: {title}"
server_internal_error: Internal server error
unknown_error_with_title: "Unknown error: {title}"
unknown_error: Unknown error
dialog:
titles:
Expand Down
7 changes: 4 additions & 3 deletions console/src/locales/zh-CN.yaml
Expand Up @@ -1076,6 +1076,10 @@ core:
placeholder: "输入 / 以选择输入类型"
toolbar:
attachment: 插入附件
select_attachment: 从附件库选择
insert_image: 从外链插入图片
insert_video: 从外链插入视频
insert_audio: 从外链插入音频
upload_attachment:
toast:
no_available_policy: 目前没有可用的存储策略
Expand Down Expand Up @@ -1168,13 +1172,10 @@ core:
save_failed_and_retry: 保存失败,请重试
publish_failed_and_retry: 发布失败,请重试
network_error: 网络错误,请检查网络连接
request_parameter_error: 请求参数错误:{title}
login_expired: 登录已过期,请重新登录
forbidden: 无权限访问
not_found: 资源不存在
server_internal_error_with_title: 服务器内部错误:{title}
server_internal_error: 服务器内部错误
unknown_error_with_title: 未知错误:{title}
unknown_error: 未知错误
dialog:
titles:
Expand Down
7 changes: 4 additions & 3 deletions console/src/locales/zh-TW.yaml
Expand Up @@ -1076,6 +1076,10 @@ core:
placeholder: "輸入 / 以選擇輸入類型"
toolbar:
attachment: 插入附件
select_attachment: 從附件庫選擇
insert_image: 從外部連結插入圖片
insert_video: 從外部連結插入影片
insert_audio: 從外部連結插入音訊
upload_attachment:
toast:
no_available_policy: 目前沒有可用的存儲策略
Expand Down Expand Up @@ -1168,13 +1172,10 @@ core:
save_failed_and_retry: 保存失敗,請重試
publish_failed_and_retry: 發布失敗,請重試
network_error: 網絡錯誤,請檢查網絡連接
request_parameter_error: 請求參數錯誤:{title}
login_expired: 登入已過期,請重新登入
forbidden: 無權限訪問
not_found: 資源不存在
server_internal_error_with_title: 伺服器內部錯誤:{title}
server_internal_error: 伺服器內部錯誤
unknown_error_with_title: 未知錯誤:{title}
unknown_error: 未知錯誤
dialog:
titles:
Expand Down
36 changes: 13 additions & 23 deletions console/src/utils/api-client.ts
Expand Up @@ -78,40 +78,30 @@ axiosInstance.interceptors.response.use(
return Promise.reject(error);
}

const { status } = errorResponse;

const { title } = errorResponse.data;

// Don't show error toast
// see https://github.com/halo-dev/halo/issues/2836
if (errorResponse.config.mute) {
return Promise.reject(error);
}

if (status === 400) {
Toast.error(
i18n.global.t("core.common.toast.request_parameter_error", { title })
);
} else if (status === 401) {
const { status } = errorResponse;
const { title, detail } = errorResponse.data;

if (status === 401) {
const userStore = useUserStore();
userStore.loginModalVisible = true;
Toast.warning(i18n.global.t("core.common.toast.login_expired"));
} else if (status === 403) {
Toast.error(i18n.global.t("core.common.toast.forbidden"));
} else if (status === 404) {
Toast.error(i18n.global.t("core.common.toast.not_found"));
} else if (status === 500) {
Toast.error(
i18n.global.t("core.common.toast.server_internal_error_with_title")
);
} else {
Toast.error(
i18n.global.t("core.common.toast.unknown_error_with_title", {
title,
})
);

return Promise.reject(error);
}

if (title || detail) {
Toast.error(detail || title);
return Promise.reject(error);
}

Toast.error(i18n.global.t("core.common.toast.unknown_error"));

return Promise.reject(error);
}
);
Expand Down
1 change: 0 additions & 1 deletion gradle.properties
@@ -1,2 +1 @@
version=2.7.0-SNAPSHOT
r2dbc-mysql.version=1.0.2
2 changes: 1 addition & 1 deletion platform/application/build.gradle
@@ -1,7 +1,7 @@
import org.springframework.boot.gradle.plugin.SpringBootPlugin

plugins {
id 'org.springframework.boot' version '3.1.0' apply false
id 'org.springframework.boot' version '3.1.1' apply false
id 'java-platform'
id 'halo.publish'
id 'signing'
Expand Down

0 comments on commit 3f62e2a

Please sign in to comment.