diff --git a/apps/web/src/app/auth/accept-organization.component.ts b/apps/web/src/app/auth/accept-organization.component.ts deleted file mode 100644 index 52e3b64494..0000000000 --- a/apps/web/src/app/auth/accept-organization.component.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { - OrganizationUserAcceptInitRequest, - OrganizationUserAcceptRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { OrgKey } from "@bitwarden/common/types/key"; - -import { BaseAcceptComponent } from "../common/base.accept.component"; - -@Component({ - selector: "app-accept-organization", - templateUrl: "accept-organization.component.html", -}) -export class AcceptOrganizationComponent extends BaseAcceptComponent { - orgName: string; - - protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"]; - - constructor( - router: Router, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - route: ActivatedRoute, - stateService: StateService, - private cryptoService: CryptoService, - private policyApiService: PolicyApiServiceAbstraction, - private policyService: PolicyService, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private organizationUserService: OrganizationUserService, - private messagingService: MessagingService, - private apiService: ApiService, - ) { - super(router, platformUtilsService, i18nService, route, stateService); - } - - async authedHandler(qParams: Params): Promise { - const initOrganization = - qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true"; - if (initOrganization) { - this.actionPromise = this.acceptInitOrganizationFlow(qParams); - } else { - const needsReAuth = (await this.stateService.getOrganizationInvitation()) == null; - if (needsReAuth) { - // Accepting an org invite requires authentication from a logged out state - this.messagingService.send("logout", { redirect: false }); - await this.prepareOrganizationInvitation(qParams); - return; - } - - // User has already logged in and passed the Master Password policy check - this.actionPromise = this.acceptFlow(qParams); - } - - await this.actionPromise; - await this.apiService.refreshIdentityToken(); - await this.stateService.setOrganizationInvitation(null); - this.platformUtilService.showToast( - "success", - this.i18nService.t("inviteAccepted"), - initOrganization - ? this.i18nService.t("inviteInitAcceptedDesc") - : this.i18nService.t("inviteAcceptedDesc"), - { timeout: 10000 }, - ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/vault"]); - } - - async unauthedHandler(qParams: Params): Promise { - await this.prepareOrganizationInvitation(qParams); - - // In certain scenarios, we want to accelerate the user through the accept org invite process - // For example, if the user has a BW account already, we want them to be taken to login instead of creation. - await this.accelerateInviteAcceptIfPossible(qParams); - } - - private async acceptInitOrganizationFlow(qParams: Params): Promise { - return this.prepareAcceptInitRequest(qParams).then((request) => - this.organizationUserService.postOrganizationUserAcceptInit( - qParams.organizationId, - qParams.organizationUserId, - request, - ), - ); - } - - private async acceptFlow(qParams: Params): Promise { - return this.prepareAcceptRequest(qParams).then((request) => - this.organizationUserService.postOrganizationUserAccept( - qParams.organizationId, - qParams.organizationUserId, - request, - ), - ); - } - - private async prepareAcceptInitRequest( - qParams: Params, - ): Promise { - const request = new OrganizationUserAcceptInitRequest(); - request.token = qParams.token; - - const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey(); - const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey); - const collection = await this.cryptoService.encrypt( - this.i18nService.t("defaultCollection"), - orgKey, - ); - - request.key = encryptedOrgKey.encryptedString; - request.keys = new OrganizationKeysRequest( - orgPublicKey, - encryptedOrgPrivateKey.encryptedString, - ); - request.collectionName = collection.encryptedString; - - return request; - } - - private async prepareAcceptRequest(qParams: Params): Promise { - const request = new OrganizationUserAcceptRequest(); - request.token = qParams.token; - - if (await this.performResetPasswordAutoEnroll(qParams)) { - const response = await this.organizationApiService.getKeys(qParams.organizationId); - - if (response == null) { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - - const publicKey = Utils.fromB64ToArray(response.publicKey); - - // RSA Encrypt user's encKey.key with organization public key - const userKey = await this.cryptoService.getUserKey(); - const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); - - // Add reset password key to accept request - request.resetPasswordKey = encryptedKey.encryptedString; - } - return request; - } - - private async performResetPasswordAutoEnroll(qParams: Params): Promise { - let policyList: Policy[] = null; - try { - const policies = await this.policyApiService.getPoliciesByToken( - qParams.organizationId, - qParams.token, - qParams.email, - qParams.organizationUserId, - ); - policyList = Policy.fromListResponse(policies); - } catch (e) { - this.logService.error(e); - } - - if (policyList != null) { - const result = this.policyService.getResetPasswordPolicyOptions( - policyList, - qParams.organizationId, - ); - // Return true if policy enabled and auto-enroll enabled - return result[1] && result[0].autoEnrollEnabled; - } - - return false; - } - - private async prepareOrganizationInvitation(qParams: Params): Promise { - this.orgName = qParams.organizationName; - if (this.orgName != null) { - // Fix URL encoding of space issue with Angular - this.orgName = this.orgName.replace(/\+/g, " "); - } - await this.stateService.setOrganizationInvitation(qParams); - } - - private async accelerateInviteAcceptIfPossible(qParams: Params): Promise { - // Extract the query params we need to make routing acceleration decisions - const orgSsoIdentifier = qParams.orgSsoIdentifier; - const orgUserHasExistingUser = this.stringToNullOrBool(qParams.orgUserHasExistingUser); - - // if orgUserHasExistingUser is null, short circuit for backwards compatibility w/ older servers - if (orgUserHasExistingUser == null) { - return; - } - - // if user exists, send user to login - if (orgUserHasExistingUser) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/login"], { - queryParams: { email: qParams.email }, - }); - return; - } - - // no user exists; so either sign in via SSO and JIT provision one or simply register. - - if (orgSsoIdentifier) { - // We only send sso org identifier if the org has SSO enabled and the SSO policy required. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/sso"], { - queryParams: { email: qParams.email, identifier: orgSsoIdentifier }, - }); - return; - } - - // if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled - // then send user to create account - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/register"], { - queryParams: { email: qParams.email, fromOrgInvite: true }, - }); - return; - } - - private stringToNullOrBool(s: string | undefined): boolean | null { - if (s === undefined) { - return null; - } - return s.toLowerCase() === "true"; - } -} diff --git a/apps/web/src/app/auth/auth.module.ts b/apps/web/src/app/auth/auth.module.ts index 056b9f161f..6aa671558a 100644 --- a/apps/web/src/app/auth/auth.module.ts +++ b/apps/web/src/app/auth/auth.module.ts @@ -1,9 +1,10 @@ import { NgModule } from "@angular/core"; +import { AcceptOrganizationInviteModule } from "./organization-invite/accept-organization.module"; import { AuthSettingsModule } from "./settings/settings.module"; @NgModule({ - imports: [AuthSettingsModule], + imports: [AuthSettingsModule, AcceptOrganizationInviteModule], declarations: [], providers: [], exports: [AuthSettingsModule], diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index 8ff847c3a2..5a92815c91 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -1,9 +1,9 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; import { SharedModule } from "../../../shared"; @@ -27,10 +27,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, route: ActivatedRoute, - stateService: StateService, + authService: AuthService, private emergencyAccessService: EmergencyAccessService, ) { - super(router, platformUtilsService, i18nService, route, stateService); + super(router, platformUtilsService, i18nService, route, authService); } async authedHandler(qParams: Params): Promise { diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts index 2c97bd227f..991fe8b597 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -1,14 +1,27 @@ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; + +import { RouterService } from "../../../core"; +import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service"; @Component({ selector: "web-login-decryption-options", templateUrl: "login-decryption-options.component.html", }) export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { + protected routerService = inject(RouterService); + protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + override async createUser(): Promise { try { await super.createUser(); + + // Invites from TDE orgs go through here, but the invite is + // accepted while being enrolled in admin recovery. So we need to clear + // the redirect and stored org invite. + await this.routerService.getAndClearLoginRedirectUrl(); + await this.acceptOrganizationInviteService.clearOrganizationInvitation(); + await this.router.navigate(["/vault"]); } catch (error) { this.validationService.showError(error); diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 9f628b9389..51d46f46a4 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -15,12 +15,10 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -32,6 +30,8 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { flagEnabled } from "../../../utils/flags"; import { RouterService, StateService } from "../../core"; +import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../organization-invite/organization-invite"; @Component({ selector: "app-login", @@ -41,10 +41,11 @@ import { RouterService, StateService } from "../../core"; export class LoginComponent extends BaseLoginComponent implements OnInit { showResetPasswordAutoEnrollWarning = false; enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; - policies: ListResponse; + policies: Policy[]; showPasswordless = false; constructor( + private acceptOrganizationInviteService: AcceptOrganizationInviteService, devicesApiService: DevicesApiServiceAbstraction, appIdService: AppIdService, loginStrategyService: LoginStrategyServiceAbstraction, @@ -112,37 +113,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { await super.ngOnInit(); }); - const invite = await this.stateService.getOrganizationInvitation(); - if (invite != null) { - let policyList: Policy[] = null; - try { - this.policies = await this.policyApiService.getPoliciesByToken( - invite.organizationId, - invite.token, - invite.email, - invite.organizationUserId, - ); - policyList = Policy.fromListResponse(this.policies); - } catch (e) { - this.logService.error(e); - } - - if (policyList != null) { - const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions( - policyList, - invite.organizationId, - ); - // Set to true if policy enabled and auto-enroll enabled - this.showResetPasswordAutoEnrollWarning = - resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; - - this.policyService - .masterPasswordPolicyOptions$(policyList) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPasswordPolicyOptions) => { - this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions; - }); - } + // If there's an existing org invite, use it to get the password policies + const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite(); + if (orgInvite != null) { + await this.initPasswordPolicies(orgInvite); } } @@ -166,50 +140,69 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ) ) { const policiesData: { [id: string]: PolicyData } = {}; - this.policies.data.map((p) => (policiesData[p.id] = new PolicyData(p))); + this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p))); await this.policyService.replace(policiesData); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["update-password"]); + await this.router.navigate(["update-password"]); return; } } this.loginEmailService.clearValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); + await this.router.navigate([this.successRoute]); } - goToHint() { + async goToHint() { this.setLoginEmailValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigateByUrl("/hint"); + await this.router.navigateByUrl("/hint"); } - goToRegister() { + async goToRegister() { const email = this.formGroup.value.email; if (email) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/register"], { queryParams: { email: email } }); + await this.router.navigate(["/register"], { queryParams: { email: email } }); return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/register"]); + await this.router.navigate(["/register"]); } - protected override handleMigrateEncryptionKey(result: AuthResult): boolean { + protected override async handleMigrateEncryptionKey(result: AuthResult): Promise { if (!result.requiresEncryptionKeyMigration) { return false; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["migrate-legacy-encryption"]); + await this.router.navigate(["migrate-legacy-encryption"]); return true; } + + private async initPasswordPolicies(invite: OrganizationInvite): Promise { + try { + this.policies = await this.policyApiService.getPoliciesByToken( + invite.organizationId, + invite.token, + invite.email, + invite.organizationUserId, + ); + } catch (e) { + this.logService.error(e); + } + + if (this.policies == null) { + return; + } + const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions( + this.policies, + invite.organizationId, + ); + // Set to true if policy enabled and auto-enroll enabled + this.showResetPasswordAutoEnrollWarning = + resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; + + this.policyService + .masterPasswordPolicyOptions$(this.policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPasswordPolicyOptions) => { + this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions; + }); + } } diff --git a/apps/web/src/app/auth/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html similarity index 97% rename from apps/web/src/app/auth/accept-organization.component.html rename to apps/web/src/app/auth/organization-invite/accept-organization.component.html index 3aef47df22..f9dd3da5ed 100644 --- a/apps/web/src/app/auth/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -18,7 +18,7 @@

- {{ orgName }} + {{ orgName$ | async }} {{ email }}

{{ "joinOrganizationDesc" | i18n }}

diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts new file mode 100644 index 0000000000..fa5507b216 --- /dev/null +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -0,0 +1,94 @@ +import { Component } from "@angular/core"; +import { ActivatedRoute, Params, Router } from "@angular/router"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { BaseAcceptComponent } from "../../common/base.accept.component"; + +import { AcceptOrganizationInviteService } from "./accept-organization.service"; +import { OrganizationInvite } from "./organization-invite"; + +@Component({ + templateUrl: "accept-organization.component.html", +}) +export class AcceptOrganizationComponent extends BaseAcceptComponent { + orgName$ = this.acceptOrganizationInviteService.orgName$; + protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"]; + + constructor( + router: Router, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + route: ActivatedRoute, + authService: AuthService, + private acceptOrganizationInviteService: AcceptOrganizationInviteService, + ) { + super(router, platformUtilsService, i18nService, route, authService); + } + + async authedHandler(qParams: Params): Promise { + const invite = OrganizationInvite.fromParams(qParams); + const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(invite); + + if (!success) { + return; + } + + this.platformUtilService.showToast( + "success", + this.i18nService.t("inviteAccepted"), + invite.initOrganization + ? this.i18nService.t("inviteInitAcceptedDesc") + : this.i18nService.t("inviteAcceptedDesc"), + { timeout: 10000 }, + ); + + await this.router.navigate(["/vault"]); + } + + async unauthedHandler(qParams: Params): Promise { + const invite = OrganizationInvite.fromParams(qParams); + await this.acceptOrganizationInviteService.setOrganizationInvitation(invite); + await this.accelerateInviteAcceptIfPossible(invite); + } + + /** + * In certain scenarios, we want to accelerate the user through the accept org invite process + * For example, if the user has a BW account already, we want them to be taken to login instead of creation. + */ + private async accelerateInviteAcceptIfPossible(invite: OrganizationInvite): Promise { + // if orgUserHasExistingUser is null, we can't determine the user's status + // so we don't want to accelerate the process + if (invite.orgUserHasExistingUser == null) { + return; + } + + // if user exists, send user to login + if (invite.orgUserHasExistingUser) { + await this.router.navigate(["/login"], { + queryParams: { email: invite.email }, + }); + return; + } + + if (invite.orgSsoIdentifier) { + // We only send sso org identifier if the org has SSO enabled and the SSO policy required. + // Will JIT provision the user. + // Note: If the organization has Admin Recovery enabled, the user will be accepted into the org + // upon enrollment. The user should not be returned here. + await this.router.navigate(["/sso"], { + queryParams: { email: invite.email, identifier: invite.orgSsoIdentifier }, + }); + return; + } + + // if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled + // then send user to create account + await this.router.navigate(["/register"], { + queryParams: { email: invite.email, fromOrgInvite: true }, + }); + return; + } +} diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.module.ts b/apps/web/src/app/auth/organization-invite/accept-organization.module.ts new file mode 100644 index 0000000000..3dc0e14489 --- /dev/null +++ b/apps/web/src/app/auth/organization-invite/accept-organization.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../shared"; + +import { AcceptOrganizationComponent } from "./accept-organization.component"; +import { AcceptOrganizationInviteService } from "./accept-organization.service"; + +@NgModule({ + declarations: [AcceptOrganizationComponent], + imports: [SharedModule], + providers: [AcceptOrganizationInviteService], +}) +export class AcceptOrganizationInviteModule {} diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts new file mode 100644 index 0000000000..97a17a5997 --- /dev/null +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -0,0 +1,185 @@ +import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { FakeGlobalState } from "@bitwarden/common/spec/fake-state"; +import { OrgKey } from "@bitwarden/common/types/key"; + +import { I18nService } from "../../core/i18n.service"; + +import { + AcceptOrganizationInviteService, + ORGANIZATION_INVITE, +} from "./accept-organization.service"; +import { OrganizationInvite } from "./organization-invite"; + +describe("AcceptOrganizationInviteService", () => { + let sut: AcceptOrganizationInviteService; + let apiService: MockProxy; + let authService: MockProxy; + let cryptoService: MockProxy; + let encryptService: MockProxy; + let policyApiService: MockProxy; + let policyService: MockProxy; + let logService: MockProxy; + let organizationApiService: MockProxy; + let organizationUserService: MockProxy; + let i18nService: MockProxy; + let globalStateProvider: FakeGlobalStateProvider; + let globalState: FakeGlobalState; + + beforeEach(() => { + apiService = mock(); + authService = mock(); + cryptoService = mock(); + encryptService = mock(); + policyApiService = mock(); + policyService = mock(); + logService = mock(); + organizationApiService = mock(); + organizationUserService = mock(); + i18nService = mock(); + globalStateProvider = new FakeGlobalStateProvider(); + globalState = globalStateProvider.getFake(ORGANIZATION_INVITE); + + sut = new AcceptOrganizationInviteService( + apiService, + authService, + cryptoService, + encryptService, + policyApiService, + policyService, + logService, + organizationApiService, + organizationUserService, + i18nService, + globalStateProvider, + ); + }); + + describe("validateAndAcceptInvite", () => { + it("initializes an organization when given an invite where initOrganization is true", async () => { + cryptoService.makeOrgKey.mockResolvedValue([ + { encryptedString: "string" } as EncString, + "orgPrivateKey" as unknown as OrgKey, + ]); + cryptoService.makeKeyPair.mockResolvedValue([ + "orgPublicKey", + { encryptedString: "string" } as EncString, + ]); + encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString); + const invite = createOrgInvite({ initOrganization: true }); + + const result = await sut.validateAndAcceptInvite(invite); + + expect(result).toBe(true); + expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled(); + expect(apiService.refreshIdentityToken).toHaveBeenCalled(); + expect(globalState.nextMock).toHaveBeenCalledWith(null); + expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled(); + expect(authService.logOut).not.toHaveBeenCalled(); + }); + + it("logs out the user and stores the invite when a master password policy check is required", async () => { + const invite = createOrgInvite(); + policyApiService.getPoliciesByToken.mockResolvedValue([ + { + type: PolicyType.MasterPassword, + enabled: true, + } as Policy, + ]); + + const result = await sut.validateAndAcceptInvite(invite); + + expect(result).toBe(false); + expect(authService.logOut).toHaveBeenCalled(); + expect(globalState.nextMock).toHaveBeenCalledWith(invite); + }); + + it("clears the stored invite when a master password policy check is required but the stored invite doesn't match the provided one", async () => { + const storedInvite = createOrgInvite({ email: "wrongemail@example.com" }); + const providedInvite = createOrgInvite(); + await globalState.update(() => storedInvite); + policyApiService.getPoliciesByToken.mockResolvedValue([ + { + type: PolicyType.MasterPassword, + enabled: true, + } as Policy, + ]); + + const result = await sut.validateAndAcceptInvite(providedInvite); + + expect(result).toBe(false); + expect(authService.logOut).toHaveBeenCalled(); + expect(globalState.nextMock).toHaveBeenCalledWith(providedInvite); + }); + + it("accepts the invitation request when the organization doesn't have a master password policy", async () => { + const invite = createOrgInvite(); + policyApiService.getPoliciesByToken.mockResolvedValue([]); + + const result = await sut.validateAndAcceptInvite(invite); + + expect(result).toBe(true); + expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(apiService.refreshIdentityToken).toHaveBeenCalled(); + expect(globalState.nextMock).toHaveBeenCalledWith(null); + expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(authService.logOut).not.toHaveBeenCalled(); + }); + + it("accepts the invitation request when the org has a master password policy, but the user has already passed it", async () => { + const invite = createOrgInvite(); + policyApiService.getPoliciesByToken.mockResolvedValue([ + { + type: PolicyType.MasterPassword, + enabled: true, + } as Policy, + ]); + // an existing invite means the user has already passed the master password policy + await globalState.update(() => invite); + + policyService.getResetPasswordPolicyOptions.mockReturnValue([ + { + autoEnrollEnabled: false, + } as ResetPasswordPolicyOptions, + false, + ]); + + const result = await sut.validateAndAcceptInvite(invite); + + expect(result).toBe(true); + expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(authService.logOut).not.toHaveBeenCalled(); + }); + }); +}); + +function createOrgInvite(custom: Partial = {}): OrganizationInvite { + return Object.assign( + { + email: "user@example.com", + initOrganization: false, + orgSsoIdentifier: null, + orgUserHasExistingUser: false, + organizationId: "organizationId", + organizationName: "organizationName", + organizationUserId: "organizationUserId", + token: "token", + }, + custom, + ); +} diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts new file mode 100644 index 0000000000..e43023c37d --- /dev/null +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -0,0 +1,248 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject, firstValueFrom, map } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { + OrganizationUserAcceptRequest, + OrganizationUserAcceptInitRequest, +} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + GlobalState, + GlobalStateProvider, + KeyDefinition, + ORGANIZATION_INVITE_DISK, +} from "@bitwarden/common/platform/state"; +import { OrgKey } from "@bitwarden/common/types/key"; + +import { OrganizationInvite } from "./organization-invite"; + +// We're storing the organization invite for 2 reasons: +// 1. If the org requires a MP policy check, we need to keep track that the user has already been redirected when they return. +// 2. The MP policy check happens on login/register flows, we need to store the token to retrieve the policies then. +export const ORGANIZATION_INVITE = new KeyDefinition( + ORGANIZATION_INVITE_DISK, + "organizationInvite", + { + deserializer: (invite) => OrganizationInvite.fromJSON(invite), + }, +); + +@Injectable() +export class AcceptOrganizationInviteService { + private organizationInvitationState: GlobalState; + private orgNameSubject: BehaviorSubject = new BehaviorSubject(null); + private policyCache: Policy[]; + + // Fix URL encoding of space issue with Angular + orgName$ = this.orgNameSubject.pipe(map((orgName) => orgName.replace(/\+/g, " "))); + + constructor( + private readonly apiService: ApiService, + private readonly authService: AuthService, + private readonly cryptoService: CryptoService, + private readonly encryptService: EncryptService, + private readonly policyApiService: PolicyApiServiceAbstraction, + private readonly policyService: PolicyService, + private readonly logService: LogService, + private readonly organizationApiService: OrganizationApiServiceAbstraction, + private readonly organizationUserService: OrganizationUserService, + private readonly i18nService: I18nService, + private readonly globalStateProvider: GlobalStateProvider, + ) { + this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE); + } + + /** Returns the currently stored organization invite */ + async getOrganizationInvite(): Promise { + return await firstValueFrom(this.organizationInvitationState.state$); + } + + /** + * Stores a new organization invite + * @param invite an organization invite + * @throws if the invite is nullish + */ + async setOrganizationInvitation(invite: OrganizationInvite): Promise { + if (invite == null) { + throw new Error("Invite cannot be null. Use clearOrganizationInvitation instead."); + } + await this.organizationInvitationState.update(() => invite); + } + + /** Clears the currently stored organization invite */ + async clearOrganizationInvitation(): Promise { + await this.organizationInvitationState.update(() => null); + } + + /** + * Validates and accepts the organization invitation if possible. + * Note: Users might need to pass a MP policy check before accepting an invite to an existing organization. If the user + * has not passed this check, they will be logged out and the invite will be stored for later use. + * @param invite an organization invite + * @returns a promise that resolves a boolean indicating if the invite was accepted. + */ + async validateAndAcceptInvite(invite: OrganizationInvite): Promise { + if (invite == null) { + throw new Error("Invite cannot be null."); + } + + // Creation of a new org + if (invite.initOrganization) { + await this.acceptAndInitOrganization(invite); + return true; + } + + // Accepting an org invite from existing org + if (await this.masterPasswordPolicyCheckRequired(invite)) { + await this.setOrganizationInvitation(invite); + this.authService.logOut(() => { + /* Do nothing */ + }); + return false; + } + + // We know the user has already logged in and passed a MP policy check + await this.accept(invite); + return true; + } + + private async acceptAndInitOrganization(invite: OrganizationInvite): Promise { + await this.prepareAcceptAndInitRequest(invite).then((request) => + this.organizationUserService.postOrganizationUserAcceptInit( + invite.organizationId, + invite.organizationUserId, + request, + ), + ); + await this.apiService.refreshIdentityToken(); + await this.clearOrganizationInvitation(); + } + + private async prepareAcceptAndInitRequest( + invite: OrganizationInvite, + ): Promise { + const request = new OrganizationUserAcceptInitRequest(); + request.token = invite.token; + + const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey(); + const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey); + const collection = await this.encryptService.encrypt( + this.i18nService.t("defaultCollection"), + orgKey, + ); + + request.key = encryptedOrgKey.encryptedString; + request.keys = new OrganizationKeysRequest( + orgPublicKey, + encryptedOrgPrivateKey.encryptedString, + ); + request.collectionName = collection.encryptedString; + + return request; + } + + private async accept(invite: OrganizationInvite): Promise { + await this.prepareAcceptRequest(invite).then((request) => + this.organizationUserService.postOrganizationUserAccept( + invite.organizationId, + invite.organizationUserId, + request, + ), + ); + + await this.apiService.refreshIdentityToken(); + await this.clearOrganizationInvitation(); + } + + private async prepareAcceptRequest( + invite: OrganizationInvite, + ): Promise { + const request = new OrganizationUserAcceptRequest(); + request.token = invite.token; + + if (await this.resetPasswordEnrollRequired(invite)) { + const response = await this.organizationApiService.getKeys(invite.organizationId); + + if (response == null) { + throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); + } + + const publicKey = Utils.fromB64ToArray(response.publicKey); + + // RSA Encrypt user's encKey.key with organization public key + const userKey = await this.cryptoService.getUserKey(); + const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); + + // Add reset password key to accept request + request.resetPasswordKey = encryptedKey.encryptedString; + } + return request; + } + + private async resetPasswordEnrollRequired(invite: OrganizationInvite): Promise { + const policies = await this.getPolicies(invite); + + if (policies == null || policies.length === 0) { + return false; + } + + const result = this.policyService.getResetPasswordPolicyOptions( + policies, + invite.organizationId, + ); + // Return true if policy enabled and auto-enroll enabled + return result[1] && result[0].autoEnrollEnabled; + } + + private async masterPasswordPolicyCheckRequired(invite: OrganizationInvite): Promise { + const policies = await this.getPolicies(invite); + + if (policies == null || policies.length === 0) { + return false; + } + const hasMasterPasswordPolicy = policies.some( + (p) => p.type === PolicyType.MasterPassword && p.enabled, + ); + + let storedInvite = await this.getOrganizationInvite(); + if (storedInvite?.email !== invite.email) { + // clear stored invites if the email doesn't match + await this.clearOrganizationInvitation(); + storedInvite = null; + } + // if we don't have an org invite stored, we know the user hasn't been redirected yet to check the MP policy + const hasNotCheckedMasterPasswordYet = storedInvite == null; + return hasMasterPasswordPolicy && hasNotCheckedMasterPasswordYet; + } + + private async getPolicies(invite: OrganizationInvite): Promise { + // if policies are not cached, fetch them + if (this.policyCache == null) { + try { + this.policyCache = await this.policyApiService.getPoliciesByToken( + invite.organizationId, + invite.token, + invite.email, + invite.organizationUserId, + ); + } catch (e) { + this.logService.error(e); + } + } + + return this.policyCache; + } +} diff --git a/apps/web/src/app/auth/organization-invite/organization-invite.ts b/apps/web/src/app/auth/organization-invite/organization-invite.ts new file mode 100644 index 0000000000..9a0bbf8334 --- /dev/null +++ b/apps/web/src/app/auth/organization-invite/organization-invite.ts @@ -0,0 +1,30 @@ +import { Params } from "@angular/router"; +import { Jsonify } from "type-fest"; + +export class OrganizationInvite { + email: string; + initOrganization: boolean; + orgSsoIdentifier: string; + orgUserHasExistingUser: boolean; + organizationId: string; + organizationName: string; + organizationUserId: string; + token: string; + + static fromJSON(json: Jsonify) { + return Object.assign(new OrganizationInvite(), json); + } + + static fromParams(params: Params): OrganizationInvite { + return Object.assign(new OrganizationInvite(), { + email: params.email, + initOrganization: params.initOrganization?.toLocaleLowerCase() === "true", + orgSsoIdentifier: params.orgSsoIdentifier, + orgUserHasExistingUser: params.orgUserHasExistingUser?.toLocaleLowerCase() === "true", + organizationId: params.organizationId, + organizationName: params.organizationName, + organizationUserId: params.organizationUserId, + token: params.token, + }); + } +} diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index 6c1b3122c6..4532cf1405 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; +import { RegisterRequest } from "@bitwarden/common/models/request/register.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -19,6 +20,8 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; + @Component({ selector: "app-register-form", templateUrl: "./register-form.component.html", @@ -48,6 +51,7 @@ export class RegisterFormComponent extends BaseRegisterComponent { logService: LogService, auditService: AuditService, dialogService: DialogService, + acceptOrgInviteService: AcceptOrganizationInviteService, ) { super( formValidationErrorService, @@ -65,6 +69,16 @@ export class RegisterFormComponent extends BaseRegisterComponent { auditService, dialogService, ); + super.modifyRegisterRequest = async (request: RegisterRequest) => { + // Org invites are deep linked. Non-existent accounts are redirected to the register page. + // Org user id and token are included here only for validation and two factor purposes. + const orgInvite = await acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite != null) { + request.organizationUserId = orgInvite.organizationUserId; + request.token = orgInvite.token; + } + // Invite is accepted after login (on deep link redirect). + }; } async ngOnInit() { diff --git a/apps/web/src/app/auth/set-password.component.ts b/apps/web/src/app/auth/set-password.component.ts index accde2e9a0..ccd329dd64 100644 --- a/apps/web/src/app/auth/set-password.component.ts +++ b/apps/web/src/app/auth/set-password.component.ts @@ -1,9 +1,30 @@ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; + +import { RouterService } from "../core"; + +import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service"; @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", }) -export class SetPasswordComponent extends BaseSetPasswordComponent {} +export class SetPasswordComponent extends BaseSetPasswordComponent { + routerService = inject(RouterService); + acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + + protected override async onSetPasswordSuccess( + masterKey: MasterKey, + userKey: [UserKey, EncString], + keyPair: [string, EncString], + ): Promise { + await super.onSetPasswordSuccess(masterKey, userKey, keyPair); + // SSO JIT accepts org invites when setting their MP, meaning + // we can clear the deep linked url for accepting it. + await this.routerService.getAndClearLoginRedirectUrl(); + await this.acceptOrganizationInviteService.clearOrganizationInvitation(); + } +} diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts index 9879743a58..a7916ae946 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts @@ -12,15 +12,16 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PlanType } from "@bitwarden/common/billing/enums"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { RouterService } from "../../core"; import { SharedModule } from "../../shared"; +import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../organization-invite/organization-invite"; import { TrialInitiationComponent } from "./trial-initiation.component"; import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; @@ -36,12 +37,16 @@ describe("TrialInitiationComponent", () => { let stateServiceMock: MockProxy; let policyApiServiceMock: MockProxy; let policyServiceMock: MockProxy; + let routerServiceMock: MockProxy; + let acceptOrgInviteServiceMock: MockProxy; beforeEach(() => { // only define services directly that we want to mock return values in this component stateServiceMock = mock(); policyApiServiceMock = mock(); policyServiceMock = mock(); + routerServiceMock = mock(); + acceptOrgInviteServiceMock = mock(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -81,7 +86,11 @@ describe("TrialInitiationComponent", () => { }, { provide: RouterService, - useValue: mock(), + useValue: routerServiceMock, + }, + { + provide: AcceptOrganizationInviteService, + useValue: acceptOrgInviteServiceMock, }, ], schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) @@ -100,8 +109,8 @@ describe("TrialInitiationComponent", () => { // These tests demonstrate mocking service calls describe("onInit() enforcedPolicyOptions", () => { - it("should not set enforcedPolicyOptions if state service returns no invite", async () => { - stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(null); + it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => { + acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null); // Need to recreate component with new service mock fixture = TestBed.createComponent(TrialInitiationComponent); component = fixture.componentInstance; @@ -109,37 +118,31 @@ describe("TrialInitiationComponent", () => { expect(component.enforcedPolicyOptions).toBe(undefined); }); - it("should set enforcedPolicyOptions if state service returns an invite", async () => { + it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => { // Set up service method mocks - stateServiceMock.getOrganizationInvitation.mockReturnValueOnce( - Promise.resolve({ - organizationId: testOrgId, - token: "token", - email: "testEmail", - organizationUserId: "123", - }), - ); + acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({ + organizationId: testOrgId, + token: "token", + email: "testEmail", + organizationUserId: "123", + } as OrganizationInvite); policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce( - Promise.resolve({ - data: [ - { - id: "345", - organizationId: testOrgId, - type: 1, - data: [ - { - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - }, - ], - enabled: true, + Promise.resolve([ + { + id: "345", + organizationId: testOrgId, + type: 1, + data: { + minComplexity: 4, + minLength: 10, + requireLower: null, + requireNumbers: null, + requireSpecial: null, + requireUpper: null, }, - ], - } as ListResponse), + enabled: true, + }, + ] as Policy[]), ); policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue( of({ diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts index 52a4120b1a..d02b2c9e2e 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts @@ -14,13 +14,14 @@ import { ProductType } from "@bitwarden/common/enums"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { OrganizationCreatedEvent, SubscriptionProduct, TrialOrganizationType, } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; +import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../organization-invite/organization-invite"; import { RouterService } from "./../../core/router.service"; import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; @@ -121,12 +122,12 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { protected router: Router, private formBuilder: UntypedFormBuilder, private titleCasePipe: TitleCasePipe, - private stateService: StateService, private logService: LogService, private policyApiService: PolicyApiServiceAbstraction, private policyService: PolicyService, private i18nService: I18nService, private routerService: RouterService, + private acceptOrgInviteService: AcceptOrganizationInviteService, ) {} async ngOnInit(): Promise { @@ -180,30 +181,10 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { : "Password Manager trial from marketing website"; }); - const invite = await this.stateService.getOrganizationInvitation(); - if (invite != null) { - try { - const policies = await this.policyApiService.getPoliciesByToken( - invite.organizationId, - invite.token, - invite.email, - invite.organizationUserId, - ); - if (policies.data != null) { - this.policies = Policy.fromListResponse(policies); - } - } catch (e) { - this.logService.error(e); - } - } - - if (this.policies != null) { - this.policyService - .masterPasswordPolicyOptions$(this.policies) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPasswordPolicyOptions) => { - this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; - }); + // If there's a deep linked org invite, use it to get the password policies + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite != null) { + await this.initPasswordPolicies(orgInvite); } this.orgInfoFormGroup.controls.name.valueChanges @@ -304,5 +285,31 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } } + private async initPasswordPolicies(invite: OrganizationInvite): Promise { + if (invite == null) { + return; + } + + try { + this.policies = await this.policyApiService.getPoliciesByToken( + invite.organizationId, + invite.token, + invite.email, + invite.organizationUserId, + ); + } catch (e) { + this.logService.error(e); + } + + if (this.policies != null) { + this.policyService + .masterPasswordPolicyOptions$(this.policies) + .pipe(takeUntil(this.destroy$)) + .subscribe((enforcedPasswordPolicyOptions) => { + this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; + }); + } + } + protected readonly SubscriptionProduct = SubscriptionProduct; } diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index b8cfb47db1..da62a6812f 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -1,60 +1,24 @@ -import { Component } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component, inject } from "@angular/core"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { DialogService } from "@bitwarden/components"; + +import { RouterService } from "../core"; + +import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service"; @Component({ selector: "app-update-password", templateUrl: "update-password.component.html", }) export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { - constructor( - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - policyService: PolicyService, - cryptoService: CryptoService, - messagingService: MessagingService, - apiService: ApiService, - logService: LogService, - stateService: StateService, - userVerificationService: UserVerificationService, - dialogService: DialogService, - kdfConfigService: KdfConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - ) { - super( - router, - i18nService, - platformUtilsService, - passwordGenerationService, - policyService, - cryptoService, - messagingService, - apiService, - stateService, - userVerificationService, - logService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, - ); + private routerService = inject(RouterService); + private acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + + override async cancel() { + // clearing the login redirect url so that the user + // does not join the organization if they cancel + await this.routerService.getAndClearLoginRedirectUrl(); + await this.acceptOrganizationInviteService.clearOrganizationInvitation(); + await super.cancel(); } } diff --git a/apps/web/src/app/common/base.accept.component.ts b/apps/web/src/app/common/base.accept.component.ts index cad1d90088..7c35751aea 100644 --- a/apps/web/src/app/common/base.accept.component.ts +++ b/apps/web/src/app/common/base.accept.component.ts @@ -1,11 +1,12 @@ import { Directive, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { Subject } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { first, switchMap, takeUntil } from "rxjs/operators"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Directive() export abstract class BaseAcceptComponent implements OnInit { @@ -25,7 +26,7 @@ export abstract class BaseAcceptComponent implements OnInit { protected platformUtilService: PlatformUtilsService, protected i18nService: I18nService, protected route: ActivatedRoute, - protected stateService: StateService, + protected authService: AuthService, ) {} abstract authedHandler(qParams: Params): Promise; @@ -41,10 +42,10 @@ export abstract class BaseAcceptComponent implements OnInit { ); let errorMessage: string = null; if (!error) { - this.authed = await this.stateService.getIsAuthenticated(); this.email = qParams.email; - if (this.authed) { + const status = await firstValueFrom(this.authService.activeAccountStatus$); + if (status !== AuthenticationStatus.LoggedOut) { try { await this.authedHandler(qParams); } catch (e) { diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index caebb22733..c0c1ec2640 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -92,7 +92,7 @@ export class RouterService { /** * Fetch and clear persisted LoginRedirectUrl if present in state */ - async getAndClearLoginRedirectUrl(): Promise | undefined { + async getAndClearLoginRedirectUrl(): Promise { const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$); if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) { diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 93d68d31b3..e543a6f083 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -17,7 +17,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; -import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link.guard"; import { HintComponent } from "./auth/hint.component"; import { LockComponent } from "./auth/lock.component"; @@ -25,6 +24,7 @@ import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-o import { LoginViaAuthRequestComponent } from "./auth/login/login-via-auth-request.component"; import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component"; import { LoginComponent } from "./auth/login/login.component"; +import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; import { RemovePasswordComponent } from "./auth/remove-password.component"; @@ -120,8 +120,8 @@ const routes: Routes = [ { path: "verify-email", component: VerifyEmailTokenComponent }, { path: "accept-organization", - component: AcceptOrganizationComponent, canActivate: [deepLinkGuard()], + component: AcceptOrganizationComponent, data: { titleId: "joinOrganization", doNotSaveUrl: false }, }, { diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index cf273ee682..79478d0cae 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -20,7 +20,6 @@ import { ProvidersComponent } from "../admin-console/providers/providers.compone import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; -import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; import { HintComponent } from "../auth/hint.component"; import { LockComponent } from "../auth/lock.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; @@ -120,7 +119,6 @@ import { SharedModule } from "./shared.module"; ], declarations: [ AcceptFamilySponsorshipComponent, - AcceptOrganizationComponent, AccountComponent, AddEditComponent, AddEditCustomFieldsComponent, @@ -193,7 +191,6 @@ import { SharedModule } from "./shared.module"; exports: [ UserVerificationModule, PremiumBadgeComponent, - AcceptOrganizationComponent, AccountComponent, AddEditComponent, AddEditCustomFieldsComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index d9d553d283..2c6742611d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -3,9 +3,9 @@ import { ActivatedRoute, Params, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; @Component({ @@ -23,11 +23,11 @@ export class AcceptProviderComponent extends BaseAcceptComponent { router: Router, i18nService: I18nService, route: ActivatedRoute, - stateService: StateService, + authService: AuthService, private apiService: ApiService, platformUtilService: PlatformUtilsService, ) { - super(router, platformUtilService, i18nService, route, stateService); + super(router, platformUtilService, i18nService, route, authService); } async authedHandler(qParams: Params) { diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index bcdf747406..cfef72c435 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -155,7 +155,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, if (this.handleCaptchaRequired(response)) { return; - } else if (this.handleMigrateEncryptionKey(response)) { + } else if (await this.handleMigrateEncryptionKey(response)) { return; } else if (response.requiresTwoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { @@ -218,9 +218,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, } this.setLoginEmailValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/login-with-device"]); + await this.router.navigate(["/login-with-device"]); } async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { @@ -310,7 +308,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, // Legacy accounts used the master key to encrypt data. Migration is required // but only performed on web - protected handleMigrateEncryptionKey(result: AuthResult): boolean { + protected async handleMigrateEncryptionKey(result: AuthResult): Promise { if (!result.requiresEncryptionKeyMigration) { return false; } diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 2ba7669290..e3197355dc 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -78,6 +78,10 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn protected captchaBypassToken: string = null; + // allows for extending classes to modify the register request before sending + // currently used by web to add organization invitation details + protected modifyRegisterRequest: (request: RegisterRequest) => Promise; + constructor( protected formValidationErrorService: FormValidationErrorsService, protected formBuilder: UntypedFormBuilder, @@ -290,10 +294,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn kdfConfig.iterations, ); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); - const orgInvite = await this.stateService.getOrganizationInvitation(); - if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { - request.token = orgInvite.token; - request.organizationUserId = orgInvite.organizationUserId; + if (this.modifyRegisterRequest) { + await this.modifyRegisterRequest(request); } return request; } diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 3b709b3e7f..62e0359038 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -72,10 +72,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { } async cancel() { - await this.stateService.setOrganizationInvitation(null); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/vault"]); + await this.router.navigate(["/vault"]); } async setupSubmitActions(): Promise { diff --git a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts index 1e941fd654..c601aad060 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts @@ -1,6 +1,7 @@ import { ListResponse } from "../../../models/response/list.response"; import { PolicyType } from "../../enums"; import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; +import { Policy } from "../../models/domain/policy"; import { PolicyRequest } from "../../models/request/policy.request"; import { PolicyResponse } from "../../models/response/policy.response"; @@ -13,7 +14,7 @@ export class PolicyApiServiceAbstraction { token: string, email: string, organizationUserId: string, - ) => Promise>; + ) => Promise; getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise; putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise; diff --git a/libs/common/src/admin-console/models/data/policy.data.ts b/libs/common/src/admin-console/models/data/policy.data.ts index 35846f2072..54185c84da 100644 --- a/libs/common/src/admin-console/models/data/policy.data.ts +++ b/libs/common/src/admin-console/models/data/policy.data.ts @@ -1,5 +1,6 @@ import { PolicyId } from "../../../types/guid"; import { PolicyType } from "../../enums"; +import { Policy } from "../domain/policy"; import { PolicyResponse } from "../response/policy.response"; export class PolicyData { @@ -20,4 +21,8 @@ export class PolicyData { this.data = response.data; this.enabled = response.enabled; } + + static fromPolicy(policy: Policy): PolicyData { + return Object.assign(new PolicyData(), policy); + } } diff --git a/libs/common/src/admin-console/services/policy/policy-api.service.ts b/libs/common/src/admin-console/services/policy/policy-api.service.ts index c7f093286e..086bbea1d2 100644 --- a/libs/common/src/admin-console/services/policy/policy-api.service.ts +++ b/libs/common/src/admin-console/services/policy/policy-api.service.ts @@ -47,7 +47,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { token: string, email: string, organizationUserId: string, - ): Promise> { + ): Promise { const r = await this.apiService.send( "GET", "/organizations/" + @@ -63,7 +63,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { false, true, ); - return new ListResponse(r, PolicyResponse); + return Policy.fromListResponse(new ListResponse(r, PolicyResponse)); } private async getMasterPasswordPolicyResponseForOrgUser( diff --git a/libs/common/src/auth/abstractions/password-reset-enrollment.service.abstraction.ts b/libs/common/src/auth/abstractions/password-reset-enrollment.service.abstraction.ts index 4089278567..7785798f5c 100644 --- a/libs/common/src/auth/abstractions/password-reset-enrollment.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/password-reset-enrollment.service.abstraction.ts @@ -3,11 +3,15 @@ import { UserKey } from "../../types/key"; export abstract class PasswordResetEnrollmentServiceAbstraction { /* * Checks the user's enrollment status and enrolls them if required + * NOTE: Will also enroll the user in the organization if in the + * invited status */ abstract enrollIfRequired(organizationSsoIdentifier: string): Promise; /** * Enroll current user in password reset + * NOTE: Will also enroll the user in the organization if in the + * invited status * @param organizationId - Organization in which to enroll the user * @returns Promise that resolves when the user is enrolled * @throws Error if the action fails @@ -16,6 +20,8 @@ export abstract class PasswordResetEnrollmentServiceAbstraction { /** * Enroll user in password reset + * NOTE: Will also enroll the user in the organization if in the + * invited status * @param organizationId - Organization in which to enroll the user * @param userId - User to enroll * @param userKey - User's symmetric key diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index b2ea27ecb0..b73a0ec784 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -101,8 +101,6 @@ export abstract class StateService { setLastSync: (value: string, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; - getOrganizationInvitation: (options?: StorageOptions) => Promise; - setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; getPasswordGenerationOptions: (options?: StorageOptions) => Promise; setPasswordGenerationOptions: ( value: PasswordGeneratorOptions, diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 2d48f09e92..f4b22702df 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,5 +1,4 @@ export class GlobalState { - organizationInvitation?: any; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0339baa3fa..6906c03190 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -474,23 +474,6 @@ export class StateService< ); } - async getOrganizationInvitation(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.organizationInvitation; - } - - async setOrganizationInvitation(value: any, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - globals.organizationInvitation = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getPasswordGenerationOptions(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 986c51f4b7..f342742943 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -63,6 +63,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); +export const ORGANIZATION_INVITE_DISK = new StateDefinition("organizationInvite", "disk"); export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition( "vaultTimeoutSettings", "disk",