Merge pull request #6688 from guqing/refactor/6468

fix: resolve 403 error on menu page when system config access is missing
pull/6703/head^2
John Niang 2024-09-28 18:30:50 +08:00 committed by GitHub
commit 9305fd51d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 517 additions and 26 deletions

View File

@ -7565,6 +7565,72 @@
]
}
},
"/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}": {
"get": {
"description": "Get system config by group",
"operationId": "getSystemConfigByGroup",
"parameters": [
{
"description": "Group of the system config",
"in": "path",
"name": "group",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"type": "object"
}
},
"application/json": {}
},
"description": "default response"
}
},
"tags": [
"SystemConfigV1alpha1Console"
]
},
"put": {
"description": "Update system config by group",
"operationId": "updateSystemConfigByGroup",
"parameters": [
{
"description": "Group of the system config",
"in": "path",
"name": "group",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"*/*": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"204 NO_CONTENT": {
"content": {},
"description": "default response"
}
},
"tags": [
"SystemConfigV1alpha1Console"
]
}
},
"/apis/console.api.migration.halo.run/v1alpha1/backup-files": {
"get": {
"description": "Get backup files from backup root.",

View File

@ -3267,6 +3267,72 @@
]
}
},
"/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}": {
"get": {
"description": "Get system config by group",
"operationId": "getSystemConfigByGroup",
"parameters": [
{
"description": "Group of the system config",
"in": "path",
"name": "group",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"content": {
"*/*": {
"schema": {
"type": "object"
}
},
"application/json": {}
},
"description": "default response"
}
},
"tags": [
"SystemConfigV1alpha1Console"
]
},
"put": {
"description": "Update system config by group",
"operationId": "updateSystemConfigByGroup",
"parameters": [
{
"description": "Group of the system config",
"in": "path",
"name": "group",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"*/*": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"204 NO_CONTENT": {
"content": {},
"description": "default response"
}
},
"tags": [
"SystemConfigV1alpha1Console"
]
}
},
"/apis/console.api.migration.halo.run/v1alpha1/backup-files": {
"get": {
"description": "Get backup files from backup root.",

View File

@ -0,0 +1,102 @@
package run.halo.app.core.endpoint;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
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 com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.utils.JsonUtils;
@Component
@RequiredArgsConstructor
public class SystemConfigEndpoint implements CustomEndpoint {
private final SystemConfigurableEnvironmentFetcher configurableEnvironmentFetcher;
private final ReactiveExtensionClient client;
@Override
public RouterFunction<ServerResponse> endpoint() {
final var tag = "SystemConfigV1alpha1Console";
return SpringdocRouteBuilder.route()
.GET("/systemconfigs/{group}", this::getConfigByGroup,
builder -> builder.operationId("getSystemConfigByGroup")
.description("Get system config by group")
.tag(tag)
.response(responseBuilder()
.content(contentBuilder()
.mediaType(MediaType.APPLICATION_JSON_VALUE)
)
.implementation(ObjectNode.class))
.parameter(parameterBuilder()
.in(ParameterIn.PATH)
.name("group")
.required(true)
.description("Group of the system config")
)
)
.PUT("/systemconfigs/{group}", this::updateConfigByGroup,
builder -> builder.operationId("updateSystemConfigByGroup")
.description("Update system config by group")
.tag(tag)
.parameter(parameterBuilder()
.in(ParameterIn.PATH)
.name("group")
.required(true)
.description("Group of the system config")
)
.requestBody(requestBodyBuilder()
.implementation(ObjectNode.class)
)
.response(responseBuilder()
.responseCode(String.valueOf(HttpStatus.NO_CONTENT))
.implementation(Void.class)
)
)
.build();
}
private Mono<ServerResponse> updateConfigByGroup(ServerRequest request) {
final var group = request.pathVariable("group");
return request.bodyToMono(ObjectNode.class)
.flatMap(objectNode -> configurableEnvironmentFetcher.getConfigMap()
.flatMap(configMap -> {
var data = configMap.getData();
data.put(group, JsonUtils.objectToJson(objectNode));
return client.update(configMap);
})
)
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
.filter(OptimisticLockingFailureException.class::isInstance))
.then(ServerResponse.noContent().build());
}
private Mono<ServerResponse> getConfigByGroup(ServerRequest request) {
final var group = request.pathVariable("group");
return configurableEnvironmentFetcher.fetch(group, ObjectNode.class)
.switchIfEmpty(Mono.fromSupplier(JsonNodeFactory.instance::objectNode))
.flatMap(json -> ServerResponse.ok().bodyValue(json));
}
@Override
public GroupVersion groupVersion() {
return new GroupVersion("console.api.halo.run", "v1alpha1");
}
}

View File

@ -14,6 +14,10 @@ rules:
- apiGroups: [ "" ]
resources: [ "menus", "menuitems" ]
verbs: [ "*" ]
- apiGroups: [ "console.api.halo.run" ]
resources: [ "systemconfigs" ]
resourceNames: [ "menu" ]
verbs: [ "update" ]
---
apiVersion: v1alpha1
kind: "Role"
@ -30,3 +34,7 @@ rules:
- apiGroups: [ "" ]
resources: [ "menus", "menuitems" ]
verbs: [ "get", "list" ]
- apiGroups: [ "console.api.halo.run" ]
resources: [ "systemconfigs" ]
resourceNames: [ "menu" ]
verbs: [ "get" ]

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { usePermission } from "@/utils/permission";
import type { Menu } from "@halo-dev/api-client";
import { coreApiClient } from "@halo-dev/api-client";
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
import {
Dialog,
Toast,
@ -21,6 +21,10 @@ import { onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import MenuEditingModal from "./MenuEditingModal.vue";
interface SystemMenuConfig {
primary: string;
}
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -64,10 +68,10 @@ const {
}
},
refetchInterval(data) {
const deletingMenus = data?.filter(
const hasDeletingMenu = data?.some(
(menu) => !!menu.metadata.deletionTimestamp
);
return deletingMenus?.length ? 1000 : false;
return hasDeletingMenu ? 1000 : false;
},
});
@ -137,35 +141,25 @@ onMounted(async () => {
const { data: primaryMenuName, refetch: refetchPrimaryMenuName } = useQuery({
queryKey: ["primary-menu-name"],
queryFn: async () => {
const { data } = await coreApiClient.configMap.getConfigMap({
name: "system",
});
const { data } =
await consoleApiClient.configMap.system.getSystemConfigByGroup({
group: "menu",
});
if (!data.data?.menu) {
return "";
}
const { primary } = (data as SystemMenuConfig) || {};
const menuConfig = JSON.parse(data.data.menu);
return menuConfig.primary;
return primary;
},
});
const handleSetPrimaryMenu = async (menu: Menu) => {
const { data: systemConfigMap } = await coreApiClient.configMap.getConfigMap({
name: "system",
await consoleApiClient.configMap.system.updateSystemConfigByGroup({
group: "menu",
body: {
primary: menu.metadata.name,
},
});
if (systemConfigMap.data) {
const menuConfigToUpdate = JSON.parse(systemConfigMap.data?.menu || "{}");
menuConfigToUpdate.primary = menu.metadata.name;
systemConfigMap.data["menu"] = JSON.stringify(menuConfigToUpdate);
await coreApiClient.configMap.updateConfigMap({
name: "system",
configMap: systemConfigMap,
});
}
await refetchPrimaryMenuName();
Toast.success(t("core.menu.operations.set_primary.toast_success"));

View File

@ -55,6 +55,7 @@ import {
SnapshotV1alpha1Api,
SnapshotV1alpha1UcApi,
SubscriptionV1alpha1Api,
SystemConfigV1alpha1ConsoleApi,
SystemV1alpha1ConsoleApi,
SystemV1alpha1PublicApi,
TagV1alpha1Api,
@ -320,6 +321,13 @@ function createConsoleApiClient(axiosInstance: AxiosInstance) {
theme: {
theme: new ThemeV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),
},
configMap: {
system: new SystemConfigV1alpha1ConsoleApi(
undefined,
baseURL,
axiosInstance
),
},
};
}
@ -448,6 +456,5 @@ export {
createPublicApiClient,
createUcApiClient,
defaultPublicApiClient as publicApiClient,
defaultUcApiClient as ucApiClient
defaultUcApiClient as ucApiClient,
};

View File

@ -65,6 +65,7 @@ api/single-page-v1alpha1-public-api.ts
api/snapshot-v1alpha1-api.ts
api/snapshot-v1alpha1-uc-api.ts
api/subscription-v1alpha1-api.ts
api/system-config-v1alpha1-console-api.ts
api/system-v1alpha1-console-api.ts
api/system-v1alpha1-public-api.ts
api/tag-v1alpha1-api.ts

View File

@ -77,6 +77,7 @@ export * from './api/single-page-v1alpha1-public-api';
export * from './api/snapshot-v1alpha1-api';
export * from './api/snapshot-v1alpha1-uc-api';
export * from './api/subscription-v1alpha1-api';
export * from './api/system-config-v1alpha1-console-api';
export * from './api/system-v1alpha1-console-api';
export * from './api/system-v1alpha1-public-api';
export * from './api/tag-v1alpha1-api';

View File

@ -0,0 +1,246 @@
/* tslint:disable */
/* eslint-disable */
/**
* Halo
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 2.20.0-SNAPSHOT
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from '../configuration';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
/**
* SystemConfigV1alpha1ConsoleApi - axios parameter creator
* @export
*/
export const SystemConfigV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Get system config by group
* @param {string} group Group of the system config
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSystemConfigByGroup: async (group: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'group' is not null or undefined
assertParamExists('getSystemConfigByGroup', 'group', group)
const localVarPath = `/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}`
.replace(`{${"group"}}`, encodeURIComponent(String(group)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication basicAuth required
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Update system config by group
* @param {string} group Group of the system config
* @param {object} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSystemConfigByGroup: async (group: string, body?: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'group' is not null or undefined
assertParamExists('updateSystemConfigByGroup', 'group', group)
const localVarPath = `/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}`
.replace(`{${"group"}}`, encodeURIComponent(String(group)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication basicAuth required
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* SystemConfigV1alpha1ConsoleApi - functional programming interface
* @export
*/
export const SystemConfigV1alpha1ConsoleApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SystemConfigV1alpha1ConsoleApiAxiosParamCreator(configuration)
return {
/**
* Get system config by group
* @param {string} group Group of the system config
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSystemConfigByGroup(group: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSystemConfigByGroup(group, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SystemConfigV1alpha1ConsoleApi.getSystemConfigByGroup']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Update system config by group
* @param {string} group Group of the system config
* @param {object} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateSystemConfigByGroup(group: string, body?: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSystemConfigByGroup(group, body, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SystemConfigV1alpha1ConsoleApi.updateSystemConfigByGroup']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
/**
* SystemConfigV1alpha1ConsoleApi - factory interface
* @export
*/
export const SystemConfigV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = SystemConfigV1alpha1ConsoleApiFp(configuration)
return {
/**
* Get system config by group
* @param {SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {
return localVarFp.getSystemConfigByGroup(requestParameters.group, options).then((request) => request(axios, basePath));
},
/**
* Update system config by group
* @param {SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.updateSystemConfigByGroup(requestParameters.group, requestParameters.body, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for getSystemConfigByGroup operation in SystemConfigV1alpha1ConsoleApi.
* @export
* @interface SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest
*/
export interface SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest {
/**
* Group of the system config
* @type {string}
* @memberof SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroup
*/
readonly group: string
}
/**
* Request parameters for updateSystemConfigByGroup operation in SystemConfigV1alpha1ConsoleApi.
* @export
* @interface SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest
*/
export interface SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest {
/**
* Group of the system config
* @type {string}
* @memberof SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroup
*/
readonly group: string
/**
*
* @type {object}
* @memberof SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroup
*/
readonly body?: object
}
/**
* SystemConfigV1alpha1ConsoleApi - object-oriented interface
* @export
* @class SystemConfigV1alpha1ConsoleApi
* @extends {BaseAPI}
*/
export class SystemConfigV1alpha1ConsoleApi extends BaseAPI {
/**
* Get system config by group
* @param {SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigV1alpha1ConsoleApi
*/
public getSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest, options?: RawAxiosRequestConfig) {
return SystemConfigV1alpha1ConsoleApiFp(this.configuration).getSystemConfigByGroup(requestParameters.group, options).then((request) => request(this.axios, this.basePath));
}
/**
* Update system config by group
* @param {SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigV1alpha1ConsoleApi
*/
public updateSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest, options?: RawAxiosRequestConfig) {
return SystemConfigV1alpha1ConsoleApiFp(this.configuration).updateSystemConfigByGroup(requestParameters.group, requestParameters.body, options).then((request) => request(this.axios, this.basePath));
}
}