mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
fix(core-connections): CB-511 connection credentials
This commit is contained in:
@@ -17,6 +17,7 @@ import { FormContext } from './FormContext';
|
||||
import { isControlPresented } from './isControlPresented';
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value'> & {
|
||||
description?: string;
|
||||
mod?: 'surface';
|
||||
long?: boolean;
|
||||
short?: boolean;
|
||||
@@ -52,6 +53,7 @@ export const InputField: InputFieldType = observer(function InputField({
|
||||
state,
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
mod,
|
||||
long,
|
||||
short,
|
||||
@@ -86,11 +88,16 @@ export const InputField: InputFieldType = observer(function InputField({
|
||||
<input
|
||||
{...rest}
|
||||
name={name}
|
||||
value={value}
|
||||
value={value ?? ''}
|
||||
onChange={handleChange}
|
||||
{...use({ mod })}
|
||||
required={required}
|
||||
/>
|
||||
{description && (
|
||||
<field-description as='div'>
|
||||
{description}
|
||||
</field-description>
|
||||
)}
|
||||
</field>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ export const Radio: RadioType = observer(function Radio({
|
||||
type="radio"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
value={value ?? ''}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ShadowInput = React.forwardRef(function ShadowInput({
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
value={children}
|
||||
value={children ?? ''}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
<field-label as='label'>{children}</field-label>
|
||||
<textarea
|
||||
{...rest}
|
||||
value={value}
|
||||
value={value ?? ''}
|
||||
name={name}
|
||||
onChange={handleChange}
|
||||
{...use({ mod })}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const baseFormControlStyles = css`
|
||||
flex-basis: 100%;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 150px;
|
||||
padding-left: 180px;
|
||||
}
|
||||
|
||||
field[|raw] {
|
||||
|
||||
@@ -20,7 +20,62 @@ import { Link } from '../Link';
|
||||
import { TextPlaceholder } from '../TextPlaceholder';
|
||||
import { formStyles } from './formStyles';
|
||||
|
||||
interface Props {
|
||||
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
|
||||
|
||||
interface RenderFieldProps {
|
||||
property: ObjectPropertyInfo;
|
||||
state: Record<string, any>;
|
||||
autofillToken?: string;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
autoHide?: boolean;
|
||||
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
|
||||
property,
|
||||
state,
|
||||
autofillToken = '',
|
||||
disabled,
|
||||
readOnly,
|
||||
autoHide,
|
||||
onFocus,
|
||||
}) {
|
||||
const href = property.features.includes('href');
|
||||
const password = property.features.includes('password');
|
||||
let description: string | undefined;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<FormFieldDescription label={property.displayName} raw>
|
||||
<Link href={property.value} target='_blank' rel='noopener noreferrer'>{property.description}</Link>
|
||||
</FormFieldDescription>
|
||||
);
|
||||
}
|
||||
|
||||
if (password && property.value) {
|
||||
description = 'Password saved';
|
||||
}
|
||||
|
||||
return (
|
||||
<InputField
|
||||
type={password ? 'password' : 'text'}
|
||||
name={property.id!}
|
||||
state={state}
|
||||
description={description}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoHide={autoHide}
|
||||
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
|
||||
mod='surface'
|
||||
onFocus={onFocus}
|
||||
>
|
||||
{property.displayName}
|
||||
</InputField>
|
||||
);
|
||||
});
|
||||
|
||||
interface ObjectPropertyFormProps {
|
||||
properties: ObjectPropertyInfo[] | undefined;
|
||||
credentials: Record<string, string | number>;
|
||||
autofillToken?: string;
|
||||
@@ -31,9 +86,7 @@ interface Props {
|
||||
onFocus?: (name: string) => void;
|
||||
}
|
||||
|
||||
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
|
||||
|
||||
export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectPropertyInfoForm({
|
||||
export const ObjectPropertyInfoForm: React.FC<ObjectPropertyFormProps> = observer(function ObjectPropertyInfoForm({
|
||||
properties,
|
||||
credentials,
|
||||
autofillToken = '',
|
||||
@@ -49,16 +102,7 @@ export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectP
|
||||
if (onFocus) {
|
||||
onFocus(e.target.name);
|
||||
}
|
||||
if (e.target.type !== 'password') {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = properties?.find(property => property.id === e.target.name);
|
||||
|
||||
if (property?.value === e.target.value) {
|
||||
credentials[e.target.name] = '';
|
||||
}
|
||||
}, [properties, credentials, onFocus]);
|
||||
}, [onFocus]);
|
||||
|
||||
if (!properties || properties.length === 0) {
|
||||
return <TextPlaceholder>Properties empty</TextPlaceholder>;
|
||||
@@ -68,25 +112,15 @@ export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectP
|
||||
<form-body as='div' className={className}>
|
||||
{properties.map(property => (
|
||||
<FormGroup key={property.id}>
|
||||
{property.features.includes('href') ? (
|
||||
<FormFieldDescription label={property.displayName} raw>
|
||||
<Link href={property.value} target='_blank' rel='noopener noreferrer'>{property.description}</Link>
|
||||
</FormFieldDescription>
|
||||
) : (
|
||||
<InputField
|
||||
type={property.features.includes('password') ? 'password' : 'text'}
|
||||
name={property.id!}
|
||||
state={credentials}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoHide={autoHide}
|
||||
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
|
||||
mod='surface'
|
||||
onFocus={handleFocus}
|
||||
>
|
||||
{property.displayName}
|
||||
</InputField>
|
||||
)}
|
||||
<RenderField
|
||||
property={property}
|
||||
state={credentials}
|
||||
autofillToken={autofillToken}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoHide={autoHide}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</FormGroup>
|
||||
))}
|
||||
</form-body>
|
||||
|
||||
+7
-3
@@ -23,7 +23,6 @@ import { IConnectionFormModel } from './IConnectionFormModel';
|
||||
export class ConnectionFormController
|
||||
implements IInitializableController {
|
||||
@observable isSaving: boolean;
|
||||
@observable readonly metadata: Map<string, any>;
|
||||
|
||||
readonly afterSave: IExecutor<string>;
|
||||
|
||||
@@ -52,7 +51,6 @@ implements IInitializableController {
|
||||
private dbDriverResource: DBDriverResource
|
||||
) {
|
||||
this.isSaving = false;
|
||||
this.metadata = new Map<string, any>();
|
||||
this.afterSave = new Executor();
|
||||
}
|
||||
|
||||
@@ -141,7 +139,13 @@ implements IInitializableController {
|
||||
return true;
|
||||
}
|
||||
for (const property of this.model.connection.authProperties) {
|
||||
if (property.value !== null && this.model.credentials[property.id!] !== property.value) {
|
||||
const value = this.model.credentials[property.id!];
|
||||
|
||||
if (property.features.includes('password')) {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
} else if (value !== property.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -139,10 +139,9 @@ export const Options = observer(function Options({
|
||||
</FormGroup>
|
||||
<ObjectPropertyInfoForm
|
||||
autofillToken='new-password'
|
||||
properties={controller.authModel.properties}
|
||||
properties={controller.properties}
|
||||
credentials={model.credentials}
|
||||
disabled={disabled}
|
||||
onFocus={controller.resetPassword}
|
||||
/>
|
||||
<FormGroup>
|
||||
<FieldCheckbox
|
||||
|
||||
+15
-64
@@ -10,7 +10,7 @@ import { action, computed } from 'mobx';
|
||||
|
||||
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { DatabaseAuthModel } from '@cloudbeaver/core-sdk';
|
||||
import { DatabaseAuthModel, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DatabaseAuthModelsResource } from '../../../../DatabaseAuthModelsResource';
|
||||
import { DBDriver, DBDriverResource } from '../../../../DBDriverResource';
|
||||
@@ -39,6 +39,13 @@ implements IInitializableController {
|
||||
return this.dbAuthModelsResource.isLoading();
|
||||
}
|
||||
|
||||
@computed get properties(): ObjectPropertyInfo[] {
|
||||
if (this.model.connection.authProperties) {
|
||||
return this.model.connection.authProperties;
|
||||
}
|
||||
return this.authModel?.properties || [];
|
||||
}
|
||||
|
||||
private model!: IConnectionFormModel;
|
||||
private nameTemplate = /^.*?\s(|\(.*?\)\s)connection$/;
|
||||
|
||||
@@ -61,39 +68,14 @@ implements IInitializableController {
|
||||
|
||||
onFormChange = (value?: unknown, name?: string): void => {
|
||||
this.updateName(name);
|
||||
this.resetPassword(name || '');
|
||||
};
|
||||
|
||||
@action
|
||||
private setDefaults(prevDriverId: string | null) {
|
||||
this.setDefaultParameters(prevDriverId);
|
||||
this.cleanCredentials();
|
||||
this.model.connection.properties = {};
|
||||
this.model.connection.authModel = this.driver?.defaultAuthModel;
|
||||
this.cleanCredentials();
|
||||
}
|
||||
|
||||
resetPassword = (name: string): void => {
|
||||
const passwordProperty = this.model.connection.authProperties.find(property => property.features.includes('password'));
|
||||
|
||||
if (passwordProperty && (this.isCredentialsChanged() || passwordProperty.id === name)) {
|
||||
if (this.model.credentials[passwordProperty.id!] === passwordProperty.value) {
|
||||
this.model.credentials[passwordProperty.id!] = '';
|
||||
this.model.connection.saveCredentials = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private isCredentialsChanged() {
|
||||
if (!Object.keys(this.model.credentials).length) {
|
||||
return false;
|
||||
}
|
||||
for (const property of this.model.connection.authProperties) {
|
||||
// commented because of case when name changed, password should be reset
|
||||
if (/* property.value !== null && */this.model.credentials[property.id!] !== property.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private cleanCredentials() {
|
||||
@@ -160,25 +142,10 @@ implements IInitializableController {
|
||||
private async loadDrivers() {
|
||||
try {
|
||||
await this.dbDriverResource.loadAll();
|
||||
await this.dbAuthModelsResource.load(
|
||||
this.model.connection.authModel || this.driver!.defaultAuthModel
|
||||
);
|
||||
this.setDefaultParameters();
|
||||
|
||||
if (!this.driver || this.driver.anonymousAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dbAuthModelsResource.load(
|
||||
this.model.connection?.authModel || this.driver.defaultAuthModel
|
||||
);
|
||||
|
||||
if (this.authModel) {
|
||||
for (const property of this.model.connection.authProperties) {
|
||||
this.model.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Can\'t load driver auth model');
|
||||
}
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Can\'t load drivers');
|
||||
}
|
||||
@@ -187,33 +154,17 @@ implements IInitializableController {
|
||||
private async loadDriver(driverId: string | null, prev: string | null) {
|
||||
if (!driverId) {
|
||||
this.model.connection.authModel = undefined;
|
||||
this.cleanCredentials();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dbDriverResource.load(driverId);
|
||||
await this.dbAuthModelsResource.load(
|
||||
this.model.connection.authModel || this.driver!.defaultAuthModel
|
||||
);
|
||||
this.setDefaults(prev);
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, `Can't load driver ${driverId}`);
|
||||
}
|
||||
|
||||
if (!this.driver || this.driver.anonymousAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dbAuthModelsResource.load(
|
||||
this.model.connection?.authModel || this.driver.defaultAuthModel
|
||||
);
|
||||
|
||||
if (this.authModel) {
|
||||
for (const property of this.model.connection.authProperties) {
|
||||
this.model.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Can\'t load driver auth model');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-6
@@ -12,8 +12,9 @@ import {
|
||||
injectable, IInitializableController, IDestructibleController
|
||||
} from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { GQLErrorCatcher, AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk';
|
||||
import { GQLErrorCatcher, AdminConnectionGrantInfo, ResourceKeyUtils, ResourceKey } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DatabaseAuthModelsResource } from '../../../DatabaseAuthModelsResource';
|
||||
import { DBDriverResource } from '../../../DBDriverResource';
|
||||
import { AdminConnection, ConnectionsResource } from '../../ConnectionsResource';
|
||||
|
||||
@@ -51,26 +52,68 @@ implements IInitializableController, IDestructibleController {
|
||||
constructor(
|
||||
private connectionsResource: ConnectionsResource,
|
||||
private notificationService: NotificationService,
|
||||
private dbDriverResource: DBDriverResource
|
||||
) { }
|
||||
private dbDriverResource: DBDriverResource,
|
||||
private dbAuthModelsResource: DatabaseAuthModelsResource
|
||||
) {
|
||||
this.updateConnectionInfo = this.updateConnectionInfo.bind(this);
|
||||
}
|
||||
|
||||
init(id: string) {
|
||||
async init(id: string): Promise<void> {
|
||||
this.connectionId = id;
|
||||
this.loadConnectionInfo();
|
||||
await this.loadConnectionInfo();
|
||||
this.connectionsResource.onItemAdd.addHandler(this.updateConnectionInfo);
|
||||
}
|
||||
|
||||
destruct(): void {
|
||||
this.connectionsResource.onItemAdd.removeHandler(this.updateConnectionInfo);
|
||||
}
|
||||
|
||||
private async loadConnectionInfo() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// we create a copy to protect the current value from mutation
|
||||
this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId)));
|
||||
await this.connectionsResource.load(this.connectionId);
|
||||
await this.updateConnectionInfo(this.connectionId);
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, "Can't load ConnectionInfo", `Can't load ConnectionInfo ${this.connectionId}`);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateConnectionInfo(key: ResourceKey<string>) {
|
||||
if (!ResourceKeyUtils.includes(key, this.connectionId)) {
|
||||
return;
|
||||
}
|
||||
this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId)));
|
||||
await this.updateCredentials();
|
||||
}
|
||||
|
||||
private cleanCredentials() {
|
||||
for (const property of Object.keys(this.credentials)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.credentials[property];
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCredentials() {
|
||||
this.cleanCredentials();
|
||||
if (!this.driver || this.driver.anonymousAccess || !this.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dbAuthModelsResource.load(
|
||||
this.connection.authModel || this.driver.defaultAuthModel
|
||||
);
|
||||
|
||||
for (const property of this.connection.authProperties) {
|
||||
if (!property.features.includes('password')) {
|
||||
this.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Can\'t load driver auth model');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ fragment AdminConnection on ConnectionInfo {
|
||||
authNeeded
|
||||
authModel
|
||||
authProperties {
|
||||
id
|
||||
value
|
||||
features
|
||||
...UserConnectionAuthProperties
|
||||
}
|
||||
|
||||
features
|
||||
|
||||
@@ -1167,7 +1167,7 @@ export interface NavGetStructContainersQuery { navGetStructContainers: { catalog
|
||||
|
||||
export type AdminConnectionFragment = (
|
||||
Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'useUrl' | 'readOnly' | 'saveCredentials' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel' | 'supportedDataFormats'>
|
||||
& { origin: ObjectOriginInfoFragment; authProperties: Array<Pick<ObjectPropertyInfo, 'id' | 'value' | 'features'>> }
|
||||
& { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[] }
|
||||
);
|
||||
|
||||
export type AdminUserInfoFragment = (
|
||||
@@ -1455,6 +1455,20 @@ export const ObjectOriginInfoFragmentDoc = `
|
||||
icon
|
||||
}
|
||||
`;
|
||||
export const UserConnectionAuthPropertiesFragmentDoc = `
|
||||
fragment UserConnectionAuthProperties on ObjectPropertyInfo {
|
||||
id
|
||||
displayName
|
||||
description
|
||||
category
|
||||
dataType
|
||||
value
|
||||
validValues
|
||||
defaultValue
|
||||
features
|
||||
order
|
||||
}
|
||||
`;
|
||||
export const AdminConnectionFragmentDoc = `
|
||||
fragment AdminConnection on ConnectionInfo {
|
||||
id
|
||||
@@ -1478,14 +1492,13 @@ export const AdminConnectionFragmentDoc = `
|
||||
authNeeded
|
||||
authModel
|
||||
authProperties {
|
||||
id
|
||||
value
|
||||
features
|
||||
...UserConnectionAuthProperties
|
||||
}
|
||||
features
|
||||
supportedDataFormats
|
||||
}
|
||||
${ObjectOriginInfoFragmentDoc}`;
|
||||
${ObjectOriginInfoFragmentDoc}
|
||||
${UserConnectionAuthPropertiesFragmentDoc}`;
|
||||
export const AdminUserInfoFragmentDoc = `
|
||||
fragment AdminUserInfo on AdminUserInfo {
|
||||
userId
|
||||
@@ -1548,20 +1561,6 @@ export const UserConnectionFragmentDoc = `
|
||||
supportedDataFormats
|
||||
}
|
||||
`;
|
||||
export const UserConnectionAuthPropertiesFragmentDoc = `
|
||||
fragment UserConnectionAuthProperties on ObjectPropertyInfo {
|
||||
id
|
||||
displayName
|
||||
description
|
||||
category
|
||||
dataType
|
||||
value
|
||||
validValues
|
||||
defaultValue
|
||||
features
|
||||
order
|
||||
}
|
||||
`;
|
||||
export const AsyncTaskCancelDocument = `
|
||||
mutation asyncTaskCancel($taskId: String!) {
|
||||
result: asyncTaskCancel(id: $taskId)
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ implements IInitializableController, IDestructibleController {
|
||||
try {
|
||||
// we create a copy to protect the current value from mutation
|
||||
await this.usersResource.load(this.userId);
|
||||
this.updateUser(this.userId);
|
||||
await this.updateUser(this.userId);
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, `Can't load user ${this.userId}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user