Merge remote-tracking branch 'origin/devel' into tech/CB-2149

This commit is contained in:
Ainur
2022-11-11 11:22:39 +01:00
27 changed files with 449 additions and 173 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
FROM eclipse-temurin:17.0.4.1_1-jre-focal
#adoptopenjdk/openjdk11:jdk-11.0.12_7-ubuntu-slim
ARG ARCH=amd64
FROM ${ARCH}/eclipse-temurin:17.0.4.1_1-jre-focal
COPY cloudbeaver /opt/cloudbeaver
@@ -29,6 +29,7 @@ import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMCredentials;
@@ -332,6 +333,25 @@ public class LocalResourceController implements RMController {
registry.checkForErrors();
}
@Override
public void deleteProjectConnectionFolders(
@NotNull String projectId,
@NotNull String[] folderPaths,
boolean dropContents
) throws DBException {
DBPProject project = getProjectMetadata(projectId);
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
for (String folderPath : folderPaths) {
DBPDataSourceFolder folder = registry.getFolder(folderPath);
if (folder != null) {
registry.removeFolder(folder, dropContents);
} else {
log.warn("Can not find folder by path [" + folderPath + "] for deletion");
}
}
registry.checkForErrors();
}
@NotNull
@Override
public RMResource[] listResources(
@@ -426,6 +446,9 @@ public class LocalResourceController implements RMController {
} catch (IOException e) {
throw new DBException("Error moving resource '" + oldResourcePath + "'", e);
}
// Move properties
getProjectMetadata(projectId).moveResourceProperties(oldResourcePath, newResourcePath);
fireRmResourceDeleteEvent(projectId, rmOldResourcePath);
fireRmResourceAddEvent(projectId, newResourcePath);
return DEFAULT_CHANGE_ID;
@@ -19,7 +19,6 @@ package io.cloudbeaver.model.session;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.model.user.WebUserOriginInfo;
import io.cloudbeaver.registry.WebAuthProviderConfiguration;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
@@ -41,7 +40,7 @@ public class WebAuthInfo implements SMSessionPrincipal {
private final WebSession session;
private final WebUser user;
private final WebAuthProviderDescriptor authProvider;
private WebAuthProviderConfiguration authProviderConfiguration;
private String authProviderConfigurationId;
private SMSession authSession;
private final OffsetDateTime loginTime;
private final DBWUserIdentity userIdentity;
@@ -101,7 +100,11 @@ public class WebAuthInfo implements SMSessionPrincipal {
@Property
public String getAuthConfiguration() {
return authProviderConfiguration == null ? null : authProviderConfiguration.getId();
return authProviderConfigurationId;
}
public void setAuthProviderConfigurationId(String authProviderConfigurationId) {
this.authProviderConfigurationId = authProviderConfigurationId;
}
public WebUser getUser() {
@@ -116,14 +119,6 @@ public class WebAuthInfo implements SMSessionPrincipal {
return authProvider;
}
public WebAuthProviderConfiguration getAuthProviderConfiguration() {
return authProviderConfiguration;
}
public void setAuthProviderConfiguration(WebAuthProviderConfiguration authProviderConfiguration) {
this.authProviderConfiguration = authProviderConfiguration;
}
public SMSession getAuthSession() {
return authSession;
}
@@ -29,6 +29,7 @@ import io.cloudbeaver.utils.WebAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.auth.SMAuthConfigurationReference;
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
import org.jkiss.dbeaver.model.auth.SMAuthProvider;
import org.jkiss.dbeaver.model.auth.SMSession;
@@ -87,8 +88,8 @@ public class WebSessionAuthProcessor {
try {
if (configMode && alreadyLoggedIn) {
for (String providerId : authInfo.getAuthData().keySet()) {
webSession.removeAuthInfo(providerId);
for (SMAuthConfigurationReference authConfiguration : authInfo.getAuthData().keySet()) {
webSession.removeAuthInfo(authConfiguration.getAuthProviderId());
}
}
webSession.updateSMAuthInfo(authInfo);
@@ -102,8 +103,9 @@ public class WebSessionAuthProcessor {
var securityController = webSession.getSecurityController();
Map<String, Object> providerConfig = Collections.emptyMap();
var newAuthInfos = new ArrayList<WebAuthInfo>();
for (Map.Entry<String, Object> entry : authInfo.getAuthData().entrySet()) {
String providerId = entry.getKey();
for (Map.Entry<SMAuthConfigurationReference, Object> entry : authInfo.getAuthData().entrySet()) {
SMAuthConfigurationReference authConfiguration = entry.getKey();
String providerId = authConfiguration.getAuthProviderId();
Map<String, Object> authAttrs = (Map<String, Object>) entry.getValue();
var authProviderDescriptor = getAuthProvider(providerId);
@@ -111,15 +113,13 @@ public class WebSessionAuthProcessor {
SMAuthProviderExternal<?> authProviderExternal = authProviderInstance instanceof SMAuthProviderExternal<?> ?
(SMAuthProviderExternal<?>) authProviderInstance : null;
boolean providerEnabled = isProviderEnabled(providerId);
boolean providerDisabled = !isProviderEnabled(providerId);
if (configMode || webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
// 1. Admin can authorize in any providers
// 2. When it authorizes in non-local provider for the first time we force linkUser flag
if (!providerEnabled && webSession.getUser() != null) {
if (providerDisabled && webSession.getUser() != null) {
linkWithActiveUser = true;
}
} else if (!providerEnabled && !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
throw new DBWebException("Authentication provider '" + providerId + "' is disabled");
}
SMSession authSession;
@@ -174,7 +174,9 @@ public class WebSessionAuthProcessor {
authProviderDescriptor,
userIdentity,
authSession,
OffsetDateTime.now());
OffsetDateTime.now()
);
webAuthInfo.setAuthProviderConfigurationId(authConfiguration.getAuthProviderConfigurationId());
webAuthInfo.setMessage("Authenticated with " + authProviderDescriptor.getLabel() + " provider");
if (configMode) {
webAuthInfo.setUserCredentials(authAttrs);
@@ -415,7 +415,9 @@ public class WebServiceNavigator implements DBWServiceNavigator {
ne.getValue().deleteObject(commandContext, object, options);
commandContext.saveChanges(session.getProgressMonitor(), options);
} else if (node instanceof DBNLocalFolder) {
node.getOwnerProject().getDataSourceRegistry().removeFolder(((DBNLocalFolder) node).getFolder(), false);
} else if (node instanceof DBNResourceManagerResource) {
DBNResourceManagerResource rmResource = ((DBNResourceManagerResource) node);
String resourceProjectId = rmResource.getResourceProject().getId();
@@ -1037,17 +1037,17 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) {
//async auth
var authProviderFederated = (SMAuthProviderFederated) authProviderInstance;
var redirectUrl = buildRedirectLink(
authProviderFederated.getSignInLink(authProviderConfigurationId, Map.of()),
var redirectUrl = buildRedirectLink(authProviderFederated.getSignInLink(authProviderConfigurationId, Map.of()),
authAttemptId);
return SMAuthInfo.inProgress(authAttemptId, redirectUrl, filteredUserCreds);
Map<SMAuthConfigurationReference, Object> authData = Map.of(new SMAuthConfigurationReference(authProviderId,
authProviderConfigurationId), filteredUserCreds);
return SMAuthInfo.inProgress(authAttemptId, redirectUrl, authData);
}
txn.commit();
return finishAuthentication(
SMAuthInfo.inProgress(
authAttemptId,
null,
Map.of(authProviderId, securedUserIdentifyingCredentials)
null, Map.of(new SMAuthConfigurationReference(authProviderId, null), securedUserIdentifyingCredentials)
),
true,
false
@@ -1125,10 +1125,12 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
@Override
public void updateAuthStatus(@NotNull String authId,
@NotNull SMAuthStatus authStatus,
@NotNull Map<String, Object> authInfo,
@Nullable String error) throws DBException {
public void updateAuthStatus(
@NotNull String authId,
@NotNull SMAuthStatus authStatus,
@NotNull Map<SMAuthConfigurationReference, Object> authInfo,
@Nullable String error
) throws DBException {
var existAuthInfo = getAuthStatus(authId);
if (existAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) {
throw new SMException("Authorization already finished and cannot be updated");
@@ -1137,13 +1139,14 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
updateAuthStatus(authId, authStatus, authInfo, error, authSessionInfo.getSmSessionId());
}
private void updateAuthStatus(@NotNull String authId,
@NotNull SMAuthStatus authStatus,
@NotNull Map<String, Object> authInfo,
@Nullable String error,
@Nullable String smSessionId) throws DBException {
try (Connection dbCon = database.openConnection();
JDBCTransaction txn = new JDBCTransaction(dbCon)) {
private void updateAuthStatus(
@NotNull String authId,
@NotNull SMAuthStatus authStatus,
@NotNull Map<SMAuthConfigurationReference, Object> authInfo,
@Nullable String error,
@Nullable String smSessionId
) throws DBException {
try (Connection dbCon = database.openConnection(); JDBCTransaction txn = new JDBCTransaction(dbCon)) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"UPDATE CB_AUTH_ATTEMPT SET AUTH_STATUS=?,AUTH_ERROR=?,SESSION_ID=? WHERE AUTH_ID=?")) {
dbStat.setString(1, authStatus.toString());
@@ -1155,20 +1158,29 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
}
for (Map.Entry<String, Object> entry : authInfo.entrySet()) {
String providerId = entry.getKey();
for (Map.Entry<SMAuthConfigurationReference, Object> entry : authInfo.entrySet()) {
SMAuthConfigurationReference providerId = entry.getKey();
String authJson = gson.toJson(entry.getValue());
try (PreparedStatement dbStat = dbCon.prepareStatement(
"UPDATE CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=?")) {
boolean configIdExist = providerId.getAuthProviderConfigurationId() != null;
var sqlBuilder = new StringBuilder();
sqlBuilder.append("UPDATE CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? ")
.append("WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=? AND ")
.append(configIdExist ? "AUTH_PROVIDER_CONFIGURATION_ID=?" : "AUTH_PROVIDER_CONFIGURATION_ID IS NULL");
try (PreparedStatement dbStat = dbCon.prepareStatement(sqlBuilder.toString())) {
dbStat.setString(1, authJson);
dbStat.setString(2, authId);
dbStat.setString(3, providerId);
dbStat.setString(3, providerId.getAuthProviderId());
if (configIdExist) {
dbStat.setString(4, providerId.getAuthProviderConfigurationId());
}
if (dbStat.executeUpdate() <= 0) {
try (PreparedStatement dbStatIns = dbCon.prepareStatement(
"INSERT INTO CB_AUTH_ATTEMPT_INFO (AUTH_ID,AUTH_PROVIDER_ID,AUTH_STATE) VALUES(?,?,?)")) {
"INSERT INTO CB_AUTH_ATTEMPT_INFO (AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) "
+ "VALUES(?,?,?,?)")) {
dbStatIns.setString(1, authId);
dbStatIns.setString(2, providerId);
dbStatIns.setString(3, authJson);
dbStatIns.setString(2, providerId.getAuthProviderId());
dbStatIns.setString(3, providerId.getAuthProviderConfigurationId());
dbStatIns.setString(4, authJson);
dbStatIns.execute();
}
}
@@ -1199,7 +1211,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
smSessionId = dbResult.getString(3);
}
}
Map<String, Object> authData = new LinkedHashMap<>();
Map<SMAuthConfigurationReference, Object> authData = new LinkedHashMap<>();
String redirectUrl = null;
try (PreparedStatement dbStat = dbCon.prepareStatement(
@@ -1217,14 +1229,13 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
WebAuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId);
var authProviderInstance = authProviderDescriptor.getInstance();
if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) {
redirectUrl = buildRedirectLink(
((SMAuthProviderFederated) authProviderInstance).getRedirectLink(authProviderConfiguration, Map.of()),
authId
);
redirectUrl = buildRedirectLink(((SMAuthProviderFederated) authProviderInstance).getRedirectLink(
authProviderConfiguration,
Map.of()), authId);
}
}
authData.put(authProviderId, authProviderData);
authData.put(new SMAuthConfigurationReference(authProviderId, authProviderConfiguration), authProviderData);
}
}
}
@@ -1368,7 +1379,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
if (authInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) {
throw new SMException("Authorization has already been completed with status: " + authInfo.getAuthStatus());
}
Set<String> authProviderIds = authInfo.getAuthData().keySet();
Set<SMAuthConfigurationReference> authProviderIds = authInfo.getAuthData().keySet();
if (authProviderIds.isEmpty()) {
throw new SMException("Authorization providers are not defined");
}
@@ -1386,20 +1397,19 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
String activeUserId = permissions == null ? null : permissions.getUserId();
Map<String, Object> storedUserData = new LinkedHashMap<>();
Map<SMAuthConfigurationReference, Object> storedUserData = new LinkedHashMap<>();
SMTeam[] allTeams = null;
String detectedAuthRole = null;
for (String authProviderId : authProviderIds) {
for (SMAuthConfigurationReference authConfiguration : authProviderIds) {
String authProviderId = authConfiguration.getAuthProviderId();
WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId);
var userAuthData = (Map<String, Object>) authInfo.getAuthData().get(authProviderId);
var userAuthData = (Map<String, Object>) authInfo.getAuthData().get(authConfiguration);
SMAutoAssign autoAssign = getAutoAssignUserData(authId, authProvider, userAuthData, finishAuthMonitor);
if (autoAssign != null) {
detectedAuthRole = autoAssign.getAuthRole();
}
var userIdFromCreds = findOrCreateExternalUserByCredentials(
authProvider,
authAttemptSessionInfo.getSessionParams(),
var userIdFromCreds = findOrCreateExternalUserByCredentials(authProvider, authAttemptSessionInfo.getSessionParams(),
userAuthData,
finishAuthMonitor,
activeUserId,
@@ -1423,10 +1433,8 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
if (activeUserId == null) {
activeUserId = userIdFromCreds;
}
storedUserData.put(
authProviderId,
saveSecuredCreds ? userAuthData : filterSecuredUserData(userAuthData, getAuthProvider(authProviderId))
);
storedUserData.put(authConfiguration,
saveSecuredCreds ? userAuthData : filterSecuredUserData(userAuthData, getAuthProvider(authProviderId)));
}
String tokenAuthRole = updateUserAuthRoleIfNeeded(activeUserId, detectedAuthRole);
@@ -2156,4 +2164,8 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
return activeUserCredentials.getUserId();
}
private boolean isProviderEnabled(@NotNull String providerId) {
WebAuthConfiguration appConfiguration = application.getAuthConfiguration();
return appConfiguration.isAuthProviderEnabled(providerId);
}
}
@@ -15,15 +15,20 @@ import { uuid } from '@cloudbeaver/core-utils';
import { AuthProvidersResource, AuthProviderConfiguration } from './AuthProvidersResource';
import { type ILoginOptions, UserInfoResource } from './UserInfoResource';
export interface IUserAuthConfiguration {
providerId: string;
configuration: AuthProviderConfiguration;
}
@injectable()
export class AuthInfoService {
get userInfo(): UserInfo | null {
return this.userInfoResource.data;
}
get userAuthConfigurations(): AuthProviderConfiguration[] {
get userAuthConfigurations(): IUserAuthConfiguration[] {
const tokens = this.userInfo?.authTokens;
const result: AuthProviderConfiguration[] = [];
const result: IUserAuthConfiguration[] = [];
if (!tokens) {
return result;
@@ -41,7 +46,7 @@ export class AuthInfoService {
);
if (configuration) {
result.push(configuration);
result.push({ providerId: provider.id, configuration });
}
}
}
@@ -158,9 +158,13 @@ UserInfoIncludes
});
}
async logout(): Promise<void> {
if (this.data) {
await this.graphQLService.sdk.authLogout();
async logout(provider?: string, configuration?: string): Promise<void> {
await this.graphQLService.sdk.authLogout({
provider,
configuration,
});
if (!provider || (this.data?.authTokens.length || 0) <= 1) {
this.setData(null);
this.resetIncludes();
}
+10 -2
View File
@@ -12,7 +12,7 @@ import styled, { css, use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { Icon } from './Icon';
import { IconOrImage } from './IconOrImage';
import { Loader } from './Loader/Loader';
import { useObservableRef } from './useObservableRef';
import { useStyles } from './useStyles';
@@ -31,6 +31,14 @@ const buttonStyles = css`
composes: theme-button from global;
display: flex;
& IconOrImage {
width: 100%;
}
&[disabled] IconOrImage {
opacity: 0.5;
}
& Loader, & button-label {
transition: opacity cubic-bezier(0.4, 0.0, 0.2, 1) 0.3s;
}
@@ -154,7 +162,7 @@ export const Button = observer<ButtonProps>(function Button({
onClick={state.click}
>
<ripple />
{icon && <button-icon><Icon name={icon} viewBox={viewBox} /></button-icon>}
{icon && <button-icon><IconOrImage icon={icon} viewBox={viewBox} /></button-icon>}
<button-label as='span'>{children}</button-label>
<Loader small />
</Button>
@@ -0,0 +1,188 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { ReactNode, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { blobToData, bytesToSize } from '@cloudbeaver/core-utils';
import { Button } from '../Button';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import { IconButton } from '../IconButton';
import { useTranslate } from '../localization/useTranslate';
import { UploadArea } from '../UploadArea';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
import { FormContext } from './FormContext';
const DEFAULT_MAX_FILE_SIZE = 2048;
const INPUT_FILE_FIELD_STYLES = css`
field-label {
display: block;
composes: theme-typography--body1 from global;
font-weight: 500;
}
field-label:not(:empty) {
padding-bottom: 10px;
}
field-description {
display: flex;
align-items: center;
gap: 2px;
}
IconButton {
width: 12px;
height: 12px;
flex-shrink: 0;
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
`;
interface Props<TState> extends ILayoutSizeProps {
name: keyof TState;
state: TState;
accept?: string;
labelTooltip?: string;
tooltip?: string;
required?: boolean;
fileName?: string;
style?: ComponentStyle;
/** Max file size in KB */
maxFileSize?: number;
disabled?: boolean;
className?: string;
children?: ReactNode;
onChange?: (value: string, name: keyof TState) => void;
mapValue?: (value: string) => string;
}
type InputFileType = <TState extends Record<string, any>>(props: Props<TState>) => React.ReactElement<any, any>;
export const InputFile: InputFileType = observer(function InputFile({
name,
state,
accept,
labelTooltip,
tooltip,
required,
fileName,
small,
medium,
large,
tiny,
style,
maxFileSize = DEFAULT_MAX_FILE_SIZE,
disabled,
className,
children,
onChange,
mapValue,
}: Props<Record<any, any>>) {
const translate = useTranslate();
const context = useContext(FormContext);
const [selected, setSelected] = useState<File | null>(null);
const [error, setError] = useState<Error | null>(null);
const styles = useStyles(
INPUT_FILE_FIELD_STYLES,
baseFormControlStyles,
style,
error ? baseInvalidFormControlStyles : baseValidFormControlStyles
);
const savedExternally = !!fileName && state[name] !== '';
const saved = savedExternally || !!state[name];
let description;
if (selected?.name || savedExternally) {
description = selected?.name ?? fileName;
}
if (error?.message) {
description = error.message;
}
if (!description) {
description = translate(saved ? 'ui_processing_saved' : 'ui_no_file_chosen');
}
function updateValue(value: string) {
const val = mapValue?.(value) ?? value;
state[name] = val;
onChange?.(val, name);
context?.change(val, name);
}
function removeFile() {
setSelected(null);
updateValue('');
}
function validateFileSize(size: number) {
const maxFileSizeBytes = maxFileSize * 1000;
if (size > maxFileSizeBytes) {
throw new Error(translate('ui_file_size_exceeds', undefined, {
size: bytesToSize(size),
maxSize: bytesToSize(maxFileSizeBytes),
}));
}
}
async function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file) {
removeFile();
} else {
try {
validateFileSize(file.size);
const value = await blobToData(file);
if (value) {
setSelected(file);
updateValue(value);
setError(null);
}
} catch (exception: any) {
removeFile();
setError(exception);
}
}
}
return styled(styles)(
<field className={className} {...use({ small, medium, large, tiny })}>
<field-label title={labelTooltip}>{children}{required && ' *'}</field-label>
<UploadArea title={tooltip} disabled={disabled} accept={accept} reset onChange={handleChange}>
<Button
icon='/icons/import.svg'
tag='div'
mod={['outlined']}
disabled={disabled}
>
{translate('ui_upload_file')}
</Button>
</UploadArea>
<field-description>
{description}
{(selected || saved) && <IconButton disabled={disabled} name='cross' onClick={removeFile} />}
</field-description>
</field>
);
});
@@ -9,14 +9,17 @@
import { observer } from 'mobx-react-lite';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { removeMetadataFromBase64 } from '@cloudbeaver/core-utils';
import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox';
import { Combobox } from '../../FormControls/Combobox';
import { FormFieldDescription } from '../../FormControls/FormFieldDescription';
import { InputField } from '../../FormControls/InputField';
import { InputFile } from '../../FormControls/InputFile';
import { isControlPresented } from '../../FormControls/isControlPresented';
import { Textarea } from '../../FormControls/Textarea';
import { Link } from '../../Link';
import { useTranslate } from '../../localization/useTranslate';
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
@@ -33,7 +36,7 @@ interface RenderFieldProps {
className?: string;
}
type ControlType = 'checkbox' | 'combobox' | 'link' | 'input' | 'textarea';
type ControlType = 'checkbox' | 'combobox' | 'link' | 'input' | 'textarea' | 'file';
function getControlTypeFor(property: ObjectPropertyInfo): ControlType {
const dataType = property.dataType?.toLowerCase();
@@ -46,6 +49,8 @@ function getControlTypeFor(property: ObjectPropertyInfo): ControlType {
return 'link';
} else if (dataType === 'string' && property.length === 'MULTILINE') {
return 'textarea';
} else if (property.features.includes('file')) {
return 'file';
}
return 'input';
@@ -77,12 +82,32 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
onFocus,
className,
}) {
const translate = useTranslate();
const controltype = getControlTypeFor(property);
const password = property.features.includes('password');
const value = getValue(property.value, controltype);
const defaultValue = getValue(property.defaultValue, controltype);
let description: string | undefined;
const passwordSaved = showRememberTip && password && !!property.value;
const description = passwordSaved ? translate('ui_processing_saved') : undefined;
if (controltype === 'file' && state) {
return (
<InputFile
tooltip={property.description}
labelTooltip={property.displayName || property.description}
name={property.id!}
state={state}
disabled={disabled}
fileName={description}
className={className}
mapValue={removeMetadataFromBase64}
>
{property.displayName}
</InputFile>
);
}
if (controltype === 'link') {
return (
@@ -103,10 +128,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
);
}
if (showRememberTip && password && property.value) {
description = 'Password saved';
}
if (controltype === 'checkbox') {
if (state !== undefined) {
return (
+1
View File
@@ -135,6 +135,7 @@ export * from './FormControls/FormContext';
export * from './FormControls/FormFieldDescription';
export * from './FormControls/FormGroup';
export * from './FormControls/InputField';
export * from './FormControls/InputFile';
export * from './FormControls/Radio';
export * from './FormControls/RadioGroup';
export * from './FormControls/RadioGroupContext';
@@ -86,6 +86,9 @@ export const defaultENLocale = [
['ui_limit', 'Limit'],
['ui_file_size', 'File size'],
['ui_processing_synchronization', 'Synchronization...'],
['ui_no_file_chosen', 'No file chosen'],
['ui_file_size_exceeds', 'Max file size exceeds. File size: {arg:size}. Max file size: {arg:maxSize}'],
['ui_upload_file', 'Upload file'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
@@ -73,6 +73,9 @@ export const defaultITLocale = [
['ui_limit', 'Limit'],
['ui_file_size', 'File size'],
['ui_processing_synchronization', 'Synchronization...'],
['ui_no_file_chosen', 'No file chosen'],
['ui_file_size_exceeds', 'Max file size exceeds. File size: {arg:size}. Max file size: {arg:maxSize}'],
['ui_upload_file', 'Upload file'],
['root_permission_denied', 'Non hai i permessi'],
['app_root_session_expire_warning_title', 'La sessione sta per scadere'],
@@ -84,6 +84,9 @@ export const defaultRULocale = [
['ui_limit', 'Лимит'],
['ui_file_size', 'Размер файла'],
['ui_processing_synchronization', 'Синхронизация...'],
['ui_no_file_chosen', 'Файл не выбран'],
['ui_file_size_exceeds', 'Максимальный размер файла превышен. Размер файла: {arg:size}. Максимальный размер: {arg:maxSize}'],
['ui_upload_file', 'Загрузить файл'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
@@ -86,6 +86,9 @@ export const defaultZHLocale = [
['ui_limit', 'Limit'],
['ui_file_size', 'File size'],
['ui_processing_synchronization', 'Synchronization...'],
['ui_no_file_chosen', 'No file chosen'],
['ui_file_size_exceeds', 'Max file size exceeds. File size: {arg:size}. Max file size: {arg:maxSize}'],
['ui_upload_file', 'Upload file'],
['root_permission_denied', '您没有权限'],
['root_permission_no_permission', '您没有权限执行此操作'],
@@ -1,3 +1,9 @@
query authLogout {
authLogout
query authLogout(
$provider: ID
$configuration: ID
) {
authLogout(
provider: $provider
configuration: $configuration
)
}
+6 -44
View File
@@ -887,7 +887,6 @@ export interface ProjectInfo {
export interface Query {
activeUser?: Maybe<UserInfo>;
allConnections: Array<ConnectionInfo>;
authChangeLocalPassword: Scalars['Boolean'];
authLogin: AuthInfo;
authLogout?: Maybe<Scalars['Boolean']>;
@@ -897,10 +896,6 @@ export interface Query {
configureServer: Scalars['Boolean'];
connectionFolders: Array<ConnectionFolderInfo>;
connectionInfo: ConnectionInfo;
/** @deprecated Field no longer supported */
copyConnectionConfiguration: ConnectionInfo;
/** @deprecated Field no longer supported */
createConnectionConfiguration: ConnectionInfo;
createTeam: AdminTeamInfo;
createUser: AdminUserInfo;
dataTransferAvailableStreamProcessors: Array<DataTransferProcessorInfo>;
@@ -909,8 +904,6 @@ export interface Query {
dataTransferExportDataFromResults: AsyncTaskInfo;
dataTransferRemoveDataFile?: Maybe<Scalars['Boolean']>;
deleteAuthProviderConfiguration: Scalars['Boolean'];
/** @deprecated Field no longer supported */
deleteConnectionConfiguration?: Maybe<Scalars['Boolean']>;
deleteTeam?: Maybe<Scalars['Boolean']>;
deleteUser?: Maybe<Scalars['Boolean']>;
deleteUserMetaParameter: Scalars['Boolean'];
@@ -969,19 +962,11 @@ export interface Query {
sqlParseScript: SqlScriptInfo;
sqlSupportedOperations: Array<DataTypeLogicalOperation>;
templateConnections: Array<ConnectionInfo>;
/** @deprecated Field no longer supported */
updateConnectionConfiguration: ConnectionInfo;
updateTeam: AdminTeamInfo;
userConnections: Array<ConnectionInfo>;
}
export interface QueryAllConnectionsArgs {
id?: InputMaybe<Scalars['ID']>;
projectId: Scalars['ID'];
}
export interface QueryAuthChangeLocalPasswordArgs {
newPassword: Scalars['String'];
oldPassword: Scalars['String'];
@@ -1025,19 +1010,6 @@ export interface QueryConnectionInfoArgs {
}
export interface QueryCopyConnectionConfigurationArgs {
config?: InputMaybe<ConnectionConfig>;
nodePath: Scalars['String'];
projectId: Scalars['ID'];
}
export interface QueryCreateConnectionConfigurationArgs {
config: ConnectionConfig;
projectId: Scalars['ID'];
}
export interface QueryCreateTeamArgs {
description?: InputMaybe<Scalars['String']>;
teamId: Scalars['ID'];
@@ -1079,12 +1051,6 @@ export interface QueryDeleteAuthProviderConfigurationArgs {
}
export interface QueryDeleteConnectionConfigurationArgs {
id: Scalars['ID'];
projectId: Scalars['ID'];
}
export interface QueryDeleteTeamArgs {
teamId: Scalars['ID'];
}
@@ -1372,13 +1338,6 @@ export interface QueryTemplateConnectionsArgs {
}
export interface QueryUpdateConnectionConfigurationArgs {
config: ConnectionConfig;
id: Scalars['ID'];
projectId: Scalars['ID'];
}
export interface QueryUpdateTeamArgs {
description?: InputMaybe<Scalars['String']>;
teamId: Scalars['ID'];
@@ -1711,7 +1670,10 @@ export type AuthLoginQueryVariables = Exact<{
export type AuthLoginQuery = { authInfo: { redirectLink?: string, authId?: string, authStatus: AuthStatus, userTokens?: Array<{ authProvider: string, authConfiguration?: string, loginTime: any, message?: string, origin: { type: string, subType?: string, displayName: string, icon?: string, details?: Array<{ id?: string, displayName?: string, description?: string, category?: string, dataType?: string, defaultValue?: any, validValues?: Array<any>, value?: any, length: ObjectPropertyLength, features: Array<string>, order: number }> } }> } };
export type AuthLogoutQueryVariables = Exact<{ [key: string]: never; }>;
export type AuthLogoutQueryVariables = Exact<{
provider?: InputMaybe<Scalars['ID']>;
configuration?: InputMaybe<Scalars['ID']>;
}>;
export type AuthLogoutQuery = { authLogout?: boolean };
@@ -3178,8 +3140,8 @@ export const AuthLoginDocument = `
}
${AuthTokenFragmentDoc}`;
export const AuthLogoutDocument = `
query authLogout {
authLogout
query authLogout($provider: ID, $configuration: ID) {
authLogout(provider: $provider, configuration: $configuration)
}
`;
export const DeleteAuthProviderConfigurationDocument = `
+1 -1
View File
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export function blobToData(blob: Blob): Promise<string | null> {
export function blobToData(blob: Blob | File): Promise<string | null> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => {
+2 -1
View File
@@ -47,4 +47,5 @@ export * from './debounce';
export * from './svgToDataUri';
export * from './uriToBlob';
export * from './utf8ToBase64';
export * from './createLastPromiseGetter';
export * from './createLastPromiseGetter';
export * from './removeMetadataFromBase64';
@@ -0,0 +1,27 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { removeMetadataFromBase64 } from './removeMetadataFromBase64';
const BASE64 = 'data:application/zip;base64,UEsDBBQACAgIAIB8CVUAAAAAAAAAAAAAAAALAAAAZXdhbGxldC5wZW3Nmceyq0qyhuc8Rc+JExiBgEEP8CC8NzMBEt4j3NNf1trH7D59um/c7slVhCJEVVKUsjK//FP65ZfrxfCirP+N11k7Ml2e+5tpyz7t8n9T+Ohr+hdAk2X+XtEpk/djUZcitcEMR/OcRmM8a4R9yZnBGawCncsdTzsso1WSGXc6voSWMeQpMO3R8lwYheqHxvfostBL22Fpwy29OiJqPLhNgQvd6bgdg7dgPWBkDDOsZrbbZHYVLwPudK1k79VpirZlyROasi04dWTJOr7fzNwbIhoC68nCKqpB4GzRMToanV0M9VcBCm0gsepQGcPZWZOzGCM70Txzy9rPS9P1eafhIG6Jw2my4gEKyWgYBv2IqZfSetjjHgjhG9BQO361HARiEoTf/bIpHpsXPTp6SF/e2mAoGT4/dSkXKJ0sFcTPybY2S0MgjuwjTUwANr0FNmiSqppurQ1SPYhPY6xBrzU4X6HX3hljtwzDkycpSJUIFqznp78LUXKaqy+/DWDAGnXw7aTcWVSR7/K8cH5LGKUzpvW0jfXqjzJW+DdY5620saGli5XG0ibNsIr7A+0AY3a6bJ5YWbDWUWA2Z36C7bRDiOQlchlrpPEKjKWAFK7a7XCa0VPEHcy6BTTfEFZNAki9BOOdeKx4t50aXGVgWLve+VDz5Z6IHk+2TZgGg3nnBsrcxdug7OKWrpqTremQYTlgrrU55QVXCcnHgMESdTMKFgx6zPjYRRM/TxLSbLD2QMxgZxg2RbXCRtRVeePZ5WkQ0Aa0kpy3wb35bI/Xl0Ye+BtpZXRvTnIHUy9PA5txFiN2fSwZ4noz1bGgitJuoWt7T0CcngXEv813HqmFR9JKj+exx3Iv4nlAkroF8wNlVjJkO1hooGg9lJuzzm1KTmllpdMA6PJq9oUEr7zsUqDZIjcVsZei7V9uPGLhiz2Xup9TYezPe0IX2CzuHQiLVWPf2/TZlcBoX9+zaHnbTLLuZsQ5pTs3TQpbxPMDPONEseHJlH5YueYd+vSZ3kXVxKbafZ7Z/HJCQF0gnKJK8Ei2TzEb/Avjl4WQiaisukF85Y3eRNWD3uUH8hJrbgPh/DztLpDKk5dFkQEejjvzxNiLmWQgReFhIDl7+xu8kWPygEaHJN9C1dqPnXCzwlcFthA0t24i7pnXBW2ogErgmuAUt2Ta7vAOIk97lzDNiYVnazSZNJxvZh4jsUgC0NXTFPcErqCsFM71V9LK/hvoY9Q5LOQ2xqxagSoZTU60mrx/F+8TnEvy04rld021haT2K3TibVzBLAjRdMp+AjnVAVUx0r1MHGOeVyJlaXdgn7e4GUBO8pPtwQpYstUPZh3NA0U/NrHj1r3nu2BeD39YIgnwSyxGqnBEEFOv2ed42EyGN+276BRjte5v4+iYVHIR3/PigS3w6bXx5Jw1C9h2Zqy/gOljLXVSqqXz6PeG52M8JX0HjjuuP6T47aOcqGw3vkyz0jTkqnbh11a4UqtPUWqbNwIQb+AAkWwmHjeEb0J+ZBA2KD+HcniVdWIDRjVu5lyOFQX1cLF6l1+61eqh9Qmi+PlpAJFH2S29zej+GtlwLk8iIeh2gSQ+50ZMfXqLHLo80R47Eb9LlulN2GEapa6ecuQ/RAJoOBa8S8LRivfCuMOxdI';
const BASE64_NO_METADATA = 'UEsDBBQACAgIAIB8CVUAAAAAAAAAAAAAAAALAAAAZXdhbGxldC5wZW3Nmceyq0qyhuc8Rc+JExiBgEEP8CC8NzMBEt4j3NNf1trH7D59um/c7slVhCJEVVKUsjK//FP65ZfrxfCirP+N11k7Ml2e+5tpyz7t8n9T+Ohr+hdAk2X+XtEpk/djUZcitcEMR/OcRmM8a4R9yZnBGawCncsdTzsso1WSGXc6voSWMeQpMO3R8lwYheqHxvfostBL22Fpwy29OiJqPLhNgQvd6bgdg7dgPWBkDDOsZrbbZHYVLwPudK1k79VpirZlyROasi04dWTJOr7fzNwbIhoC68nCKqpB4GzRMToanV0M9VcBCm0gsepQGcPZWZOzGCM70Txzy9rPS9P1eafhIG6Jw2my4gEKyWgYBv2IqZfSetjjHgjhG9BQO361HARiEoTf/bIpHpsXPTp6SF/e2mAoGT4/dSkXKJ0sFcTPybY2S0MgjuwjTUwANr0FNmiSqppurQ1SPYhPY6xBrzU4X6HX3hljtwzDkycpSJUIFqznp78LUXKaqy+/DWDAGnXw7aTcWVSR7/K8cH5LGKUzpvW0jfXqjzJW+DdY5620saGli5XG0ibNsIr7A+0AY3a6bJ5YWbDWUWA2Z36C7bRDiOQlchlrpPEKjKWAFK7a7XCa0VPEHcy6BTTfEFZNAki9BOOdeKx4t50aXGVgWLve+VDz5Z6IHk+2TZgGg3nnBsrcxdug7OKWrpqTremQYTlgrrU55QVXCcnHgMESdTMKFgx6zPjYRRM/TxLSbLD2QMxgZxg2RbXCRtRVeePZ5WkQ0Aa0kpy3wb35bI/Xl0Ye+BtpZXRvTnIHUy9PA5txFiN2fSwZ4noz1bGgitJuoWt7T0CcngXEv813HqmFR9JKj+exx3Iv4nlAkroF8wNlVjJkO1hooGg9lJuzzm1KTmllpdMA6PJq9oUEr7zsUqDZIjcVsZei7V9uPGLhiz2Xup9TYezPe0IX2CzuHQiLVWPf2/TZlcBoX9+zaHnbTLLuZsQ5pTs3TQpbxPMDPONEseHJlH5YueYd+vSZ3kXVxKbafZ7Z/HJCQF0gnKJK8Ei2TzEb/Avjl4WQiaisukF85Y3eRNWD3uUH8hJrbgPh/DztLpDKk5dFkQEejjvzxNiLmWQgReFhIDl7+xu8kWPygEaHJN9C1dqPnXCzwlcFthA0t24i7pnXBW2ogErgmuAUt2Ta7vAOIk97lzDNiYVnazSZNJxvZh4jsUgC0NXTFPcErqCsFM71V9LK/hvoY9Q5LOQ2xqxagSoZTU60mrx/F+8TnEvy04rld021haT2K3TibVzBLAjRdMp+AjnVAVUx0r1MHGOeVyJlaXdgn7e4GUBO8pPtwQpYstUPZh3NA0U/NrHj1r3nu2BeD39YIgnwSyxGqnBEEFOv2ed42EyGN+276BRjte5v4+iYVHIR3/PigS3w6bXx5Jw1C9h2Zqy/gOljLXVSqqXz6PeG52M8JX0HjjuuP6T47aOcqGw3vkyz0jTkqnbh11a4UqtPUWqbNwIQb+AAkWwmHjeEb0J+ZBA2KD+HcniVdWIDRjVu5lyOFQX1cLF6l1+61eqh9Qmi+PlpAJFH2S29zej+GtlwLk8iIeh2gSQ+50ZMfXqLHLo80R47Eb9LlulN2GEapa6ecuQ/RAJoOBa8S8LRivfCuMOxdI';
describe('Remove metadata from base64', () => {
test('should return base64 without metadata when base64 with metadata is passed', () => {
expect(removeMetadataFromBase64(BASE64)).toMatch(BASE64_NO_METADATA);
});
test('should return base64 when base64 with no metadata is passed', () => {
expect(removeMetadataFromBase64(BASE64_NO_METADATA)).toMatch(BASE64_NO_METADATA);
});
test('should return empty string when empty string is passed', () => {
expect(removeMetadataFromBase64('')).toMatch('');
});
});
@@ -0,0 +1,17 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 removeMetadataFromBase64(base64: string) {
const parts = base64.split(',');
if (parts[1]) {
return parts[1];
}
return parts[0];
}
@@ -9,7 +9,7 @@
import { observable } from 'mobx';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, RequestedProvider, UserInfoResource } from '@cloudbeaver/core-authentication';
import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, IUserAuthConfiguration, RequestedProvider, UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import type { DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -71,25 +71,30 @@ export class AuthenticationService extends Bootstrap {
await this.auth(false, { providerId, linkUser });
}
async logout(): Promise<void> {
async logout(providerId?: string, configurationId?: string): Promise<void> {
const contexts = await this.onLogout.execute('before');
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
}
if (this.authInfoService.userAuthConfigurations.length > 0) {
const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0];
let userAuthConfiguration: IUserAuthConfiguration | undefined = undefined;
if (userAuthConfiguration.signOutLink) {
this.logoutConfiguration(userAuthConfiguration.id, true);
}
if (providerId) {
userAuthConfiguration = this.authInfoService.userAuthConfigurations
.find(c => c.providerId === providerId && c.configuration.id === configurationId);
} else if (this.authInfoService.userAuthConfigurations.length > 0) {
userAuthConfiguration = this.authInfoService.userAuthConfigurations[0];
}
if (userAuthConfiguration?.configuration.signOutLink) {
this.logoutConfiguration(userAuthConfiguration);
}
try {
await this.userInfoResource.logout();
await this.userInfoResource.logout(providerId, configurationId);
if (!this.administrationScreenService.isConfigurationMode) {
if (!this.administrationScreenService.isConfigurationMode && !providerId) {
this.screenService.navigateToRoot();
}
@@ -99,14 +104,11 @@ export class AuthenticationService extends Bootstrap {
}
}
logoutConfiguration(configurationId: string, full: boolean): void {
const userAuthConfiguration = this.authInfoService.userAuthConfigurations
.find(configuration => configuration.id === configurationId);
if (userAuthConfiguration?.signOutLink) {
const id = `${userAuthConfiguration.id}-sign-out`;
private async logoutConfiguration(configuration: IUserAuthConfiguration): Promise<void> {
if (configuration.configuration.signOutLink) {
const id = `${configuration.configuration.id}-sign-out`;
const popup = this.windowsService.open(id, {
url: userAuthConfiguration.signOutLink,
url: configuration.configuration.signOutLink,
target: id,
width: 600,
height: 700,
@@ -116,18 +118,6 @@ export class AuthenticationService extends Bootstrap {
popup.blur();
window.focus();
}
if (!full) {
let maxTime = 1000 / 100 * 10;
const interval = setInterval(() => {
if (popup?.location.href !== userAuthConfiguration.signOutLink || maxTime === 0) {
this.userInfoResource.markOutdated();
clearInterval(interval);
}
maxTime--;
}, 100);
}
}
}
@@ -399,8 +399,8 @@ export class ConnectionOptionsTabService extends Bootstrap {
const value = credentials[property.id!];
if (property.features.includes('password')) {
if (value) {
return true;
if (value !== undefined) {
return property.features.includes('file') ? true : !!value;
}
} else if (value !== property.value) {
return true;
@@ -49,6 +49,7 @@ export const DBAuthDialogFooter = observer<React.PropsWithChildren<Props>>(funct
type="button"
mod={['unelevated']}
disabled={isAuthenticating}
loading={isAuthenticating}
onClick={onLogin}
>
{translate('authentication_login')}
@@ -12,7 +12,6 @@ import styled, { css } from 'reshadow';
import { useAdministrationSettings } from '@cloudbeaver/core-administration';
import {
SubmittingForm,
Loader,
useFocus,
ErrorMessage,
useStyles,
@@ -87,21 +86,21 @@ export const DatabaseAuthDialog: DialogComponent<Payload> = observer(function Da
)}
onReject={options?.persistent ? undefined : rejectDialog}
>
{(!connection.isLoaded() || connection.isLoading() || !controller.configured)
? <Loader />
: (
<SubmittingForm ref={focusedRef} onSubmit={controller.login}>
<ConnectionAuthenticationForm
config={controller.config}
authModelId={authModelId}
authProperties={connection.connectionInfo?.authProperties}
networkHandlers={payload.networkHandlers}
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
disabled={controller.isAuthenticating}
/>
</SubmittingForm>
)}
<SubmittingForm
ref={focusedRef}
disabled={!connection.isLoaded() || connection.isLoading() || !controller.configured}
onSubmit={controller.login}
>
<ConnectionAuthenticationForm
config={controller.config}
authModelId={authModelId}
authProperties={connection.connectionInfo?.authProperties}
networkHandlers={payload.networkHandlers}
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
allowSaveCredentials={credentialsSavingEnabled}
disabled={controller.isAuthenticating}
/>
</SubmittingForm>
</CommonDialogWrapper>
);
});
@@ -95,10 +95,10 @@ export const AuthTokenList = observer<Props>(function AuthTokenList({ user, clas
>
<Cell
before={icon ? <IconOrImage icon={icon} /> : undefined}
after={configuration && (
after={(
<Button
mod={['outlined']}
onClick={() => authenticationService.logoutConfiguration(configuration!.id, false)}
onClick={() => authenticationService.logout(provider.id, configuration?.id)}
>
{translate('authentication_logout')}
</Button>