mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
dbeaver/pro#6462 error instead of redirect for gql requests (#3690)
* dbeaver/pro#6462 error instead of redirect for gql requests * feat: force redirect on supportedHosts change * fix: replace redirect with warning message * fix: prevent active host removal * fix: error message and ignore when on IP address --------- Co-authored-by: Alexey Potsetsuev <wrouds@gmail.com> Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexey Potsetsuev
kseniaguzeeva
parent
564ceb7d82
commit
3877e914c4
+7
-2
@@ -120,12 +120,13 @@ public class CBJettyServer {
|
||||
|
||||
GraphQLEndpoint endpoint = new GraphQLEndpoint(new ServerConfigurationTimeLimitFilter(application));
|
||||
application.addApplicationContextValue(GraphQL.class.getName(), endpoint.getGraphQL());
|
||||
String gqlServletPath = serverConfiguration.getServicesURI() + "gql/*";
|
||||
servletContextHandler.addServlet(
|
||||
new ServletHolder(
|
||||
"graphql",
|
||||
endpoint
|
||||
),
|
||||
serverConfiguration.getServicesURI() + "gql/*"
|
||||
gqlServletPath
|
||||
);
|
||||
servletContextHandler.addEventListener(new CBServerContextListener(application));
|
||||
|
||||
@@ -157,7 +158,11 @@ public class CBJettyServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
FilterHolder hostsFilter = new FilterHolder(new RequestHostFilter(application, excludedFilterPaths));
|
||||
FilterHolder hostsFilter = new FilterHolder(new RequestHostFilter(
|
||||
application,
|
||||
excludedFilterPaths,
|
||||
Set.of(gqlServletPath)
|
||||
));
|
||||
servletContextHandler.addFilter(hostsFilter, "/*", null);
|
||||
|
||||
|
||||
|
||||
+44
-13
@@ -39,18 +39,30 @@ public class RequestHostFilter implements Filter {
|
||||
@NotNull
|
||||
private final CBApplication<?> application;
|
||||
private final Set<String> excludedPaths = new HashSet<>();
|
||||
private final Set<String> errorPaths = new HashSet<>();
|
||||
|
||||
public RequestHostFilter(@NotNull CBApplication<?> application, @NotNull Set<String> excludedPaths) {
|
||||
public RequestHostFilter(
|
||||
@NotNull CBApplication<?> application,
|
||||
@NotNull Set<String> excludedPaths,
|
||||
@NotNull Set<String> errorPaths
|
||||
) {
|
||||
this.application = application;
|
||||
this.excludedPaths.addAll(
|
||||
excludedPaths.stream()
|
||||
.map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", "")))
|
||||
.toList()
|
||||
);
|
||||
this.errorPaths.addAll(
|
||||
errorPaths.stream()
|
||||
.map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", "")))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
boolean requestAllowed = true;
|
||||
|
||||
if (request instanceof HttpServletRequest httpRequest) {
|
||||
CBServerConfig serverConfig = application.getServerConfiguration();
|
||||
URI originUri;
|
||||
@@ -64,7 +76,8 @@ public class RequestHostFilter implements Filter {
|
||||
}
|
||||
boolean isIpAddress = InetAddresses.isInetAddress(originUri.getHost());
|
||||
String servletPath = httpRequest.getServletPath();
|
||||
if (!isIpAddress) {
|
||||
requestAllowed = isIpAddress;
|
||||
if (!requestAllowed) {
|
||||
if (CommonUtils.isNotEmpty(servletPath)) {
|
||||
for (String excludedPath : excludedPaths) {
|
||||
if (servletPath.contains(excludedPath)) {
|
||||
@@ -73,17 +86,21 @@ public class RequestHostFilter implements Filter {
|
||||
}
|
||||
}
|
||||
}
|
||||
validateHosts(serverConfig, httpRequest, response, originUri);
|
||||
requestAllowed = validateHosts(serverConfig, httpRequest, (HttpServletResponse) response, originUri);
|
||||
}
|
||||
if (requestAllowed) {
|
||||
requestAllowed = validateSchema(serverConfig, httpRequest, (HttpServletResponse) response, originUri);
|
||||
}
|
||||
validateSchema(serverConfig, httpRequest, response, originUri);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
if (requestAllowed) {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSchema(
|
||||
private boolean validateSchema(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
@NotNull HttpServletRequest httpRequest,
|
||||
@NotNull ServletResponse response,
|
||||
@NotNull HttpServletResponse response,
|
||||
@NotNull URI originUri
|
||||
) {
|
||||
boolean httpsExpected = serverConfig.isForceHttps();
|
||||
@@ -100,22 +117,24 @@ public class RequestHostFilter implements Filter {
|
||||
redirectUrlBuilder.append("?")
|
||||
.append(httpRequest.getQueryString());
|
||||
}
|
||||
((HttpServletResponse) response).sendRedirect(redirectUrlBuilder.toString());
|
||||
response.sendRedirect(redirectUrlBuilder.toString());
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to redirect to HTTPS", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void validateHosts(
|
||||
private boolean validateHosts(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
@NotNull HttpServletRequest httpRequest,
|
||||
@NotNull ServletResponse response,
|
||||
@NotNull HttpServletResponse response,
|
||||
URI originUri
|
||||
) throws IOException {
|
||||
List<String> availableHosts = serverConfig.getSupportedHosts();
|
||||
if (CommonUtils.isEmpty(availableHosts)) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
var requestHostBuilder = new StringBuilder(originUri.getHost());
|
||||
@@ -124,13 +143,25 @@ public class RequestHostFilter implements Filter {
|
||||
}
|
||||
String requestHost = requestHostBuilder.toString();
|
||||
if (!availableHosts.contains(requestHost)) {
|
||||
for (String errorPath : errorPaths) {
|
||||
if (httpRequest.getServletPath().contains(errorPath)) {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.getWriter().write(
|
||||
"Request host '" + requestHost + "' is not allowed. Available hosts: " + availableHosts
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
log.warn("Request host '" + requestHost + "' is not allowed. Redirect to default: " + availableHosts);
|
||||
redirectToDefaultHost((HttpServletResponse) response, httpRequest, availableHosts);
|
||||
redirectToDefaultHost(response, httpRequest, availableHosts);
|
||||
return false;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error(e.getMessage(), e);
|
||||
redirectToDefaultHost((HttpServletResponse) response, httpRequest, availableHosts);
|
||||
redirectToDefaultHost(response, httpRequest, availableHosts);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void redirectToDefaultHost(
|
||||
|
||||
@@ -23,10 +23,12 @@ import { FieldDescription } from './FieldDescription.js';
|
||||
import { FieldLabel } from './FieldLabel.js';
|
||||
import { FormContext } from './FormContext.js';
|
||||
import textareaStyle from './Textarea.module.css';
|
||||
import { useMergeRefs } from '../useMergeRefs.js';
|
||||
|
||||
type BaseProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'style'> &
|
||||
ILayoutSizeProps & {
|
||||
description?: string;
|
||||
ref?: React.Ref<HTMLTextAreaElement | null>;
|
||||
description?: React.ReactNode;
|
||||
labelTooltip?: string;
|
||||
embedded?: boolean;
|
||||
cursorInitiallyAtEnd?: boolean;
|
||||
@@ -53,6 +55,7 @@ interface TextareaType {
|
||||
}
|
||||
|
||||
export const Textarea: TextareaType = observer(function Textarea({
|
||||
ref,
|
||||
name,
|
||||
value: controlledValue,
|
||||
state,
|
||||
@@ -71,6 +74,7 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
const translate = useTranslate();
|
||||
const inputId = useId();
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const mergedRef = useMergeRefs(...[textareaRef, ref!].filter(Boolean));
|
||||
const layoutProps = getLayoutProps(rest);
|
||||
rest = filterLayoutFakeProps(rest);
|
||||
const styles = useS(textareaStyle);
|
||||
@@ -108,7 +112,7 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
</FieldLabel>
|
||||
<textarea
|
||||
{...rest}
|
||||
ref={textareaRef}
|
||||
ref={mergedRef}
|
||||
id={inputId}
|
||||
required={required}
|
||||
className={s(styles, { textarea: true })}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,15 +13,17 @@ import { useTranslate } from '../localization/useTranslate.js';
|
||||
import { useExecutor } from '../useExecutor.js';
|
||||
import { FormContext } from './FormContext.js';
|
||||
|
||||
export function useCustomInputValidation<T = void>(validation: (value: T) => string | null): React.RefObject<HTMLInputElement | null> {
|
||||
export function useCustomInputValidation<T = void, TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
|
||||
validation: (value: T) => string | null,
|
||||
): React.RefObject<TType | null> {
|
||||
const context = useContext(FormContext);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef<TType | null>(null);
|
||||
const translate = useTranslate();
|
||||
|
||||
function validate(element: HTMLInputElement): boolean {
|
||||
function validate(element: TType): boolean {
|
||||
let value: T = undefined as unknown as T;
|
||||
|
||||
if (element instanceof HTMLInputElement) {
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||
value = element.value as unknown as T;
|
||||
}
|
||||
|
||||
@@ -64,14 +66,14 @@ export function useCustomInputValidation<T = void>(validation: (value: T) => str
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const target = event.target as TType;
|
||||
if (target.validity.valid === false) {
|
||||
validate(target);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const target = event.target as TType;
|
||||
if (target.validity.valid === true) {
|
||||
validate(target);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from './getPathName.js';
|
||||
export * from './getPathParent.js';
|
||||
export * from './getPathParents.js';
|
||||
export * from './getPathParts.js';
|
||||
export * from './isIp.js';
|
||||
export * from './GlobalConstants.js';
|
||||
export * from './ILoadableState.js';
|
||||
export * from './errorOf.js';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function isIp(host: string): boolean {
|
||||
// Matches IPv4 like 127.0.0.1
|
||||
const ipv4 = /^(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)\.(25[0-5]|2[0-4]\d|1\d\d|\d\d?)$/;
|
||||
|
||||
// Matches IPv6 like [2001:0db8::1]
|
||||
const ipv6 = /^\[?([a-f0-9:]+)\]?$/i;
|
||||
|
||||
return ipv4.test(host) || ipv6.test(host);
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap {
|
||||
onConfigurationFinish: async () => {
|
||||
const state = this.serverConfigurationFormStateManager.formState;
|
||||
if (state) {
|
||||
const saved = await state.save();
|
||||
const saved = await this.serverConfigurationFormStateManager.save();
|
||||
|
||||
if (!saved) {
|
||||
const error = getFirstException(state.exception);
|
||||
|
||||
+23
-1
@@ -7,12 +7,24 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Group, GroupTitle, Link, IconOrImage, InputField, Switch, Textarea, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Group,
|
||||
GroupTitle,
|
||||
Link,
|
||||
IconOrImage,
|
||||
InputField,
|
||||
Switch,
|
||||
Textarea,
|
||||
useResource,
|
||||
useTranslate,
|
||||
useCustomInputValidation,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
|
||||
import type { IServerConfigurationPageState } from '../IServerConfigurationPageState.js';
|
||||
import { MIN_SESSION_EXPIRE_TIME } from './MIN_SESSION_EXPIRE_TIME.js';
|
||||
import { WEBSITE_LINKS } from '@cloudbeaver/core-links';
|
||||
import { isIp } from '@cloudbeaver/core-utils';
|
||||
|
||||
interface Props {
|
||||
state: IServerConfigurationPageState;
|
||||
@@ -21,6 +33,15 @@ interface Props {
|
||||
export const ServerConfigurationInfoForm = observer<Props>(function ServerConfigurationInfoForm({ state }) {
|
||||
const serverConfigLoader = useResource(ServerConfigurationInfoForm, ServerConfigResource, undefined);
|
||||
const translate = useTranslate();
|
||||
const validation = useCustomInputValidation<string, HTMLTextAreaElement>(value => {
|
||||
const currentHost = window.location.host;
|
||||
|
||||
if (!isIp(window.location.hostname) && value.trim() && !value.includes(currentHost)) {
|
||||
return translate('administration_configuration_wizard_configuration_supported_hosts_warning', undefined, { host: currentHost });
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
function constructSupportedHostsExample() {
|
||||
const exampleWithPort = serverConfigLoader.data?.distributed ? 'localhost' : 'localhost:5000';
|
||||
@@ -35,6 +56,7 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
|
||||
{translate('administration_configuration_wizard_configuration_server_name')}
|
||||
</InputField>
|
||||
<Textarea
|
||||
ref={validation}
|
||||
title={translate('administration_configuration_wizard_configuration_server_url_description')}
|
||||
name="supportedHosts"
|
||||
rows={3}
|
||||
|
||||
+17
-2
@@ -11,8 +11,9 @@ import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections';
|
||||
import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { DefaultNavigatorSettingsResource, PasswordPolicyResource, ProductInfoResource, ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { FormPart, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import { isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import { isIp, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
|
||||
import { MIN_SESSION_EXPIRE_TIME } from './Form/MIN_SESSION_EXPIRE_TIME.js';
|
||||
import type { IServerConfigurationFormPartState } from './IServerConfigurationFormPartState.js';
|
||||
@@ -51,6 +52,7 @@ export class ServerConfigurationFormPart extends FormPart<IServerConfigurationFo
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
private readonly passwordPolicyResource: PasswordPolicyResource,
|
||||
private readonly passwordPolicyService: PasswordPolicyService,
|
||||
private readonly localizationService: LocalizationService,
|
||||
) {
|
||||
super(formState, DEFAULT_STATE_GETTER());
|
||||
}
|
||||
@@ -67,6 +69,19 @@ export class ServerConfigurationFormPart extends FormPart<IServerConfigurationFo
|
||||
data: IFormState<IServerConfigurationFormPartState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IServerConfigurationFormPartState>>,
|
||||
) {
|
||||
const validation = contexts.getContext(formValidationContext);
|
||||
|
||||
const supportedHosts = this.state.serverConfig.supportedHosts;
|
||||
const currentHost = window.location.host;
|
||||
|
||||
if (!isIp(window.location.hostname) && supportedHosts.trim() && !supportedHosts.includes(currentHost)) {
|
||||
validation.error(
|
||||
this.localizationService.translate('administration_configuration_wizard_configuration_supported_hosts_warning', undefined, {
|
||||
host: currentHost,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.administrationScreenService.isConfigurationMode) {
|
||||
await this.authProvidersResource.load(CachedMapAllKey);
|
||||
|
||||
|
||||
+11
-2
@@ -8,6 +8,7 @@
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
|
||||
import { injectable, IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ServerConfigurationFormService } from './ServerConfigurationFormService.js';
|
||||
import { ServerConfigurationFormState } from './ServerConfigurationFormState.js';
|
||||
@@ -27,7 +28,7 @@ export class ServerConfigurationFormStateManager {
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
create(): IFormState<null> {
|
||||
if (this.formState) {
|
||||
return this.formState;
|
||||
}
|
||||
@@ -36,7 +37,15 @@ export class ServerConfigurationFormStateManager {
|
||||
return this.formState;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
async save(): Promise<boolean> {
|
||||
if (!this.formState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.formState.save();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.formState) {
|
||||
this.formState?.dispose();
|
||||
this.formState = null;
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
|
||||
}
|
||||
}
|
||||
|
||||
const saved = await formState.save();
|
||||
const saved = await serverConfigurationFormStateManager.save();
|
||||
|
||||
if (!saved) {
|
||||
const error = getFirstException(part.exception);
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -10,6 +10,7 @@ import { AuthProvidersResource, PasswordPolicyService } from '@cloudbeaver/core-
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import { DefaultNavigatorSettingsResource, PasswordPolicyResource, ProductInfoResource, ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
|
||||
import { ServerConfigurationFormPart } from './ServerConfigurationFormPart.js';
|
||||
|
||||
@@ -25,6 +26,7 @@ export function getServerConfigurationFormPart(formState: IFormState<null>): Ser
|
||||
const authProvidersResource = di.getService(AuthProvidersResource);
|
||||
const passwordPolicyResource = di.getService(PasswordPolicyResource);
|
||||
const passwordPolicyService = di.getService(PasswordPolicyService);
|
||||
const localizationService = di.getService(LocalizationService);
|
||||
|
||||
return new ServerConfigurationFormPart(
|
||||
formState,
|
||||
@@ -35,6 +37,7 @@ export function getServerConfigurationFormPart(formState: IFormState<null>): Ser
|
||||
authProvidersResource,
|
||||
passwordPolicyResource,
|
||||
passwordPolicyService,
|
||||
localizationService,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', 'Server Information'],
|
||||
['administration_configuration_wizard_configuration_server_name', 'Server Name'],
|
||||
|
||||
@@ -41,6 +41,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', 'Informations sur le serveur'],
|
||||
['administration_configuration_wizard_configuration_server_name', 'Nom du serveur'],
|
||||
|
||||
@@ -41,6 +41,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', 'Informazioni sul Server'],
|
||||
['administration_configuration_wizard_configuration_server_name', 'Nome del Server'],
|
||||
|
||||
@@ -20,6 +20,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'Вы можете указать несколько URL серверов, разделенных новой строкой. Пустое значение означает, что все URL разрешены',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', 'Информация о сервере'],
|
||||
['administration_configuration_wizard_configuration_server_name', 'Название сервера'],
|
||||
|
||||
@@ -32,6 +32,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
['administration_configuration_tools_save_tooltip', 'Lưu cấu hình'],
|
||||
['administration_configuration_tools_cancel_tooltip', 'Đặt lại thay đổi'],
|
||||
['administration_configuration_wizard_configuration_server_info', 'Thông tin Server'],
|
||||
|
||||
@@ -38,6 +38,10 @@ export default [
|
||||
'administration_configuration_wizard_configuration_supported_hosts_description',
|
||||
'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed',
|
||||
],
|
||||
[
|
||||
'administration_configuration_wizard_configuration_supported_hosts_warning',
|
||||
'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.',
|
||||
],
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', '服务器信息'],
|
||||
['administration_configuration_wizard_configuration_server_name', '服务器名称'],
|
||||
|
||||
Reference in New Issue
Block a user