mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e30bbdc77 | ||
|
|
7419bd1412 | ||
|
|
68758381aa | ||
|
|
23ae5b410e | ||
|
|
a7d4ec7e6f |
@@ -292,7 +292,7 @@ public class ServletAppUtils {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getOriginFromRequestOrThrow(HttpServletRequest request) throws DBWebException {
|
||||
public static String getOriginFromRequest(@NotNull HttpServletRequest request) {
|
||||
String origin = request.getHeader(HEADER_ORIGIN);
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
origin = request.getHeader(HEADER_X_ORIGIN);
|
||||
@@ -353,7 +353,22 @@ public class ServletAppUtils {
|
||||
if (CommonUtils.isEmpty(rootUri)) {
|
||||
return uri;
|
||||
}
|
||||
String[] split = uri.split(rootUri);
|
||||
return split[0];
|
||||
rootUri = "/" + rootUri;
|
||||
if (!uri.endsWith(rootUri)) {
|
||||
rootUri = rootUri + "/";
|
||||
}
|
||||
return substringBeforeLast(uri, rootUri);
|
||||
}
|
||||
|
||||
public static String substringBeforeLast(
|
||||
@NotNull String string,
|
||||
@NotNull String separator
|
||||
) {
|
||||
if (CommonUtils.isNotEmpty(string) && CommonUtils.isNotEmpty(separator)) {
|
||||
int pos = string.lastIndexOf(separator);
|
||||
return pos == -1 ? string : string.substring(0, pos);
|
||||
} else {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -103,7 +103,6 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
// Persistence
|
||||
protected SMAdminController securityController;
|
||||
private boolean configurationMode = false;
|
||||
private String localHostAddress;
|
||||
protected String containerId;
|
||||
private final List<InetAddress> localInetAddresses = new ArrayList<>();
|
||||
|
||||
@@ -226,15 +225,6 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
|
||||
eventController.setForceSkipEvents(isConfigurationMode()); // do not send events if configuration mode is on
|
||||
|
||||
// Determine address for local host
|
||||
localHostAddress = System.getenv(CBConstants.VAR_CB_LOCAL_HOST_ADDR);
|
||||
if (CommonUtils.isEmpty(localHostAddress)) {
|
||||
localHostAddress = System.getProperty(CBConstants.VAR_CB_LOCAL_HOST_ADDR);
|
||||
}
|
||||
if (CommonUtils.isEmpty(localHostAddress) || CBConstants.HOST_127_0_0_1.equals(localHostAddress) || "::0".equals(
|
||||
localHostAddress)) {
|
||||
localHostAddress = CBConstants.HOST_LOCALHOST;
|
||||
}
|
||||
|
||||
Location instanceLoc = Platform.getInstanceLocation();
|
||||
try {
|
||||
@@ -529,7 +519,7 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
}
|
||||
|
||||
public String getLocalHostAddress() {
|
||||
return localHostAddress;
|
||||
return getServerConfigurationController().getLocalHostAddress();
|
||||
}
|
||||
|
||||
public List<InetAddress> getLocalInetAddresses() {
|
||||
|
||||
+15
-8
@@ -44,8 +44,6 @@ import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -66,6 +64,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
protected final Path homeDirectory;
|
||||
private final Map<String, String> externalProperties = new LinkedHashMap<>();
|
||||
private final Map<String, Object> originalConfigurationProperties = new LinkedHashMap<>();
|
||||
private String localHostAddress;
|
||||
|
||||
protected CBServerConfigurationController(@NotNull T serverConfiguration, @NotNull Path homeDirectory) {
|
||||
super(homeDirectory);
|
||||
@@ -80,6 +79,15 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
@Override
|
||||
public void loadServerConfiguration(Path configPath) throws DBException {
|
||||
log.debug("Using configuration [" + configPath + "]");
|
||||
// Determine address for local host
|
||||
localHostAddress = System.getenv(CBConstants.VAR_CB_LOCAL_HOST_ADDR);
|
||||
if (CommonUtils.isEmpty(localHostAddress)) {
|
||||
localHostAddress = System.getProperty(CBConstants.VAR_CB_LOCAL_HOST_ADDR);
|
||||
}
|
||||
if (CommonUtils.isEmpty(localHostAddress) || CBConstants.HOST_127_0_0_1.equals(localHostAddress) || "::0".equals(
|
||||
localHostAddress)) {
|
||||
localHostAddress = CBConstants.HOST_LOCALHOST;
|
||||
}
|
||||
|
||||
if (!Files.exists(configPath)) {
|
||||
log.error("Configuration file " + configPath + " doesn't exist. Use defaults.");
|
||||
@@ -162,12 +170,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
if (config.getServerURL() == null) {
|
||||
String hostName = config.getServerHost();
|
||||
if (CommonUtils.isEmpty(hostName)) {
|
||||
try {
|
||||
hostName = InetAddress.getLocalHost().getHostName();
|
||||
} catch (UnknownHostException e) {
|
||||
log.debug("Error resolving localhost address: " + e.getMessage());
|
||||
hostName = CBConstants.HOST_LOCALHOST;
|
||||
}
|
||||
hostName = getLocalHostAddress();
|
||||
}
|
||||
config.setServerURL("http://" + hostName + ":" + config.getServerPort());
|
||||
}
|
||||
@@ -669,4 +672,8 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
public void validateFinalServerConfiguration() throws DBException {
|
||||
|
||||
}
|
||||
|
||||
public String getLocalHostAddress() {
|
||||
return localHostAddress;
|
||||
}
|
||||
}
|
||||
+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);
|
||||
|
||||
|
||||
|
||||
+45
-14
@@ -39,23 +39,35 @@ 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;
|
||||
try {
|
||||
String origin = ServletAppUtils.getOriginFromRequestOrThrow(httpRequest);
|
||||
String origin = ServletAppUtils.getOriginFromRequest(httpRequest);
|
||||
originUri = URI.create(origin);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get origin from request", e);
|
||||
@@ -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(
|
||||
|
||||
+2
-2
@@ -494,7 +494,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String providerId
|
||||
) throws DBWebException {
|
||||
String origin = ServletAppUtils.getOriginFromRequestOrThrow(request);
|
||||
String origin = ServletAppUtils.getOriginFromRequest(request);
|
||||
List<WebAuthProviderConfiguration> result = new ArrayList<>();
|
||||
for (SMAuthProviderCustomConfiguration cfg : CBApplication.getInstance().getAppConfiguration().getAuthCustomConfigurations()) {
|
||||
if (providerId != null && !providerId.equals(cfg.getProvider())) {
|
||||
@@ -544,7 +544,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
providerConfig.getProvider(),
|
||||
webSession.getUserId()
|
||||
));
|
||||
return new WebAuthProviderConfiguration(authProvider, providerConfig, ServletAppUtils.getOriginFromRequestOrThrow(request));
|
||||
return new WebAuthProviderConfiguration(authProvider, providerConfig, ServletAppUtils.getOriginFromRequest(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-3
@@ -111,7 +111,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
}
|
||||
try {
|
||||
Map<String, Object> authParameters = new HashMap<>();
|
||||
authParameters.put(SMConstants.USER_ORIGIN, ServletAppUtils.getOriginFromRequestOrThrow(httpRequest));
|
||||
authParameters.put(SMConstants.USER_ORIGIN, ServletAppUtils.getOriginFromRequest(httpRequest));
|
||||
|
||||
var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout);
|
||||
if (smAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) {
|
||||
@@ -227,7 +227,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
List<WebAuthInfo> removedInfos = webSession.removeAuthInfo(providerId);
|
||||
List<String> logoutUrls = new ArrayList<>();
|
||||
var cbApp = CBApplication.getInstance();
|
||||
String origin = ServletAppUtils.getOriginFromRequestOrThrow(httpRequest);
|
||||
String origin = ServletAppUtils.getOriginFromRequest(httpRequest);
|
||||
for (WebAuthInfo removedInfo : removedInfos) {
|
||||
if (removedInfo.getAuthProviderDescriptor()
|
||||
.getInstance() instanceof SMSignOutLinkProvider provider
|
||||
@@ -297,7 +297,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
|
||||
@Override
|
||||
public WebAuthProviderInfo[] getAuthProviders(@NotNull HttpServletRequest request) throws DBWebException {
|
||||
String origin = ServletAppUtils.getOriginFromRequestOrThrow(request);
|
||||
String origin = ServletAppUtils.getOriginFromRequest(request);
|
||||
return WebAuthProviderRegistry.getInstance().getAuthProviders()
|
||||
.stream()
|
||||
.map(descriptor -> new WebAuthProviderInfo(descriptor, origin))
|
||||
|
||||
@@ -240,3 +240,46 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT_INFO
|
||||
PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID),
|
||||
FOREIGN KEY (AUTH_ID) REFERENCES {table_prefix}CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Secrets
|
||||
CREATE TABLE {table_prefix}CB_ACCESS_TOKEN
|
||||
(
|
||||
TOKEN_ID VARCHAR(128) NOT NULL,
|
||||
USER_ID VARCHAR(128) NOT NULL,
|
||||
TOKEN_NAME VARCHAR(128) NOT NULL,
|
||||
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
EXPIRATION_TIME TIMESTAMP NULL,
|
||||
|
||||
PRIMARY KEY (USER_ID, TOKEN_ID),
|
||||
UNIQUE (USER_ID, TOKEN_NAME),
|
||||
FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER(USER_ID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Tasks
|
||||
CREATE TABLE {table_prefix}CB_TASKS
|
||||
(
|
||||
TASK_ID VARCHAR(128) NOT NULL,
|
||||
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
TIMEOUT INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (TASK_ID)
|
||||
);
|
||||
|
||||
CREATE TABLE {table_prefix}CB_SUBJECT_SECRETS
|
||||
(
|
||||
SUBJECT_ID VARCHAR(128) NOT NULL,
|
||||
SECRET_ID VARCHAR(255) NOT NULL,
|
||||
|
||||
PROJECT_ID VARCHAR(128),
|
||||
OBJECT_TYPE VARCHAR(32),
|
||||
OBJECT_ID VARCHAR(128),
|
||||
|
||||
SECRET_VALUE TEXT NOT NULL,
|
||||
|
||||
ENCODING_TYPE VARCHAR(32) DEFAULT 'PLAINTEXT' NOT NULL,
|
||||
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
PRIMARY KEY (SUBJECT_ID, SECRET_ID),
|
||||
FOREIGN KEY (SUBJECT_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const CaptureView = observer<React.PropsWithChildren<ICaptureViewProps>>(
|
||||
useHotkeys(
|
||||
keys,
|
||||
(event, handler) => {
|
||||
if (!state.reference?.contains(document.activeElement)) {
|
||||
if (!event.isTrusted || !state.reference?.contains(document.activeElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+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