diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2UserController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2UserController.kt index 8afc668b5e..71025644bc 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2UserController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2UserController.kt @@ -178,6 +178,7 @@ class V2UserController( summary = "Get organization which manages user", description = "Returns the organization that manages a given user or null", ) + @BypassForcedSsoAuthentication @OpenApiHideFromPublicDocs fun getManagedBy(): PrivateOrganizationModel? { val userAccount = authenticationFacade.authenticatedUser diff --git a/backend/api/src/main/kotlin/io/tolgee/controllers/AuthProviderChangeController.kt b/backend/api/src/main/kotlin/io/tolgee/controllers/AuthProviderChangeController.kt index bbc4cfc305..fe94b21f92 100644 --- a/backend/api/src/main/kotlin/io/tolgee/controllers/AuthProviderChangeController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/controllers/AuthProviderChangeController.kt @@ -44,18 +44,14 @@ class AuthProviderChangeController( } @DeleteMapping("") - @Operation(summary = "Remove current third party authentication provider") + @Operation(summary = "Initiate provider change to remove current third party authentication provider") @AllowApiAccess(AuthTokenType.ONLY_PAT) @BypassForcedSsoAuthentication @RequiresSuperAuthentication @Transactional - fun deleteCurrentAuthProvider(): JwtAuthenticationResponse { + fun deleteCurrentAuthProvider() { val user = authenticationFacade.authenticatedUserEntity - authProviderChangeService.removeCurrent(user) - userAccountService.invalidateTokens(user) - return JwtAuthenticationResponse( - jwtService.emitToken(authenticationFacade.authenticatedUser.id, true), - ) + authProviderChangeService.initiateRemove(user) } @GetMapping("/change") diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/AuthProviderChangeService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/AuthProviderChangeService.kt index 3219e5293c..cca8ab8f65 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/AuthProviderChangeService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/AuthProviderChangeService.kt @@ -97,16 +97,13 @@ class AuthProviderChangeService( self.apply(AuthProviderChangeRequest().from(user, data)) } - fun removeCurrent(user: UserAccount) { - if (user.password == null) { - throw BadRequestException(Message.USER_MISSING_PASSWORD) - } + fun initiateRemove(user: UserAccount) { val data = AuthProviderChangeData( UserAccount.AccountType.LOCAL, null, ) - self.apply(AuthProviderChangeRequest().from(user, data)) + self.save(user, data) } @Transactional @@ -120,7 +117,11 @@ class AuthProviderChangeService( fun apply(req: AuthProviderChangeRequest) { val userAccount = req.userAccount ?: return throw NotFoundException() if (userAccount.accountType === UserAccount.AccountType.MANAGED) { - throw AuthenticationException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE) + throw BadRequestException(Message.OPERATION_UNAVAILABLE_FOR_ACCOUNT_TYPE) + } + + if (req.accountType == UserAccount.AccountType.LOCAL && userAccount.password == null) { + throw BadRequestException(Message.USER_MISSING_PASSWORD) } userAccount.apply { diff --git a/webapp/src/component/layout/Notifications/NotificationsPopup.tsx b/webapp/src/component/layout/Notifications/NotificationsPopup.tsx index fa0c0c188e..4b77ab4ef9 100644 --- a/webapp/src/component/layout/Notifications/NotificationsPopup.tsx +++ b/webapp/src/component/layout/Notifications/NotificationsPopup.tsx @@ -105,6 +105,10 @@ export const NotificationsPopup: React.FC = ({ }); }, }, + fetchOptions: { + disableAutoErrorHandle: true, + disableAuthRedirect: true, + }, }); const markSeenMutation = useApiMutation({ diff --git a/webapp/src/component/layout/Notifications/NotificationsTopBarButton.tsx b/webapp/src/component/layout/Notifications/NotificationsTopBarButton.tsx index 58cc5f215f..7d8900939e 100644 --- a/webapp/src/component/layout/Notifications/NotificationsTopBarButton.tsx +++ b/webapp/src/component/layout/Notifications/NotificationsTopBarButton.tsx @@ -29,6 +29,11 @@ export const NotificationsTopBarButton: React.FC = () => { staleTime: Infinity, cacheTime: Infinity, }, + fetchOptions: { + disableAutoErrorHandle: true, + disableAuthRedirect: true, + disableErrorNotification: true, + }, }); const handleOpen = (event: React.MouseEvent) => { diff --git a/webapp/src/component/security/AcceptAuthProviderChangeView.tsx b/webapp/src/component/security/AcceptAuthProviderChangeView.tsx index f1f5868d19..38e73e3ca9 100644 --- a/webapp/src/component/security/AcceptAuthProviderChangeView.tsx +++ b/webapp/src/component/security/AcceptAuthProviderChangeView.tsx @@ -1,6 +1,6 @@ import { useHistory } from 'react-router-dom'; import { T, useTranslate } from '@tolgee/react'; -import { Box, Paper, styled, Typography } from '@mui/material'; +import { Box, styled } from '@mui/material'; import { LINKS } from 'tg.constants/links'; import { messageService } from 'tg.service/MessageService'; @@ -11,10 +11,9 @@ import { useWindowTitle } from 'tg.hooks/useWindowTitle'; import LoadingButton from 'tg.component/common/form/LoadingButton'; import { FullPageLoading } from 'tg.component/common/FullPageLoading'; import { TranslatedError } from 'tg.translationTools/TranslatedError'; -import React from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { useIsSsoMigrationRequired } from 'tg.globalContext/helpers'; - -export const FULL_PAGE_BREAK_POINT = '(max-width: 700px)'; +import { AuthProviderChangeBody } from 'tg.component/security/AuthProviderChangeBody'; const StyledContainer = styled(Box)` display: grid; @@ -28,18 +27,6 @@ const StyledContent = styled(Box)` gap: 32px; `; -const StyledPaper = styled(Paper)` - padding: 60px; - display: grid; - gap: 32px; - background: ${({ theme }) => theme.palette.tokens.background['paper-1']}; - @media ${FULL_PAGE_BREAK_POINT} { - padding: 10px; - box-shadow: none; - background: transparent; - } -`; - const AcceptAuthProviderChangeView: React.FC = () => { const history = useHistory(); const { t } = useTranslate(); @@ -130,136 +117,69 @@ const AcceptAuthProviderChangeView: React.FC = () => { } const accountType = authProviderChangeInfo.data?.accountType; - const authType = authProviderChangeInfo.data?.authType ?? 'NONE'; - const authTypeOld = authProviderCurrentInfo.data?.authType ?? 'NONE'; - const ssoDomain = authProviderChangeInfo.data?.ssoDomain ?? ''; - const params = { - authType, - authTypeOld, - ssoDomain, - b: , - br:
, - }; - const willBeManaged = accountType === 'MANAGED'; - let titleText: React.ReactNode | null; - let infoText: React.ReactNode; - - switch (true) { - case willBeManaged && isSsoMigrationRequired: - // Migrating to SSO; migration is forced - titleText = null; - infoText = ( - - ); - break; - case willBeManaged: - // Migrating to SSO; migration is voluntary - titleText = null; - infoText = ( - - ); - break; - case authTypeOld === 'NONE': - // Currently user has no third-party provider - titleText = ( - - ); - infoText = ( - - ); - break; - case authType === 'NONE': - // User is removing third-party provider - titleText = ( - - ); - infoText = ( - - ); - break; - default: - // From one third-party provider to another third-party provider - titleText = ( - - ); - infoText = ( - - ); - break; - } + const [autoAccepted, setAutoAccepted] = useState(false); + useEffect(() => { + // Auto-accept forced sso migration to avoid second confirmation dialog + if ( + authProviderChangeInfo.data && + willBeManaged && + isSsoMigrationRequired && + !autoAccepted + ) { + setAutoAccepted(true); + handleAccept(); + } + }, [ + authProviderChangeInfo, + willBeManaged, + isSsoMigrationRequired, + autoAccepted, + ]); if (!authProviderChangeInfo.data || authProviderCurrentInfo.isLoading) { return ; } + const buttons = ( + + + {willBeManaged + ? t('accept_auth_provider_change_accept') + : t('accept_auth_provider_change_accept_non_managed')} + + {(!willBeManaged || !isSsoMigrationRequired) && ( + + {t('accept_auth_provider_change_decline')} + + )} + + ); + return ( - - {titleText && ( - - {titleText} - - )} - - - - {infoText} - - - - {willBeManaged - ? t('accept_auth_provider_change_accept') - : t('accept_auth_provider_change_accept_non_managed')} - - {(!willBeManaged || !isSsoMigrationRequired) && ( - - {t('accept_auth_provider_change_decline')} - - )} - - - + + {buttons} + diff --git a/webapp/src/component/security/AuthProviderChangeBody.tsx b/webapp/src/component/security/AuthProviderChangeBody.tsx new file mode 100644 index 0000000000..126210be28 --- /dev/null +++ b/webapp/src/component/security/AuthProviderChangeBody.tsx @@ -0,0 +1,131 @@ +import React, { FunctionComponent } from 'react'; +import { Box, Paper, styled, Typography } from '@mui/material'; +import { T } from '@tolgee/react'; + +import { components } from 'tg.service/apiSchema.generated'; +import { useIsSsoMigrationRequired } from 'tg.globalContext/helpers'; + +export const FULL_PAGE_BREAK_POINT = '(max-width: 700px)'; + +type AuthProviderDto = components['schemas']['AuthProviderDto']; + +const StyledPaper = styled(Paper)` + padding: 60px; + display: grid; + gap: 32px; + background: ${({ theme }) => theme.palette.tokens.background['paper-1']}; + @media ${FULL_PAGE_BREAK_POINT} { + padding: 10px; + box-shadow: none; + background: transparent; + } +`; + +type Props = { + willBeManaged: boolean | undefined; + authType: AuthProviderDto['authType'] | 'NONE'; + authTypeOld: AuthProviderDto['authType'] | 'NONE'; + ssoDomain: AuthProviderDto['ssoDomain']; + children: React.ReactNode | undefined; +}; + +export const AuthProviderChangeBody: FunctionComponent = ({ + willBeManaged, + authType, + authTypeOld, + ssoDomain, + children, +}: Props) => { + const isSsoMigrationRequired = useIsSsoMigrationRequired(); + const params = { + authType, + authTypeOld, + ssoDomain, + b: , + br:
, + }; + + let titleText: React.ReactNode | null; + let infoText: React.ReactNode; + + switch (true) { + case willBeManaged && isSsoMigrationRequired: + // Migrating to SSO; migration is forced + titleText = null; + infoText = ( + + ); + break; + case willBeManaged: + // Migrating to SSO; migration is voluntary + titleText = null; + infoText = ( + + ); + break; + case authTypeOld === 'NONE': + // Currently user has no third-party provider + titleText = ( + + ); + infoText = ( + + ); + break; + case authType === 'NONE': + // User is removing third-party provider + titleText = ( + + ); + infoText = ( + + ); + break; + default: + // From one third-party provider to another third-party provider + titleText = ( + + ); + infoText = ( + + ); + break; + } + + return ( + + {titleText && ( + + {titleText} + + )} + + + + {infoText} + + + {children} + + + + ); +}; diff --git a/webapp/src/component/security/SsoMigrationView.tsx b/webapp/src/component/security/SsoMigrationView.tsx index 97aac479dd..ee9c66d623 100644 --- a/webapp/src/component/security/SsoMigrationView.tsx +++ b/webapp/src/component/security/SsoMigrationView.tsx @@ -100,10 +100,6 @@ const SsoMigrationView: React.FC = () => { keyName="sso_migration_description" params={{ domain: user?.domain || '', br:
}} /> - {/*{ssoUrlUnavailable && (*/} - {/* // TODO: should we do this? ; use error color*/} - {/* */} - {/*)}*/} { - const { handleAfterLogin, useSsoAuthLinkByDomain } = useGlobalActions(); + const { useSsoAuthLinkByDomain } = useGlobalActions(); const user = useUser(); const oAuthServices = useOAuthServices(); + const history = useHistory(); const remoteConfig = useConfig(); const organizationsSsoEnabled = @@ -29,6 +31,7 @@ export const ChangeAuthProvider: FunctionComponent = () => { fetchOptions: { disableAutoErrorHandle: true, }, + invalidatePrefix: '/v2/auth-provider', }); if (!user) return null; @@ -38,8 +41,7 @@ export const ChangeAuthProvider: FunctionComponent = () => { {}, { onSuccess(r) { - handleAfterLogin(r); - messageService.success(); + history.push(LINKS.ACCEPT_AUTH_PROVIDER_CHANGE.build()); }, } );