From a0fe8869c13b8d37d8420fac8dcc1b820a1b7cc1 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 30 Jun 2022 14:49:43 +0300 Subject: [PATCH 1/5] fix(core-administration): CB-2227 slider animation --- .../core-administration/src/Administration/Administration.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/packages/core-administration/src/Administration/Administration.tsx b/webapp/packages/core-administration/src/Administration/Administration.tsx index 69af2fec1f..9e7cc4ba5b 100644 --- a/webapp/packages/core-administration/src/Administration/Administration.tsx +++ b/webapp/packages/core-administration/src/Administration/Administration.tsx @@ -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; From 57b72515b6a1031c61c71786127f84e054445f38 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 30 Jun 2022 17:13:25 +0300 Subject: [PATCH 2/5] fix: CB-2177 exception message details --- webapp/packages/core-app/src/Body.tsx | 8 +++++++- .../packages/core-authentication/src/AppAuthService.ts | 4 ++-- webapp/packages/core-blocks/src/ExceptionMessage.tsx | 2 +- webapp/packages/core-events/src/INotification.ts | 6 +++--- webapp/packages/core-events/src/NotificationService.ts | 9 +++++++-- webapp/packages/core-sdk/src/getErrorDetails.ts | 8 ++++---- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 95ca9d84b1..aa7d7d8898 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -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)( - {() => styled(style)( + {() => styled(style)( <> {Screen && } diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index d0c5bba8b1..5ef5b55807 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -26,8 +26,8 @@ export class AppAuthService extends Bootstrap { readonly auth: IExecutor; constructor( - private serverService: ServerService, - private userInfoResource: UserInfoResource, + private readonly serverService: ServerService, + private readonly userInfoResource: UserInfoResource, ) { super(); this.auth = new Executor(); diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index c2e8f692f1..79f5e24232 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -111,7 +111,7 @@ export const ExceptionMessage = observer(function ExceptionMessage({ {name || error.details?.name} {message || error.details?.message} - {exception && ( + {exception && error.details?.hasDetails && ( diff --git a/webapp/packages/core-events/src/INotification.ts b/webapp/packages/core-events/src/INotification.ts index 9f99ba539e..b25bf6ac7f 100644 --- a/webapp/packages/core-events/src/INotification.ts +++ b/webapp/packages/core-events/src/INotification.ts @@ -21,7 +21,7 @@ export enum ENotificationType { Error = 'Error', Success = 'Success', Loading = 'Loading', - Custom =' Custom' + Custom = ' Custom' } export interface INotificationExtraProps { @@ -53,7 +53,7 @@ export interface INotification = 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>(({ 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) { diff --git a/webapp/packages/core-sdk/src/getErrorDetails.ts b/webapp/packages/core-sdk/src/getErrorDetails.ts index ae3fe4e574..e7471bb785 100644 --- a/webapp/packages/core-sdk/src/getErrorDetails.ts +++ b/webapp/packages/core-sdk/src/getErrorDetails.ts @@ -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), }; From ea946e1ae2c4fc9eca96127273e399366c9b94db Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 30 Jun 2022 17:14:32 +0300 Subject: [PATCH 3/5] feat: CB-2177 federated authentication cancellation --- .../src/AuthInfoService.ts | 33 ++++-- .../src/UserInfoResource.ts | 66 ++++++----- .../core-blocks/src/Loader/ILoadableState.ts | 5 +- .../core-blocks/src/Loader/Loader.tsx | 23 ++-- webapp/packages/core-blocks/src/index.ts | 2 + .../core-blocks/src/useDataResource.ts | 2 +- .../core-blocks/src/useMapResource.ts | 2 +- .../core-blocks/src/usePromiseState.ts | 70 +++++++++++ .../src/TaskScheduler/AutoRunningTask.ts | 28 +++++ .../core-executor/src/TaskScheduler/Task.ts | 35 +++--- webapp/packages/core-executor/src/index.ts | 3 + .../packages/core-executor/src/whileTask.ts | 73 ++++++++++++ webapp/packages/core-ui/src/WindowsService.ts | 8 ++ .../src/AuthenticationService.ts | 16 ++- .../src/Dialog/AuthDialog.tsx | 60 ++++++---- .../src/Dialog/AuthDialogService.ts | 8 +- .../AuthProviderForm/ConfigurationsList.tsx | 73 +++++++++++- .../src/Dialog/useAuthDialogState.ts | 110 +++++++++++------- .../plugin-authentication/src/IAuthOptions.ts | 13 +++ .../src/PluginBootstrap.ts | 2 +- .../plugin-authentication/src/locales/en.ts | 1 + .../plugin-authentication/src/locales/it.ts | 1 + .../plugin-authentication/src/locales/ru.ts | 1 + .../plugin-authentication/src/locales/zh.ts | 1 + 24 files changed, 495 insertions(+), 141 deletions(-) create mode 100644 webapp/packages/core-blocks/src/usePromiseState.ts create mode 100644 webapp/packages/core-executor/src/TaskScheduler/AutoRunningTask.ts create mode 100644 webapp/packages/core-executor/src/whileTask.ts create mode 100644 webapp/packages/plugin-authentication/src/IAuthOptions.ts diff --git a/webapp/packages/core-authentication/src/AuthInfoService.ts b/webapp/packages/core-authentication/src/AuthInfoService.ts index d4ae14dd8a..bc9c66ddff 100644 --- a/webapp/packages/core-authentication/src/AuthInfoService.ts +++ b/webapp/packages/core-authentication/src/AuthInfoService.ts @@ -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 { - const authInfo = await this.userInfoResource.login(providerId, options); - - await this.federatedAuthentication(providerId, options, authInfo); - - return this.userInfoResource.data; + login(providerId: string, options: ILoginOptions): ITask { + return new AutoRunningTask(async () => await this.userInfoResource.login(providerId, options)) + .then(authInfo => this.federatedAuthentication(providerId, options, authInfo)); } async logout(): Promise { await this.userInfoResource.logout(); } - private async federatedAuthentication( + private federatedAuthentication( providerId: string, options: ILoginOptions, { redirectLink, authId, authStatus }: AuthInfo - ): Promise { + ): ITask { + 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); + } + }); } } diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 2429c89994..827785f597 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -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 { - return await this.performUpdate(undefined, [], async () => { - await new Promise((resolve, reject) => { - const interval = setInterval(async () => { - try { + finishFederatedAuthentication(authId: string, link?: boolean): ITask { + let activeTask: ITask | undefined; + + return new AutoRunningTask(() => this.performUpdate( + undefined, + [], + async () => { + activeTask = whileTask( + 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(); }); } diff --git a/webapp/packages/core-blocks/src/Loader/ILoadableState.ts b/webapp/packages/core-blocks/src/Loader/ILoadableState.ts index 8237c68a41..c6de66c91c 100644 --- a/webapp/packages/core-blocks/src/Loader/ILoadableState.ts +++ b/webapp/packages/core-blocks/src/Loader/ILoadableState.ts @@ -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; } diff --git a/webapp/packages/core-blocks/src/Loader/Loader.tsx b/webapp/packages/core-blocks/src/Loader/Loader.tsx index 9db519f986..3f870fb2d7 100644 --- a/webapp/packages/core-blocks/src/Loader/Loader.tsx +++ b/webapp/packages/core-blocks/src/Loader/Loader.tsx @@ -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(function Loader({ overlay, message, hideMessage, + hideException, secondary, small, inline, @@ -101,6 +98,16 @@ export const Loader = observer(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(function Loader({ } }); - if (exception && !loading) { + if (exception && !loading && !hideException) { return styled(style)( { diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index ef0d378e69..9d77412f2b 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -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'; diff --git a/webapp/packages/core-blocks/src/usePromiseState.ts b/webapp/packages/core-blocks/src/usePromiseState.ts new file mode 100644 index 0000000000..97f9197f55 --- /dev/null +++ b/webapp/packages/core-blocks/src/usePromiseState.ts @@ -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(promise: Promise | 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; +} \ No newline at end of file diff --git a/webapp/packages/core-executor/src/TaskScheduler/AutoRunningTask.ts b/webapp/packages/core-executor/src/TaskScheduler/AutoRunningTask.ts new file mode 100644 index 0000000000..ceed8e337b --- /dev/null +++ b/webapp/packages/core-executor/src/TaskScheduler/AutoRunningTask.ts @@ -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 extends Task { + constructor( + task: () => Promise, + externalCancel?: () => Promise | void + ) { + super(task, externalCancel); + this.run(); + } + + static resolve(value: TValue): ITask { + return new AutoRunningTask(() => Promise.resolve(value)); + } + + static reject(exception: any): ITask { + return new AutoRunningTask(() => Promise.reject(exception)); + } +} \ No newline at end of file diff --git a/webapp/packages/core-executor/src/TaskScheduler/Task.ts b/webapp/packages/core-executor/src/TaskScheduler/Task.ts index 6d8b30c429..032cf38bc5 100644 --- a/webapp/packages/core-executor/src/TaskScheduler/Task.ts +++ b/webapp/packages/core-executor/src/TaskScheduler/Task.ts @@ -35,7 +35,7 @@ export class Task implements ITask { private resolve!: (value: TValue) => void; private reject!: (reason?: any) => void; - private innerPromise: Promise; + private readonly innerPromise: Promise; private sourcePromise: Promise | null; get [Symbol.toStringTag](): string { @@ -44,7 +44,7 @@ export class Task implements ITask { constructor( readonly task: () => Promise, - private externalCancel?: () => Promise | void + private readonly externalCancel?: () => Promise | void ) { this.innerPromise = new Promise((resolve, reject) => { this.reject = reject; @@ -66,17 +66,24 @@ export class Task implements ITask { onfulfilled?: ((value: TValue) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null ): ITask { + 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( @@ -127,7 +134,7 @@ export class Task implements ITask { return this; } - cancel(): Promise | void { + async cancel(): Promise { if (this.cancelled) { return; } @@ -140,11 +147,11 @@ export class Task implements ITask { } if (this.externalCancel) { - return this.externalCancel(); + await this.externalCancel(); } if (this.sourcePromise instanceof Task) { - return this.sourcePromise.cancel(); + await this.sourcePromise.cancel(); } } } diff --git a/webapp/packages/core-executor/src/index.ts b/webapp/packages/core-executor/src/index.ts index b97a93f6f2..22f248d2d2 100644 --- a/webapp/packages/core-executor/src/index.ts +++ b/webapp/packages/core-executor/src/index.ts @@ -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'; diff --git a/webapp/packages/core-executor/src/whileTask.ts b/webapp/packages/core-executor/src/whileTask.ts new file mode 100644 index 0000000000..2ad37d87b3 --- /dev/null +++ b/webapp/packages/core-executor/src/whileTask.ts @@ -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( + callback: (value: T) => Promise | boolean, + task: () => Promise, + interval: number, + cancelMessage?: string +): ITask { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: any) => void; + + const lockPromise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + let timeoutId: NodeJS.Timeout | null; + let activeTask: Promise | 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')) + ); +} \ No newline at end of file diff --git a/webapp/packages/core-ui/src/WindowsService.ts b/webapp/packages/core-ui/src/WindowsService.ts index 087a62de92..1c857b5424 100644 --- a/webapp/packages/core-ui/src/WindowsService.ts +++ b/webapp/packages/core-ui/src/WindowsService.ts @@ -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 { 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; } }); } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 913943929a..7125ae1ebe 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -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, diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 728d61ef07..26b35b1b5d 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -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 = observer(function AuthDialog({ +export const AuthDialog: DialogComponent = 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 = observer(function } async function login() { - await state.login(link); + await dialogData.login(link); rejectDialog(); } @@ -120,7 +124,7 @@ export const AuthDialog: DialogComponent = observer(function return {translate('authentication_select_provider')}; } - if (state.configure) { + if (dialogData.configure) { return ( {translate('authentication_provider_disabled')} @@ -137,7 +141,7 @@ export const AuthDialog: DialogComponent = observer(function ); } @@ -147,15 +151,15 @@ export const AuthDialog: DialogComponent = observer(function - {state.exception && ( + {dialogData.exception && ( = observer(function > {showTabs && ( - {state.providers.map(provider => ( + {dialogData.providers.map(provider => ( { state.setActiveProvider(provider); }} > {provider.label} ))} - {state.configurations.length > 0 && ( + {dialogData.configurations.length > 0 && ( { state.setActiveProvider(null); }} > {translate('authentication_auth_federated')} @@ -194,10 +198,18 @@ export const AuthDialog: DialogComponent = observer(function )} - + {() => federate ? ( - + { + state.setActiveConfiguration(provider, configuration); + }} + onClose={rejectDialog} + /> ) : renderForm(state.activeProvider)} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts index 18bcf79815..7c43e84315 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts @@ -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 { - return this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent }); + return this.commonDialogService.open(AuthDialog, options, { persistent }); } closeLoginForm(promise: Promise): void { diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx index f6b2eb1f1d..7ff614334c 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx @@ -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(function ConfigurationsList({ providers, onClose, className }) { +export const ConfigurationsList = observer(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(function ConfigurationsList({ const style = useStyles(styles); const [search, setSearch] = useState(''); + const [authTask, setAuthTask] = useState | null>(null); + const authTaskState = usePromiseState(authTask); const configurations = getComputed(() => providers.map( provider => ( (provider.configurations || []) @@ -87,15 +110,23 @@ export const ConfigurationsList = observer(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(function ConfigurationsList({ ); } + if (activeProvider && activeConfiguration) { + return styled(style)( + + +
+ +
+
+
+ ); + } + return styled(style)( {configurations.length >= 10 && ( @@ -148,6 +202,13 @@ export const ConfigurationsList = observer(function ConfigurationsList({ ); })} + ); }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts index 276966968d..92a4115627 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts @@ -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; } -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(() => ({ + 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(() => ({ - tabId: null, - activeProvider: null, + const data = useObservableRef(() => ({ 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 { - 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 { diff --git a/webapp/packages/plugin-authentication/src/IAuthOptions.ts b/webapp/packages/plugin-authentication/src/IAuthOptions.ts new file mode 100644 index 0000000000..3e75191db2 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/IAuthOptions.ts @@ -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; +} \ No newline at end of file diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index 7934ce6063..4b2aae2e3d 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -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 }) } ), ]; } diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index 3762fef8cb..8c88c85c77 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication/src/locales/it.ts b/webapp/packages/plugin-authentication/src/locales/it.ts index 2339dc7495..846d50c7f7 100644 --- a/webapp/packages/plugin-authentication/src/locales/it.ts +++ b/webapp/packages/plugin-authentication/src/locales/it.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index db6dfdd4d7..bac98340c9 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -3,6 +3,7 @@ export default [ ['authentication_login', 'Войти'], ['authentication_logout', 'Выйти'], ['authentication_authenticate', 'Аутентифицироваться'], + ['authentication_authorizing', 'Авторизация...'], ['authentication_auth_federated', 'Федеративная'], ['authentication_select_provider', 'Выберите способ аутентификации'], ['authentication_configure', 'Пожалуйста настройте способы аутентификации'], diff --git a/webapp/packages/plugin-authentication/src/locales/zh.ts b/webapp/packages/plugin-authentication/src/locales/zh.ts index c1939a9121..bd6c7a63af 100644 --- a/webapp/packages/plugin-authentication/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication/src/locales/zh.ts @@ -3,6 +3,7 @@ export default [ ['authentication_login', '登录'], ['authentication_logout', '登出'], ['authentication_authenticate', '认证'], + ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', '联合认证'], ['authentication_select_provider', '选择可用提供者'], ['authentication_configure', '请配置认证方法'], From 9cda2a71b4f2f856801b8da173a4567b17f447bb Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Thu, 30 Jun 2022 19:28:13 +0300 Subject: [PATCH 4/5] CB-2228 expire success auth attempt (#943) --- .../cloudbeaver/service/auth/impl/WebServiceAuthImpl.java | 2 ++ .../security/internal/CBEmbeddedSecurityController.java | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 7e33741caa..8701708a22 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -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()); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java index 3615dc139e..3a09fa2f79 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBEmbeddedSecurityController.java @@ -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); From 60b17e93b36842d4e5ae22356fa7791a383e163c Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Thu, 30 Jun 2022 20:37:45 +0300 Subject: [PATCH 5/5] CB-2217 add auto-login action (#944) --- .../server/servlets/CBStaticServlet.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index 90bc9b1d29..a0ae2c203f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -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 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;