mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
+19
-3
@@ -2,6 +2,7 @@ package io.cloudbeaver.server.servlets;
|
||||
|
||||
import io.cloudbeaver.DBWConstants;
|
||||
import io.cloudbeaver.auth.SMWAuthProviderFederated;
|
||||
import io.cloudbeaver.model.session.WebActionParameters;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.registry.WebHandlerRegistry;
|
||||
import io.cloudbeaver.registry.WebServletHandlerDescriptor;
|
||||
@@ -16,6 +17,7 @@ import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthProvider;
|
||||
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
|
||||
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
|
||||
@@ -29,8 +31,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
|
||||
@WebServlet(urlPatterns = "/")
|
||||
public class CBStaticServlet extends DefaultServlet {
|
||||
@@ -99,14 +101,28 @@ public class CBStaticServlet extends DefaultServlet {
|
||||
WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(
|
||||
request, response, false);
|
||||
if (webSession.getUser() == null) {
|
||||
String signInLink = ((SMWAuthProviderFederated) authProviderInstance).getSignInLink(
|
||||
activeAuthConfig.getId(), Collections.emptyMap());
|
||||
var securityController = webSession.getSecurityController();
|
||||
SMAuthInfo authInfo = securityController.authenticate(
|
||||
webSession.getSessionId(),
|
||||
null,
|
||||
webSession.getSessionParameters(),
|
||||
WebSession.CB_SESSION_TYPE,
|
||||
authProvider.getId(),
|
||||
activeAuthConfig.getId(),
|
||||
Map.of()
|
||||
);
|
||||
String signInLink = authInfo.getRedirectUrl();
|
||||
//ignore current routing if non-root page is open
|
||||
if (!signInLink.endsWith("#")) {
|
||||
signInLink += "#";
|
||||
}
|
||||
if (!CommonUtils.isEmpty(signInLink)) {
|
||||
// Redirect to it
|
||||
Map<String, Object> authActionParams = Map.of(
|
||||
"action", "auto-login",
|
||||
"auth-id", authInfo.getAuthAttemptId()
|
||||
);
|
||||
WebActionParameters.saveToSession(webSession, authActionParams);
|
||||
request.getSession().setAttribute(DBWConstants.STATE_ATTR_SIGN_IN_STATE, DBWConstants.SignInState.GLOBAL);
|
||||
response.sendRedirect(signInLink);
|
||||
return true;
|
||||
|
||||
+2
@@ -106,6 +106,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus());
|
||||
case ERROR:
|
||||
throw new DBWebException(smAuthInfo.getError());
|
||||
case EXPIRED:
|
||||
throw new DBException("Authorization has already been processed");
|
||||
default:
|
||||
throw new DBWebException("Unknown auth status:" + smAuthInfo.getAuthStatus());
|
||||
}
|
||||
|
||||
+6
-1
@@ -1067,13 +1067,18 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
return SMAuthInfo.inProgress(authId, redirectUrl, authData);
|
||||
case ERROR:
|
||||
return SMAuthInfo.error(authId, authError);
|
||||
case EXPIRED:
|
||||
return SMAuthInfo.expired(authId);
|
||||
default:
|
||||
throw new SMException("Unknown auth status:" + smAuthStatus);
|
||||
}
|
||||
}
|
||||
|
||||
String smToken = findTokenBySmSession(smSessionId);
|
||||
return SMAuthInfo.success(authId, smToken, getTokenPermissions(smToken), authData);
|
||||
SMAuthPermissions authPermissions = getTokenPermissions(smToken);
|
||||
var successAuthStatus = SMAuthInfo.success(authId, smToken, authPermissions, authData);
|
||||
updateAuthStatus(authId, SMAuthStatus.EXPIRED, authData, null, authPermissions.getSessionId());
|
||||
return successAuthStatus;
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new DBException("Error while read auth info", e);
|
||||
|
||||
@@ -31,6 +31,9 @@ const tabsStyles = css`
|
||||
`;
|
||||
|
||||
const administrationStyles = css`
|
||||
SlideElement {
|
||||
composes: theme-background-background theme-text-on-background from global;
|
||||
}
|
||||
container {
|
||||
composes: theme-background-secondary theme-text-on-secondary from global;
|
||||
display: flex;
|
||||
|
||||
@@ -32,6 +32,12 @@ const bodyStyles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const loaderStyle = css`
|
||||
ExceptionMessage {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Body = observer(function Body() {
|
||||
useAppLoadingScreen();
|
||||
const themeService = useService(ThemeService);
|
||||
@@ -53,7 +59,7 @@ export const Body = observer(function Body() {
|
||||
return styled(style)(
|
||||
<DNDProvider>
|
||||
<theme ref={ref} className={`theme-${themeService.currentTheme.id}`}>
|
||||
<Loader state={permissionsService}>{() => styled(style)(
|
||||
<Loader state={permissionsService} style={loaderStyle}>{() => styled(style)(
|
||||
<>
|
||||
{Screen && <Screen {...screenService.routerService.params} />}
|
||||
</>
|
||||
|
||||
@@ -26,8 +26,8 @@ export class AppAuthService extends Bootstrap {
|
||||
readonly auth: IExecutor<boolean>;
|
||||
|
||||
constructor(
|
||||
private serverService: ServerService,
|
||||
private userInfoResource: UserInfoResource,
|
||||
private readonly serverService: ServerService,
|
||||
private readonly userInfoResource: UserInfoResource,
|
||||
) {
|
||||
super();
|
||||
this.auth = new Executor();
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
*/
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { type ITask, AutoRunningTask } from '@cloudbeaver/core-executor';
|
||||
import { AuthInfo, AuthProviderConfiguration, AuthStatus, UserInfo } from '@cloudbeaver/core-sdk';
|
||||
import { WindowsService } from '@cloudbeaver/core-ui';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { AuthProvidersResource } from './AuthProvidersResource';
|
||||
import { type ILoginOptions, UserInfoResource } from './UserInfoResource';
|
||||
@@ -55,23 +57,21 @@ export class AuthInfoService {
|
||||
) {
|
||||
}
|
||||
|
||||
async login(providerId: string, options: ILoginOptions): Promise<UserInfo | null> {
|
||||
const authInfo = await this.userInfoResource.login(providerId, options);
|
||||
|
||||
await this.federatedAuthentication(providerId, options, authInfo);
|
||||
|
||||
return this.userInfoResource.data;
|
||||
login(providerId: string, options: ILoginOptions): ITask<UserInfo | null> {
|
||||
return new AutoRunningTask(async () => await this.userInfoResource.login(providerId, options))
|
||||
.then(authInfo => this.federatedAuthentication(providerId, options, authInfo));
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.userInfoResource.logout();
|
||||
}
|
||||
|
||||
private async federatedAuthentication(
|
||||
private federatedAuthentication(
|
||||
providerId: string,
|
||||
options: ILoginOptions,
|
||||
{ redirectLink, authId, authStatus }: AuthInfo
|
||||
): Promise<void> {
|
||||
): ITask<UserInfo | null> {
|
||||
let window: Window | null = null;
|
||||
let id = providerId;
|
||||
|
||||
if (options.configurationId) {
|
||||
@@ -83,7 +83,8 @@ export class AuthInfoService {
|
||||
}
|
||||
|
||||
if (redirectLink) {
|
||||
const window = this.windowsService.open(id, {
|
||||
id = uuid();
|
||||
window = this.windowsService.open(id, {
|
||||
url: redirectLink,
|
||||
target: id,
|
||||
width: 600,
|
||||
@@ -95,8 +96,16 @@ export class AuthInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
if (authId && authStatus === AuthStatus.InProgress) {
|
||||
await this.userInfoResource.finishFederatedAuthentication(authId, options.linkUser);
|
||||
}
|
||||
return new AutoRunningTask(() => {
|
||||
if (authId && authStatus === AuthStatus.InProgress) {
|
||||
return this.userInfoResource.finishFederatedAuthentication(authId, options.linkUser);
|
||||
}
|
||||
|
||||
return AutoRunningTask.resolve(this.userInfoResource.data);
|
||||
}, () => {
|
||||
if (window) {
|
||||
this.windowsService.close(window);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { computed, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { SyncExecutor, ISyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { SyncExecutor, ISyncExecutor, ITask, AutoRunningTask, whileTask } from '@cloudbeaver/core-executor';
|
||||
import { SessionResource } from '@cloudbeaver/core-root';
|
||||
import { AuthInfo, AuthStatus, CachedDataResource, GetActiveUserQueryVariables, GraphQLService, isResourceKeyList, ObjectOrigin, ResourceKey, UserAuthToken, UserInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
@@ -121,41 +121,51 @@ UserInfoIncludes
|
||||
});
|
||||
}
|
||||
|
||||
async finishFederatedAuthentication(authId: string, link?: boolean): Promise<void> {
|
||||
return await this.performUpdate(undefined, [], async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
finishFederatedAuthentication(authId: string, link?: boolean): ITask<UserInfo | null> {
|
||||
let activeTask: ITask<AuthInfo> | undefined;
|
||||
|
||||
return new AutoRunningTask<UserInfo | null>(() => this.performUpdate(
|
||||
undefined,
|
||||
[],
|
||||
async () => {
|
||||
activeTask = whileTask<AuthInfo>(
|
||||
authInfo => {
|
||||
if (authInfo.authStatus === AuthStatus.Success) {
|
||||
return true;
|
||||
} else if (authInfo.authStatus === AuthStatus.Error) {
|
||||
throw new Error('Authentication error');
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async () => {
|
||||
const { authInfo } = await this.graphQLService.sdk.getAuthStatus({
|
||||
authId,
|
||||
linkUser: link,
|
||||
customIncludeOriginDetails: true,
|
||||
});
|
||||
return authInfo as AuthInfo;
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
|
||||
if (this.data === null) {
|
||||
this.resetIncludes();
|
||||
this.setData(await this.loader());
|
||||
} else {
|
||||
this.data.authTokens.push(...authInfo.userTokens as UserAuthToken[]);
|
||||
}
|
||||
const authInfo = await activeTask;
|
||||
|
||||
this.sessionResource.markOutdated();
|
||||
}
|
||||
|
||||
if (authInfo.authStatus === AuthStatus.Success) {
|
||||
resolve();
|
||||
clearInterval(interval);
|
||||
} else if (authInfo.authStatus === AuthStatus.Error) {
|
||||
reject(new Error('Authentication error'));
|
||||
clearInterval(interval);
|
||||
}
|
||||
} catch (exception: any) {
|
||||
reject(exception);
|
||||
clearInterval(interval);
|
||||
if (authInfo.userTokens && authInfo.authStatus === AuthStatus.Success) {
|
||||
if (this.data === null) {
|
||||
this.resetIncludes();
|
||||
this.setData(await this.loader());
|
||||
} else {
|
||||
this.data.authTokens.push(...authInfo.userTokens as UserAuthToken[]);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
this.sessionResource.markOutdated();
|
||||
}
|
||||
|
||||
return this.data;
|
||||
}
|
||||
), () => {
|
||||
activeTask?.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({
|
||||
<error-name><span>{name || error.details?.name}</span></error-name>
|
||||
<error-message>{message || error.details?.message}</error-message>
|
||||
<error-actions>
|
||||
{exception && (
|
||||
{exception && error.details?.hasDetails && (
|
||||
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
export interface ILoadableState {
|
||||
isLoading: () => boolean;
|
||||
isLoaded: () => boolean;
|
||||
exception?: Error[] | Error | null;
|
||||
readonly exception?: Error[] | Error | null;
|
||||
reload?: () => void;
|
||||
|
||||
isCancelled?: () => boolean;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
@@ -18,16 +18,10 @@ import { uuid } from '@cloudbeaver/core-utils';
|
||||
import { Button } from '../Button';
|
||||
import { ExceptionMessage } from '../ExceptionMessage';
|
||||
import { StaticImage } from '../StaticImage';
|
||||
import type { ILoadableState } from './ILoadableState';
|
||||
import { ILoaderContext, LoaderContext } from './LoaderContext';
|
||||
import { loaderStyles, overlayStyles } from './loaderStyles';
|
||||
|
||||
export interface ILoadableState {
|
||||
isLoading: () => boolean;
|
||||
isLoaded: () => boolean;
|
||||
readonly exception?: Error[] | Error | null;
|
||||
reload?: () => void;
|
||||
}
|
||||
|
||||
type LoaderState = ILoadableState | {
|
||||
loading: boolean;
|
||||
};
|
||||
@@ -41,6 +35,8 @@ interface Props {
|
||||
message?: string;
|
||||
/** hides message */
|
||||
hideMessage?: boolean;
|
||||
/** hides error message */
|
||||
hideException?: boolean;
|
||||
/** render loader as overlay with white spinner */
|
||||
overlay?: boolean;
|
||||
/** loader with white spinner */
|
||||
@@ -69,6 +65,7 @@ export const Loader = observer<Props>(function Loader({
|
||||
overlay,
|
||||
message,
|
||||
hideMessage,
|
||||
hideException,
|
||||
secondary,
|
||||
small,
|
||||
inline,
|
||||
@@ -101,6 +98,16 @@ export const Loader = observer<Props>(function Loader({
|
||||
) {
|
||||
loaded = element.isLoaded();
|
||||
loading = element.isLoading();
|
||||
|
||||
if (loading) {
|
||||
if (element.cancel) {
|
||||
onCancel = element.cancel;
|
||||
}
|
||||
|
||||
if (element.isCancelled) {
|
||||
cancelDisabled = element.isCancelled();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loading = element.loading;
|
||||
loaded = !loading;
|
||||
@@ -168,7 +175,7 @@ export const Loader = observer<Props>(function Loader({
|
||||
}
|
||||
});
|
||||
|
||||
if (exception && !loading) {
|
||||
if (exception && !loading && !hideException) {
|
||||
return styled(style)(
|
||||
<ExceptionMessage
|
||||
exception={exception}
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from './layout/BASE_LAYOUT_GRID_STYLES';
|
||||
export * from './layout/TopAppBar';
|
||||
|
||||
export * from './Loader/Loader';
|
||||
export * from './Loader/ILoadableState';
|
||||
|
||||
export * from './Marks/ConnectionMark';
|
||||
|
||||
@@ -164,6 +165,7 @@ export * from './useMapResource';
|
||||
export * from './useMouse';
|
||||
export * from './useObjectRef';
|
||||
export * from './useObservableRef';
|
||||
export * from './usePromiseState';
|
||||
export * from './useStateDelay';
|
||||
export * from './useErrorDetails';
|
||||
export * from './useAppLoadingScreen';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { CachedDataResource, CachedDataResourceContext, CachedDataResourceGetter, CachedDataResourceParam, CachedResourceData, CachedResourceIncludeArgs, isResourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { ILoadableState } from './Loader/Loader';
|
||||
import type { ILoadableState } from './Loader/ILoadableState';
|
||||
import { useObjectRef } from './useObjectRef';
|
||||
|
||||
interface KeyWithIncludes<TKey, TIncludes> {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { ILoadableState } from './Loader/Loader';
|
||||
import type { ILoadableState } from './Loader/ILoadableState';
|
||||
import { useObjectRef } from './useObjectRef';
|
||||
import { useObservableRef } from './useObservableRef';
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { computed, observable } from 'mobx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Task } from '@cloudbeaver/core-executor';
|
||||
|
||||
import type { ILoadableState } from './Loader/ILoadableState';
|
||||
import { useObservableRef } from './useObservableRef';
|
||||
|
||||
|
||||
export function usePromiseState<T>(promise: Promise<T> | null): ILoadableState {
|
||||
const state = useObservableRef(() => ({
|
||||
exception: null,
|
||||
loading: true,
|
||||
get cancel() {
|
||||
if (this.promise instanceof Task && this.promise.cancellable) {
|
||||
return this.promise.cancel.bind(this.promise);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
isCancelled(): boolean {
|
||||
if (this.promise instanceof Task) {
|
||||
return this.promise.cancelled;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isLoading(): boolean {
|
||||
if (this.promise instanceof Task) {
|
||||
return this.promise.executing;
|
||||
}
|
||||
return this.promise !== null && this.loading;
|
||||
},
|
||||
isLoaded(): boolean {
|
||||
return this.promise === null;
|
||||
},
|
||||
}), {
|
||||
cancel: computed,
|
||||
promise: observable.ref,
|
||||
exception: observable.ref,
|
||||
loading: observable.ref,
|
||||
}, { promise });
|
||||
|
||||
useEffect(() => {
|
||||
if (promise) {
|
||||
state.loading = true;
|
||||
state.exception = null;
|
||||
|
||||
promise
|
||||
.catch(exception => {
|
||||
if (promise === state.promise) {
|
||||
state.exception = exception;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (promise === state.promise) {
|
||||
state.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [promise]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export enum ENotificationType {
|
||||
Error = 'Error',
|
||||
Success = 'Success',
|
||||
Loading = 'Loading',
|
||||
Custom =' Custom'
|
||||
Custom = ' Custom'
|
||||
}
|
||||
|
||||
export interface INotificationExtraProps<T = never> {
|
||||
@@ -53,7 +53,7 @@ export interface INotification<TProps extends INotificationExtraProps<any> = INo
|
||||
title: string;
|
||||
message?: string;
|
||||
timestamp: number;
|
||||
details?: string | Error;
|
||||
details?: string | Error | null;
|
||||
persistent?: boolean;
|
||||
state: { deleteDelay: number };
|
||||
isSilent: boolean;
|
||||
@@ -67,7 +67,7 @@ export interface INotificationOptions<TProps extends INotificationExtraProps<any
|
||||
title: string;
|
||||
uuid?: string;
|
||||
message?: string;
|
||||
details?: string | Error;
|
||||
details?: string | Error | null;
|
||||
isSilent?: boolean;
|
||||
persistent?: boolean;
|
||||
extraProps?: TProps;
|
||||
|
||||
@@ -39,7 +39,7 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private settings: EventsSettingsService
|
||||
private readonly settings: EventsSettingsService
|
||||
) {
|
||||
this.notificationList = new OrderedMap<number, INotification<any>>(({ id }) => id);
|
||||
this.closeTask = new Executor();
|
||||
@@ -147,7 +147,12 @@ export class NotificationService {
|
||||
return this.notify(notification, ENotificationType.Error);
|
||||
}
|
||||
|
||||
logException(exception: Error | GQLError, title?: string, message?: string, silent?: boolean): void {
|
||||
logException(
|
||||
exception: Error | GQLError | undefined | null,
|
||||
title?: string,
|
||||
message?: string,
|
||||
silent?: boolean
|
||||
): void {
|
||||
const errorDetails = getErrorDetails(exception);
|
||||
|
||||
if (!silent) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 type { ITask } from './ITask';
|
||||
import { Task } from './Task';
|
||||
|
||||
export class AutoRunningTask<TValue> extends Task<TValue> {
|
||||
constructor(
|
||||
task: () => Promise<TValue>,
|
||||
externalCancel?: () => Promise<void> | void
|
||||
) {
|
||||
super(task, externalCancel);
|
||||
this.run();
|
||||
}
|
||||
|
||||
static resolve<TValue>(value: TValue): ITask<TValue> {
|
||||
return new AutoRunningTask(() => Promise.resolve(value));
|
||||
}
|
||||
|
||||
static reject<TValue>(exception: any): ITask<TValue> {
|
||||
return new AutoRunningTask(() => Promise.reject(exception));
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export class Task<TValue> implements ITask<TValue> {
|
||||
|
||||
private resolve!: (value: TValue) => void;
|
||||
private reject!: (reason?: any) => void;
|
||||
private innerPromise: Promise<TValue>;
|
||||
private readonly innerPromise: Promise<TValue>;
|
||||
private sourcePromise: Promise<TValue> | null;
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
@@ -44,7 +44,7 @@ export class Task<TValue> implements ITask<TValue> {
|
||||
|
||||
constructor(
|
||||
readonly task: () => Promise<TValue>,
|
||||
private externalCancel?: () => Promise<void> | void
|
||||
private readonly externalCancel?: () => Promise<void> | void
|
||||
) {
|
||||
this.innerPromise = new Promise((resolve, reject) => {
|
||||
this.reject = reject;
|
||||
@@ -66,17 +66,24 @@ export class Task<TValue> implements ITask<TValue> {
|
||||
onfulfilled?: ((value: TValue) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): ITask<TResult1 | TResult2> {
|
||||
let cancel = this.cancel.bind(this);
|
||||
|
||||
return new Task(async () => {
|
||||
try {
|
||||
const value = await this.innerPromise;
|
||||
return await onfulfilled?.(value) as TResult1;
|
||||
} catch (e: any) {
|
||||
if (onrejected) {
|
||||
return await onrejected(e);
|
||||
}
|
||||
throw e;
|
||||
const value = await this.innerPromise;
|
||||
|
||||
const task = onfulfilled?.(value);
|
||||
|
||||
if (task instanceof Task) {
|
||||
cancel = async () => {
|
||||
await task.cancel();
|
||||
await this.cancel();
|
||||
};
|
||||
}
|
||||
}, () => this.cancel()).run();
|
||||
|
||||
return await task as TResult1;
|
||||
}, () => cancel())
|
||||
.run()
|
||||
.catch(onrejected);
|
||||
}
|
||||
|
||||
catch<TResult = never>(
|
||||
@@ -127,7 +134,7 @@ export class Task<TValue> implements ITask<TValue> {
|
||||
return this;
|
||||
}
|
||||
|
||||
cancel(): Promise<void> | void {
|
||||
async cancel(): Promise<void> {
|
||||
if (this.cancelled) {
|
||||
return;
|
||||
}
|
||||
@@ -140,11 +147,11 @@ export class Task<TValue> implements ITask<TValue> {
|
||||
}
|
||||
|
||||
if (this.externalCancel) {
|
||||
return this.externalCancel();
|
||||
await this.externalCancel();
|
||||
}
|
||||
|
||||
if (this.sourcePromise instanceof Task) {
|
||||
return this.sourcePromise.cancel();
|
||||
await this.sourcePromise.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './TaskScheduler/AutoRunningTask';
|
||||
export * from './TaskScheduler/ITask';
|
||||
export * from './TaskScheduler/Task';
|
||||
export * from './TaskScheduler/TaskScheduler';
|
||||
export * from './ExecutionContext';
|
||||
export * from './Executor';
|
||||
@@ -10,3 +12,4 @@ export * from './IExecutorHandler';
|
||||
export * from './IExecutorHandlersCollection';
|
||||
export * from './ISyncExecutor';
|
||||
export * from './SyncExecutor';
|
||||
export * from './whileTask';
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { AutoRunningTask } from './TaskScheduler/AutoRunningTask';
|
||||
import type { ITask } from './TaskScheduler/ITask';
|
||||
import { Task } from './TaskScheduler/Task';
|
||||
|
||||
export function whileTask<T>(
|
||||
callback: (value: T) => Promise<boolean> | boolean,
|
||||
task: () => Promise<T>,
|
||||
interval: number,
|
||||
cancelMessage?: string
|
||||
): ITask<T> {
|
||||
let resolve: (value: T | PromiseLike<T>) => void;
|
||||
let reject: (reason?: any) => void;
|
||||
|
||||
const lockPromise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
let timeoutId: NodeJS.Timeout | null;
|
||||
let activeTask: Promise<T> | null;
|
||||
let stopped = false;
|
||||
|
||||
function stop() {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
async function cancelTask(exception?: any) {
|
||||
stop();
|
||||
|
||||
if (activeTask instanceof Task) {
|
||||
await activeTask.cancel();
|
||||
}
|
||||
|
||||
reject(exception);
|
||||
}
|
||||
|
||||
function runTask() {
|
||||
activeTask = task();
|
||||
activeTask
|
||||
.finally(() => {
|
||||
activeTask = null;
|
||||
})
|
||||
.then(async value => {
|
||||
const state = await callback(value);
|
||||
|
||||
if (state) {
|
||||
resolve(value);
|
||||
} else if (!stopped) {
|
||||
timeoutId = setTimeout(runTask, interval);
|
||||
}
|
||||
})
|
||||
.catch(cancelTask);
|
||||
}
|
||||
|
||||
return new AutoRunningTask(
|
||||
() => {
|
||||
runTask();
|
||||
return lockPromise;
|
||||
},
|
||||
() => cancelTask(new Error(cancelMessage ?? 'Task was cancelled'))
|
||||
);
|
||||
}
|
||||
@@ -16,15 +16,15 @@ export interface IErrorDetails {
|
||||
errorType?: ServerErrorType;
|
||||
}
|
||||
|
||||
export function hasDetails(error: Error): error is DetailsError {
|
||||
export function hasDetails(error: Error | undefined | null): error is DetailsError {
|
||||
return error instanceof DetailsError && error.hasDetails();
|
||||
}
|
||||
|
||||
export function getErrorDetails(error: Error | DetailsError): IErrorDetails {
|
||||
const exceptionMessage = hasDetails(error) ? error.errorMessage : error.message || error.name;
|
||||
export function getErrorDetails(error: Error | DetailsError | undefined | null): IErrorDetails {
|
||||
const exceptionMessage = (hasDetails(error) ? error.errorMessage : error?.message) || error?.name || 'Unknown error';
|
||||
|
||||
const details: IErrorDetails = {
|
||||
name: error.name,
|
||||
name: error?.name ?? 'Error',
|
||||
message: exceptionMessage,
|
||||
hasDetails: hasDetails(error),
|
||||
};
|
||||
|
||||
@@ -60,6 +60,13 @@ export class WindowsService {
|
||||
return null;
|
||||
}
|
||||
|
||||
close(window: Window) {
|
||||
const current = Array.from(this.windows.values())
|
||||
.find(info => info.window === window);
|
||||
|
||||
current?.window.close();
|
||||
}
|
||||
|
||||
async waitWindowsClose(window: Window): Promise<void> {
|
||||
const current = Array.from(this.windows.values())
|
||||
.find(info => info.window === window);
|
||||
@@ -81,6 +88,7 @@ export class WindowsService {
|
||||
|
||||
if (this.windows.size === 0) {
|
||||
clearInterval(this.trackWindowClose);
|
||||
this.trackWindowClose = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observable } from 'mobx';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
|
||||
@@ -18,6 +20,7 @@ import type { ObjectOrigin } from '@cloudbeaver/core-sdk';
|
||||
import { WindowsService } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { AuthDialogService } from './Dialog/AuthDialogService';
|
||||
import type { IAuthOptions } from './IAuthOptions';
|
||||
|
||||
export type LogoutEventType = 'before' | 'after';
|
||||
|
||||
@@ -125,12 +128,17 @@ export class AuthenticationService extends Bootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private async auth(persistent: boolean, provider: string | null = null, link?: boolean) {
|
||||
private async auth(persistent: boolean, providerId: string | null = null, link?: boolean) {
|
||||
if (this.authPromise) {
|
||||
return this.authPromise;
|
||||
}
|
||||
|
||||
this.authPromise = this.authDialogService.showLoginForm(persistent, provider, link);
|
||||
const loginFormOptions: IAuthOptions = observable({
|
||||
providerId,
|
||||
link,
|
||||
});
|
||||
|
||||
this.authPromise = this.authDialogService.showLoginForm(persistent, loginFormOptions);
|
||||
|
||||
if (this.serverConfigResource.redirectOnFederatedAuth) {
|
||||
await this.authProvidersResource.loadAll();
|
||||
@@ -143,6 +151,10 @@ export class AuthenticationService extends Bootstrap {
|
||||
|
||||
if (configurableProvider?.configurations?.length === 1) {
|
||||
const configuration = configurableProvider.configurations[0];
|
||||
|
||||
loginFormOptions.providerId = configurableProvider.id;
|
||||
loginFormOptions.configurationId = configuration.id;
|
||||
|
||||
const user = await this.authInfoService.login(configurableProvider.id, {
|
||||
configurationId: configuration.id,
|
||||
linkUser: link,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useStyles } from '@cloudbeaver/core-theming';
|
||||
import { TabsState, TabList, Tab, TabTitle, UNDERLINE_TAB_STYLES, BASE_TAB_STYLES } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { AuthenticationService } from '../AuthenticationService';
|
||||
import type { IAuthOptions } from '../IAuthOptions';
|
||||
import { AuthDialogFooter } from './AuthDialogFooter';
|
||||
import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
|
||||
import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList';
|
||||
@@ -62,41 +63,44 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
interface IAuthPayload {
|
||||
providerId: string | null;
|
||||
link?: boolean;
|
||||
}
|
||||
|
||||
export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function AuthDialog({
|
||||
export const AuthDialog: DialogComponent<IAuthOptions, null> = observer(function AuthDialog({
|
||||
payload: {
|
||||
providerId,
|
||||
configurationId,
|
||||
link = false,
|
||||
},
|
||||
options,
|
||||
rejectDialog,
|
||||
}) {
|
||||
const state = useAuthDialogState(providerId);
|
||||
const errorDetails = useErrorDetails(state.exception);
|
||||
const dialogData = useAuthDialogState(providerId, configurationId);
|
||||
const errorDetails = useErrorDetails(dialogData.exception);
|
||||
const authenticationService = useService(AuthenticationService);
|
||||
const userInfo = useService(UserInfoResource);
|
||||
const translate = useTranslate();
|
||||
const state = dialogData.state;
|
||||
|
||||
const additional = userInfo.data !== null
|
||||
&& state.activeProvider?.id !== undefined
|
||||
&& !userInfo.hasToken(state.activeProvider.id);
|
||||
|
||||
const showTabs = (state.providers.length + state.configurations.length) > 1;
|
||||
const showTabs = (dialogData.providers.length + dialogData.configurations.length) > 1;
|
||||
const federate = state.tabId === FEDERATED_AUTH;
|
||||
|
||||
let dialogTitle = translate('authentication_login_dialog_title');
|
||||
let subTitle: string | undefined;
|
||||
let icon: string | undefined;
|
||||
|
||||
if (state.activeProvider) {
|
||||
dialogTitle += `: ${state.activeProvider.label}`;
|
||||
subTitle = state.activeProvider.description;
|
||||
}
|
||||
icon = state.activeProvider.icon;
|
||||
|
||||
if (federate) {
|
||||
if (state.activeConfiguration) {
|
||||
dialogTitle += `: ${state.activeConfiguration.displayName}`;
|
||||
subTitle = state.activeConfiguration.description;
|
||||
icon = state.activeConfiguration.iconURL || icon;
|
||||
}
|
||||
} else if (federate) {
|
||||
dialogTitle += `: ${translate('authentication_auth_federated')}`;
|
||||
subTitle = 'authentication_identity_provider_dialog_subtitle';
|
||||
}
|
||||
@@ -106,7 +110,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
}
|
||||
|
||||
async function login() {
|
||||
await state.login(link);
|
||||
await dialogData.login(link);
|
||||
rejectDialog();
|
||||
}
|
||||
|
||||
@@ -120,7 +124,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
return <TextPlaceholder>{translate('authentication_select_provider')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
if (state.configure) {
|
||||
if (dialogData.configure) {
|
||||
return (
|
||||
<TextPlaceholder>
|
||||
{translate('authentication_provider_disabled')}
|
||||
@@ -137,7 +141,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
<AuthProviderForm
|
||||
provider={provider}
|
||||
credentials={state.credentials}
|
||||
authenticate={state.authenticating}
|
||||
authenticate={dialogData.authenticating}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -147,15 +151,15 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
<CommonDialogWrapper
|
||||
size='large'
|
||||
title={dialogTitle}
|
||||
icon={state.activeProvider?.icon}
|
||||
icon={icon}
|
||||
subTitle={subTitle}
|
||||
footer={!federate && (
|
||||
<AuthDialogFooter
|
||||
authAvailable={!state.configure}
|
||||
isAuthenticating={state.authenticating}
|
||||
authAvailable={!dialogData.configure}
|
||||
isAuthenticating={dialogData.authenticating}
|
||||
onLogin={login}
|
||||
>
|
||||
{state.exception && (
|
||||
{dialogData.exception && (
|
||||
<ErrorMessage
|
||||
text={errorDetails.details?.message || ''}
|
||||
hasDetails={errorDetails.details?.hasDetails}
|
||||
@@ -169,23 +173,23 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
>
|
||||
{showTabs && (
|
||||
<TabList aria-label='Auth providers'>
|
||||
{state.providers.map(provider => (
|
||||
{dialogData.providers.map(provider => (
|
||||
<Tab
|
||||
key={provider.id}
|
||||
tabId={provider.id}
|
||||
title={provider.description || provider.label}
|
||||
disabled={state.authenticating}
|
||||
disabled={dialogData.authenticating}
|
||||
onClick={() => { state.setActiveProvider(provider); }}
|
||||
>
|
||||
<TabTitle>{provider.label}</TabTitle>
|
||||
</Tab>
|
||||
))}
|
||||
{state.configurations.length > 0 && (
|
||||
{dialogData.configurations.length > 0 && (
|
||||
<Tab
|
||||
key={FEDERATED_AUTH}
|
||||
tabId={FEDERATED_AUTH}
|
||||
title={translate('authentication_auth_federated')}
|
||||
disabled={state.authenticating}
|
||||
disabled={dialogData.authenticating}
|
||||
onClick={() => { state.setActiveProvider(null); }}
|
||||
>
|
||||
<TabTitle>{translate('authentication_auth_federated')}</TabTitle>
|
||||
@@ -194,10 +198,18 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
</TabList>
|
||||
)}
|
||||
<SubmittingForm {...use({ form: !federate })} onSubmit={login}>
|
||||
<Loader state={state.loadingState}>
|
||||
<Loader state={dialogData.loadingState}>
|
||||
{() => federate
|
||||
? (
|
||||
<ConfigurationsList providers={state.configurations} onClose={rejectDialog} />
|
||||
<ConfigurationsList
|
||||
activeProvider={state.activeProvider}
|
||||
activeConfiguration={state.activeConfiguration}
|
||||
providers={dialogData.configurations}
|
||||
onAuthorize={(provider, configuration) => {
|
||||
state.setActiveConfiguration(provider, configuration);
|
||||
}}
|
||||
onClose={rejectDialog}
|
||||
/>
|
||||
)
|
||||
: renderForm(state.activeProvider)}
|
||||
</Loader>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
|
||||
import type { IAuthOptions } from '../IAuthOptions';
|
||||
import { AuthDialog } from './AuthDialog';
|
||||
|
||||
@injectable()
|
||||
@@ -19,10 +20,11 @@ export class AuthDialogService {
|
||||
|
||||
showLoginForm(
|
||||
persistent = false,
|
||||
providerId: string | null = null,
|
||||
link?: boolean
|
||||
options: IAuthOptions = {
|
||||
providerId: null,
|
||||
}
|
||||
): Promise<DialogueStateResult | null> {
|
||||
return this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent });
|
||||
return this.commonDialogService.open(AuthDialog, options, { persistent });
|
||||
}
|
||||
|
||||
closeLoginForm(promise: Promise<DialogueStateResult | null>): void {
|
||||
|
||||
+67
-6
@@ -10,12 +10,13 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { AuthInfoService, AuthProvider, comparePublicAuthConfigurations } from '@cloudbeaver/core-authentication';
|
||||
import { Filter, IconOrImage, Link, Cell, getComputed, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { AuthInfoService, AuthProvider, AuthProviderConfiguration, comparePublicAuthConfigurations } from '@cloudbeaver/core-authentication';
|
||||
import { Filter, IconOrImage, Link, Cell, getComputed, TextPlaceholder, usePromiseState, Loader, Button } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { AuthProviderConfiguration } from '@cloudbeaver/core-sdk';
|
||||
import type { ITask } from '@cloudbeaver/core-executor';
|
||||
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { AuthProviderInfo, UserInfo } from '@cloudbeaver/core-sdk';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { AuthenticationService } from '../../AuthenticationService';
|
||||
@@ -25,6 +26,7 @@ const styles = css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
Filter {
|
||||
margin: 0 24px 12px 24px;
|
||||
@@ -41,20 +43,39 @@ const styles = css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
center {
|
||||
margin: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const loaderStyle = css`
|
||||
ExceptionMessage {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface IProviderConfiguration {
|
||||
provider: AuthProvider;
|
||||
configuration: AuthProviderConfiguration;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
activeProvider: AuthProviderInfo | null;
|
||||
activeConfiguration: AuthProviderConfiguration | null;
|
||||
providers: AuthProvider[];
|
||||
onAuthorize?: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ConfigurationsList = observer<Props>(function ConfigurationsList({ providers, onClose, className }) {
|
||||
export const ConfigurationsList = observer<Props>(function ConfigurationsList({
|
||||
activeProvider,
|
||||
activeConfiguration,
|
||||
providers,
|
||||
onAuthorize,
|
||||
onClose,
|
||||
className,
|
||||
}) {
|
||||
const authInfoService = useService(AuthInfoService);
|
||||
const authenticationService = useService(AuthenticationService);
|
||||
const notificationService = useService(NotificationService);
|
||||
@@ -62,6 +83,8 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
|
||||
const style = useStyles(styles);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [authTask, setAuthTask] = useState<ITask<UserInfo | null> | null>(null);
|
||||
const authTaskState = usePromiseState(authTask);
|
||||
const configurations = getComputed<IProviderConfiguration[]>(() => providers.map(
|
||||
provider => (
|
||||
(provider.configurations || [])
|
||||
@@ -87,15 +110,23 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
|
||||
|
||||
async function auth({ provider, configuration }: IProviderConfiguration) {
|
||||
try {
|
||||
const user = await authInfoService.login(provider.id, {
|
||||
onAuthorize?.(provider, configuration);
|
||||
const authTask = authInfoService.login(provider.id, {
|
||||
configurationId: configuration.id,
|
||||
});
|
||||
setAuthTask(authTask);
|
||||
|
||||
const user = await authTask;
|
||||
|
||||
if (user) {
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
setAuthTask(null);
|
||||
} catch (exception: any) {
|
||||
notificationService.logException(exception, 'Federated authentication error');
|
||||
} finally {
|
||||
onAuthorize?.(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +148,29 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
|
||||
);
|
||||
}
|
||||
|
||||
if (activeProvider && activeConfiguration) {
|
||||
return styled(style)(
|
||||
<container className={className}>
|
||||
<Loader
|
||||
state={authTaskState}
|
||||
style={loaderStyle}
|
||||
message="authentication_authorizing"
|
||||
hideException
|
||||
>
|
||||
<center>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
onClick={() => auth({ provider: activeProvider, configuration: activeConfiguration })}
|
||||
>
|
||||
<Translate token='authentication_login' />
|
||||
</Button>
|
||||
</center>
|
||||
</Loader>
|
||||
</container>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<container className={className}>
|
||||
{configurations.length >= 10 && (
|
||||
@@ -148,6 +202,13 @@ export const ConfigurationsList = observer<Props>(function ConfigurationsList({
|
||||
);
|
||||
})}
|
||||
</list>
|
||||
<Loader
|
||||
state={authTaskState}
|
||||
style={loaderStyle}
|
||||
message="authentication_authorizing"
|
||||
overlay
|
||||
hideException
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed, observable } from 'mobx';
|
||||
import { action, computed, observable } from 'mobx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { AuthInfoService, AuthProvider, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication';
|
||||
import { AuthInfoService, AuthProvider, AuthProviderConfiguration, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication';
|
||||
import { ILoadableState, useMapResource, useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
@@ -18,25 +18,32 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { FEDERATED_AUTH } from './FEDERATED_AUTH';
|
||||
|
||||
interface IState {
|
||||
tabId: string | null;
|
||||
activeProvider: AuthProvider | null;
|
||||
interface IData {
|
||||
state: IState;
|
||||
exception: Error | null;
|
||||
authenticating: boolean;
|
||||
destroyed: boolean;
|
||||
configure: boolean;
|
||||
adminPageActive: boolean;
|
||||
credentials: IAuthCredentials;
|
||||
loadingState: ILoadableState;
|
||||
providers: AuthProvider[];
|
||||
configurations: AuthProvider[];
|
||||
|
||||
setTabId: (tabId: string) => void;
|
||||
setActiveProvider: (provider: AuthProvider | null) => void;
|
||||
login: (link: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useAuthDialogState(providerId: string | null): IState {
|
||||
interface IState {
|
||||
tabId: string | null;
|
||||
activeProvider: AuthProvider | null;
|
||||
activeConfiguration: AuthProviderConfiguration | null;
|
||||
credentials: IAuthCredentials;
|
||||
|
||||
setTabId: (tabId: string) => void;
|
||||
setActiveProvider: (provider: AuthProvider | null) => void;
|
||||
setActiveConfiguration: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void;
|
||||
}
|
||||
|
||||
export function useAuthDialogState(providerId: string | null, configurationId?: string): IData {
|
||||
const authProvidersResource = useMapResource(useAuthDialogState, AuthProvidersResource, CachedMapAllKey);
|
||||
const administrationScreenService = useService(AdministrationScreenService);
|
||||
const authInfoService = useService(AuthInfoService);
|
||||
@@ -48,9 +55,43 @@ export function useAuthDialogState(providerId: string | null): IState {
|
||||
.filter(notEmptyProvider)
|
||||
.sort(compareProviders);
|
||||
|
||||
const state = useObservableRef<IState>(() => ({
|
||||
tabId: null,
|
||||
activeProvider: null,
|
||||
activeConfiguration: null,
|
||||
credentials: {
|
||||
profile: '0',
|
||||
credentials: {},
|
||||
},
|
||||
|
||||
setTabId(tabId: string): void {
|
||||
this.tabId = tabId;
|
||||
},
|
||||
setActiveProvider(provider: AuthProvider | null): void {
|
||||
this.activeProvider = provider;
|
||||
this.credentials.profile = '0';
|
||||
this.credentials.credentials = {};
|
||||
this.activeConfiguration = null;
|
||||
},
|
||||
setActiveConfiguration(
|
||||
provider: AuthProvider | null,
|
||||
configuration: AuthProviderConfiguration | null
|
||||
): void {
|
||||
this.setActiveProvider(provider);
|
||||
this.activeConfiguration = configuration;
|
||||
},
|
||||
}), {
|
||||
tabId: observable.ref,
|
||||
activeProvider: observable.ref,
|
||||
activeConfiguration: observable.ref,
|
||||
credentials: observable,
|
||||
setActiveProvider: action,
|
||||
setActiveConfiguration: action,
|
||||
}, false);
|
||||
|
||||
const activeProviders = providers
|
||||
.filter(provider => {
|
||||
if (provider.configurable) {
|
||||
if (provider.configurable || state.activeConfiguration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -83,45 +124,30 @@ export function useAuthDialogState(providerId: string | null): IState {
|
||||
tabIds.push(FEDERATED_AUTH);
|
||||
}
|
||||
|
||||
const state = useObservableRef<IState>(() => ({
|
||||
tabId: null,
|
||||
activeProvider: null,
|
||||
const data = useObservableRef<IData>(() => ({
|
||||
exception: null,
|
||||
authenticating: false,
|
||||
destroyed: false,
|
||||
credentials: {
|
||||
profile: '0',
|
||||
credentials: {},
|
||||
},
|
||||
loadingState: authProvidersResource,
|
||||
|
||||
get configure(): boolean {
|
||||
if (this.activeProvider) {
|
||||
if (this.adminPageActive && authProvidersResource.resource.isPrimary(this.activeProvider.id)) {
|
||||
if (state.activeProvider) {
|
||||
if (this.adminPageActive && authProvidersResource.resource.isPrimary(state.activeProvider.id)) {
|
||||
return false;
|
||||
}
|
||||
return !authProvidersResource.resource.isAuthEnabled(this.activeProvider.id);
|
||||
return !authProvidersResource.resource.isAuthEnabled(state.activeProvider.id);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
setTabId(tabId: string): void {
|
||||
this.tabId = tabId;
|
||||
},
|
||||
setActiveProvider(provider: AuthProvider | null): void {
|
||||
this.activeProvider = provider;
|
||||
this.credentials.profile = '0';
|
||||
this.credentials.credentials = {};
|
||||
},
|
||||
async login(link: boolean): Promise<void> {
|
||||
if (!this.activeProvider || this.authenticating) {
|
||||
if (!state.activeProvider || this.authenticating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authenticating = true;
|
||||
try {
|
||||
await authInfoService.login(this.activeProvider.id, {
|
||||
credentials: this.credentials,
|
||||
await authInfoService.login(state.activeProvider.id, {
|
||||
credentials: state.credentials,
|
||||
linkUser: link,
|
||||
});
|
||||
} catch (exception: any) {
|
||||
@@ -136,29 +162,33 @@ export function useAuthDialogState(providerId: string | null): IState {
|
||||
}
|
||||
},
|
||||
}), {
|
||||
tabId: observable.ref,
|
||||
activeProvider: observable.ref,
|
||||
state: observable.ref,
|
||||
exception: observable.ref,
|
||||
authenticating: observable.ref,
|
||||
configure: computed,
|
||||
adminPageActive: observable.ref,
|
||||
credentials: observable,
|
||||
}, {
|
||||
state,
|
||||
adminPageActive,
|
||||
providers: activeProviders,
|
||||
configurations,
|
||||
});
|
||||
|
||||
useEffect(() => () => { state.destroyed = true; }, []);
|
||||
useEffect(() => () => { data.destroyed = true; }, []);
|
||||
|
||||
if (tabIds.length > 0 && (state.tabId === null || !tabIds.includes(state.tabId))
|
||||
) {
|
||||
if (tabIds.length > 0 && (state.tabId === null || !tabIds.includes(state.tabId))) {
|
||||
const tabId = tabIds[0];
|
||||
state.setTabId(tabId);
|
||||
state.setActiveProvider(activeProviders.find(provider => provider.id === tabId) || null);
|
||||
|
||||
const provider = activeProviders.find(provider => provider.id === tabId) || null;
|
||||
const configuration = provider?.configurations?.find(
|
||||
configuration => configuration.id === configurationId
|
||||
) ?? null;
|
||||
|
||||
state.setActiveConfiguration(provider, configuration);
|
||||
}
|
||||
|
||||
return state;
|
||||
return data;
|
||||
}
|
||||
|
||||
function notEmptyProvider(obj: any): obj is AuthProvider {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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 interface IAuthOptions {
|
||||
providerId: string | null;
|
||||
configurationId?: string;
|
||||
link?: boolean;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class PluginBootstrap extends Bootstrap {
|
||||
label: 'authentication_login',
|
||||
tooltip: 'authentication_login',
|
||||
},
|
||||
{ onSelect: () => this.authDialogService.showLoginForm(false, null, true) }
|
||||
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, link: true }) }
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export default [
|
||||
['authentication_login', 'Login'],
|
||||
['authentication_logout', 'Logout'],
|
||||
['authentication_authenticate', 'Authenticate'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', 'Federated'],
|
||||
['authentication_select_provider', 'Select available provider'],
|
||||
['authentication_configure', 'Please configure authentication methods'],
|
||||
|
||||
@@ -3,6 +3,7 @@ export default [
|
||||
['authentication_login', 'Login'],
|
||||
['authentication_logout', 'Logout'],
|
||||
['authentication_authenticate', 'Autentica'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', 'Federated'],
|
||||
['authentication_request_token', 'Autenticazione addizionale richiesta'],
|
||||
['authentication_user_name', 'Nome utente'],
|
||||
|
||||
@@ -3,6 +3,7 @@ export default [
|
||||
['authentication_login', 'Войти'],
|
||||
['authentication_logout', 'Выйти'],
|
||||
['authentication_authenticate', 'Аутентифицироваться'],
|
||||
['authentication_authorizing', 'Авторизация...'],
|
||||
['authentication_auth_federated', 'Федеративная'],
|
||||
['authentication_select_provider', 'Выберите способ аутентификации'],
|
||||
['authentication_configure', 'Пожалуйста настройте способы аутентификации'],
|
||||
|
||||
@@ -3,6 +3,7 @@ export default [
|
||||
['authentication_login', '登录'],
|
||||
['authentication_logout', '登出'],
|
||||
['authentication_authenticate', '认证'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', '联合认证'],
|
||||
['authentication_select_provider', '选择可用提供者'],
|
||||
['authentication_configure', '请配置认证方法'],
|
||||
|
||||
Reference in New Issue
Block a user