mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
PM-1391-Added previous-url to global-state (#5733)
* added previous-url to global-state * updated storage of previousUrl for SSO/MFA flows * revert file changes * added post login routing * Clear PreviousUrl from storage on new Login * Components do not call StateService anymore * removed needed query params * refactored components to use RouterService * fixed build error * fixed mfa component * updated logic for previous Url * removed unneeded base implementation * Added state call for Redirect Guard * Fixed test cases * Remove routing service calls * renamed global field, changed routing to guard * reverting constructor changes and git lint issue * fixing constructor ordering * fixing diffs to be clearer on actual cahnges. * addressing accepting emergency access case * refactor and add locked state logic * refactor name of guard to be more clear * Added comments and tests * comments + support lock page deep linking + code ownership * readability updates * Combined guards and specs updated routing * Update oss-routing.module.ts * fixed stroybook build
This commit is contained in:
parent
a6e3d4d244
commit
f1691a5ef1
@ -16,13 +16,14 @@ import { OrganizationPermissionsGuard } from "../../admin-console/organizations/
|
|||||||
import { OrganizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
import { OrganizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
||||||
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
||||||
import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component";
|
import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component";
|
||||||
|
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
||||||
import { VaultModule } from "../../vault/org-vault/vault.module";
|
import { VaultModule } from "../../vault/org-vault/vault.module";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: ":organizationId",
|
path: ":organizationId",
|
||||||
component: OrganizationLayoutComponent,
|
component: OrganizationLayoutComponent,
|
||||||
canActivate: [AuthGuard, OrganizationPermissionsGuard],
|
canActivate: [deepLinkGuard(), AuthGuard, OrganizationPermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
organizationPermissions: canAccessOrgAdmin,
|
organizationPermissions: canAccessOrgAdmin,
|
||||||
},
|
},
|
||||||
|
@ -111,14 +111,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.notificationsService.updateConnection(false);
|
this.notificationsService.updateConnection(false);
|
||||||
break;
|
break;
|
||||||
case "loggedOut":
|
case "loggedOut":
|
||||||
this.routerService.setPreviousUrl(null);
|
|
||||||
this.notificationsService.updateConnection(false);
|
this.notificationsService.updateConnection(false);
|
||||||
break;
|
break;
|
||||||
case "unlocked":
|
case "unlocked":
|
||||||
this.notificationsService.updateConnection(false);
|
this.notificationsService.updateConnection(false);
|
||||||
break;
|
break;
|
||||||
case "authBlocked":
|
case "authBlocked":
|
||||||
this.routerService.setPreviousUrl(message.url);
|
|
||||||
this.router.navigate(["/"]);
|
this.router.navigate(["/"]);
|
||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
@ -132,7 +130,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(["lock"]);
|
this.router.navigate(["lock"]);
|
||||||
break;
|
break;
|
||||||
case "lockedUrl":
|
case "lockedUrl":
|
||||||
this.routerService.setPreviousUrl(message.url);
|
|
||||||
break;
|
break;
|
||||||
case "syncStarted":
|
case "syncStarted":
|
||||||
break;
|
break;
|
||||||
|
@ -36,7 +36,6 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
|||||||
async authedHandler(qParams: Params): Promise<void> {
|
async authedHandler(qParams: Params): Promise<void> {
|
||||||
this.actionPromise = this.emergencyAccessService.accept(qParams.id, qParams.token);
|
this.actionPromise = this.emergencyAccessService.accept(qParams.id, qParams.token);
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
await this.stateService.setEmergencyAccessInvitation(null);
|
|
||||||
this.platformUtilService.showToast(
|
this.platformUtilService.showToast(
|
||||||
"success",
|
"success",
|
||||||
this.i18nService.t("inviteAccepted"),
|
this.i18nService.t("inviteAccepted"),
|
||||||
@ -52,8 +51,5 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
|||||||
// Fix URL encoding of space issue with Angular
|
// Fix URL encoding of space issue with Angular
|
||||||
this.name = this.name.replace(/\+/g, " ");
|
this.name = this.name.replace(/\+/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the invitation to state so sso logins can find it later
|
|
||||||
await this.stateService.setEmergencyAccessInvitation(qParams);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
190
apps/web/src/app/auth/guards/deep-link.guard.spec.ts
Normal file
190
apps/web/src/app/auth/guards/deep-link.guard.spec.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { Router, provideRouter } from "@angular/router";
|
||||||
|
import { RouterTestingHarness } from "@angular/router/testing";
|
||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
|
import { RouterService } from "../../core/router.service";
|
||||||
|
|
||||||
|
import { deepLinkGuard } from "./deep-link.guard";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "",
|
||||||
|
})
|
||||||
|
export class GuardedRouteTestComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "",
|
||||||
|
})
|
||||||
|
export class LockTestComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "",
|
||||||
|
})
|
||||||
|
export class RedirectTestComponent {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are assuming the guard is always being called. We are creating routes using the
|
||||||
|
* RouterTestingHarness.
|
||||||
|
*
|
||||||
|
* when persisting a URL to storage we don't care wether or not the user is locked or logged out.
|
||||||
|
* We only care about where the user is going, and has been.
|
||||||
|
*
|
||||||
|
* We are testing the activatedComponent because we are testing that the guard redirects when a user is
|
||||||
|
* unlocked.
|
||||||
|
*/
|
||||||
|
describe("Deep Link Guard", () => {
|
||||||
|
let authService: MockProxy<AuthService>;
|
||||||
|
let routerService: MockProxy<RouterService>;
|
||||||
|
let routerHarness: RouterTestingHarness;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
authService = mock<AuthService>();
|
||||||
|
routerService = mock<RouterService>();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: RouterService, useValue: routerService },
|
||||||
|
provideRouter([
|
||||||
|
{
|
||||||
|
path: "guarded-route",
|
||||||
|
component: GuardedRouteTestComponent,
|
||||||
|
canActivate: [deepLinkGuard()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "lock-route",
|
||||||
|
component: LockTestComponent,
|
||||||
|
canActivate: [deepLinkGuard()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "redirect-route",
|
||||||
|
component: RedirectTestComponent,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
routerHarness = await RouterTestingHarness.create();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User's vault times out
|
||||||
|
it('should persist routerService.previousUrl when routerService.previousUrl does not contain "lock"', async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||||
|
routerService.getPreviousUrl.mockReturnValue("/previous-url");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/lock-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).toHaveBeenCalledWith("/previous-url");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User's vault times out and previousUrl contains "lock"
|
||||||
|
it('should not persist routerService.previousUrl when routerService.previousUrl contains "lock"', async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||||
|
routerService.getPreviousUrl.mockReturnValue("/lock");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/lock-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User's vault times out and previousUrl is undefined
|
||||||
|
it("should not persist routerService.previousUrl when routerService.previousUrl is undefined", async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||||
|
routerService.getPreviousUrl.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/lock-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User tries to deep link to a guarded route and is logged out
|
||||||
|
it('should persist currentUrl when currentUrl does not contain "lock"', async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/guarded-route?item=123");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).toHaveBeenCalledWith("/guarded-route?item=123");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User tries to deep link to "lock"
|
||||||
|
it('should not persist currentUrl if the currentUrl contains "lock"', async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/lock-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User tries to deep link to a guarded route from the lock page
|
||||||
|
it("should persist currentUrl over previousUrl", async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||||
|
routerService.getPreviousUrl.mockReturnValue("/previous-url");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/guarded-route?item=123");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).toHaveBeenCalledWith("/guarded-route?item=123");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: user tries to deep link and is unlocked
|
||||||
|
it("should not persist any URL if the user is unlocked", async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await routerHarness.navigateByUrl("/guarded-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User is redirected
|
||||||
|
it("should redirect user", async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
|
routerService.getAndClearLoginRedirectUrl.mockResolvedValue("/redirect-route");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const activatedComponent = await routerHarness.navigateByUrl("/guarded-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
expect(TestBed.inject(Router).url).toEqual("/redirect-route");
|
||||||
|
expect(activatedComponent).toBeInstanceOf(RedirectTestComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Story: User is not redirected
|
||||||
|
it("should not redirect user", async () => {
|
||||||
|
// Arrange
|
||||||
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
|
routerService.getAndClearLoginRedirectUrl.mockResolvedValue("");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const activatedComponent = await routerHarness.navigateByUrl("/guarded-route");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||||
|
expect(TestBed.inject(Router).url).toEqual("/guarded-route");
|
||||||
|
expect(activatedComponent).toBeInstanceOf(GuardedRouteTestComponent);
|
||||||
|
});
|
||||||
|
});
|
56
apps/web/src/app/auth/guards/deep-link.guard.ts
Normal file
56
apps/web/src/app/auth/guards/deep-link.guard.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { CanActivateFn, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
import { RouterService } from "../../core/router.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard to persist and apply deep links to handle users who are not unlocked.
|
||||||
|
* @returns returns true. If user is not Unlocked will store URL to state for redirect once
|
||||||
|
* user is unlocked/Authenticated.
|
||||||
|
*/
|
||||||
|
export function deepLinkGuard(): CanActivateFn {
|
||||||
|
return async (route, routerState) => {
|
||||||
|
// Inject Services
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const routerService = inject(RouterService);
|
||||||
|
|
||||||
|
// Fetch State
|
||||||
|
const currentUrl = routerState.url;
|
||||||
|
const transientPreviousUrl = routerService.getPreviousUrl();
|
||||||
|
const authStatus = await authService.getAuthStatus();
|
||||||
|
|
||||||
|
// Evaluate State
|
||||||
|
/** before anything else, check if the user is already unlocked. */
|
||||||
|
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||||
|
const persistedPreLoginUrl = await routerService.getAndClearLoginRedirectUrl();
|
||||||
|
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||||
|
return router.navigateByUrl(persistedPreLoginUrl);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* At this point the user is either `locked` or `loggedOut`, it doesn't matter.
|
||||||
|
* We opt to persist the currentUrl over the transient previousUrl. This supports
|
||||||
|
* the case where a user is locked out of their vault and they deep link from
|
||||||
|
* the "lock" page.
|
||||||
|
*
|
||||||
|
* When the user is locked out of their vault the currentUrl contains "lock" so it will
|
||||||
|
* not be persisted, the previousUrl will be persisted instead.
|
||||||
|
*/
|
||||||
|
if (isValidUrl(currentUrl)) {
|
||||||
|
await routerService.persistLoginRedirectUrl(currentUrl);
|
||||||
|
} else if (isValidUrl(transientPreviousUrl)) {
|
||||||
|
await routerService.persistLoginRedirectUrl(transientPreviousUrl);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValidUrl(url: string | null | undefined): boolean {
|
||||||
|
return !Utils.isNullOrEmpty(url) && !url?.toLocaleLowerCase().includes("lock");
|
||||||
|
}
|
||||||
|
}
|
@ -19,8 +19,6 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
|||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { RouterService } from "../core";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-lock",
|
selector: "app-lock",
|
||||||
templateUrl: "lock.component.html",
|
templateUrl: "lock.component.html",
|
||||||
@ -35,7 +33,6 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
vaultTimeoutService: VaultTimeoutService,
|
vaultTimeoutService: VaultTimeoutService,
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
private routerService: RouterService,
|
|
||||||
stateService: StateService,
|
stateService: StateService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
@ -72,10 +69,6 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
this.onSuccessfulSubmit = async () => {
|
this.onSuccessfulSubmit = async () => {
|
||||||
const previousUrl = this.routerService.getPreviousUrl();
|
|
||||||
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
|
||||||
this.successRoute = previousUrl;
|
|
||||||
}
|
|
||||||
this.router.navigateByUrl(this.successRoute);
|
this.router.navigateByUrl(this.successRoute);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -172,13 +172,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousUrl = this.routerService.getPreviousUrl();
|
this.loginService.clearValues();
|
||||||
if (previousUrl) {
|
this.router.navigate([this.successRoute]);
|
||||||
this.router.navigateByUrl(previousUrl);
|
|
||||||
} else {
|
|
||||||
this.loginService.clearValues();
|
|
||||||
this.router.navigate([this.successRoute]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToHint() {
|
goToHint() {
|
||||||
|
@ -7,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
|
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
|
||||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
|
||||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
@ -39,7 +38,6 @@ export class SsoComponent extends BaseSsoComponent {
|
|||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||||
private loginService: LoginService,
|
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
configService: ConfigServiceAbstraction
|
configService: ConfigServiceAbstraction
|
||||||
) {
|
) {
|
||||||
@ -64,21 +62,6 @@ export class SsoComponent extends BaseSsoComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
|
|
||||||
// if we have an emergency access invite, redirect to emergency access
|
|
||||||
const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation();
|
|
||||||
if (emergencyAccessInvite != null) {
|
|
||||||
this.onSuccessfulLoginNavigate = async () => {
|
|
||||||
this.router.navigate(["/accept-emergency"], {
|
|
||||||
queryParams: {
|
|
||||||
id: emergencyAccessInvite.id,
|
|
||||||
name: emergencyAccessInvite.name,
|
|
||||||
email: emergencyAccessInvite.email,
|
|
||||||
token: emergencyAccessInvite.token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
if (qParams.identifier != null) {
|
if (qParams.identifier != null) {
|
||||||
|
@ -18,8 +18,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
import { RouterService } from "../core";
|
|
||||||
|
|
||||||
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -44,7 +42,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
twoFactorService: TwoFactorService,
|
twoFactorService: TwoFactorService,
|
||||||
appIdService: AppIdService,
|
appIdService: AppIdService,
|
||||||
private routerService: RouterService,
|
|
||||||
loginService: LoginService,
|
loginService: LoginService,
|
||||||
configService: ConfigServiceAbstraction,
|
configService: ConfigServiceAbstraction,
|
||||||
@Inject(WINDOW) protected win: Window
|
@Inject(WINDOW) protected win: Window
|
||||||
@ -97,29 +94,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
|
|
||||||
goAfterLogIn = async () => {
|
goAfterLogIn = async () => {
|
||||||
this.loginService.clearValues();
|
this.loginService.clearValues();
|
||||||
const previousUrl = this.routerService.getPreviousUrl();
|
this.router.navigate([this.successRoute], {
|
||||||
if (previousUrl) {
|
queryParams: {
|
||||||
this.router.navigateByUrl(previousUrl);
|
identifier: this.orgIdentifier,
|
||||||
} else {
|
},
|
||||||
// if we have an emergency access invite, redirect to emergency access
|
});
|
||||||
const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation();
|
|
||||||
if (emergencyAccessInvite != null) {
|
|
||||||
this.router.navigate(["/accept-emergency"], {
|
|
||||||
queryParams: {
|
|
||||||
id: emergencyAccessInvite.id,
|
|
||||||
name: emergencyAccessInvite.name,
|
|
||||||
email: emergencyAccessInvite.email,
|
|
||||||
token: emergencyAccessInvite.token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.router.navigate([this.successRoute], {
|
|
||||||
queryParams: {
|
|
||||||
identifier: this.orgIdentifier,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
|
|||||||
import { filter } from "rxjs";
|
import { filter } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RouterService {
|
export class RouterService {
|
||||||
@ -14,6 +16,7 @@ export class RouterService {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private titleService: Title,
|
private titleService: Title,
|
||||||
|
private stateService: StateService,
|
||||||
i18nService: I18nService
|
i18nService: I18nService
|
||||||
) {
|
) {
|
||||||
this.currentUrl = this.router.url;
|
this.currentUrl = this.router.url;
|
||||||
@ -51,11 +54,33 @@ export class RouterService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviousUrl() {
|
getPreviousUrl(): string | undefined {
|
||||||
return this.previousUrl;
|
return this.previousUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviousUrl(url: string) {
|
setPreviousUrl(url: string): void {
|
||||||
this.previousUrl = url;
|
this.previousUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save URL to Global State. This service is used during the login process
|
||||||
|
* @param url URL being saved to the Global State
|
||||||
|
*/
|
||||||
|
async persistLoginRedirectUrl(url: string): Promise<void> {
|
||||||
|
await this.stateService.setDeepLinkRedirectUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and clear persisted LoginRedirectUrl if present in state
|
||||||
|
*/
|
||||||
|
async getAndClearLoginRedirectUrl(): Promise<string> | undefined {
|
||||||
|
const persistedPreLoginUrl = await this.stateService.getDeepLinkRedirectUrl();
|
||||||
|
|
||||||
|
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||||
|
await this.persistLoginRedirectUrl(null);
|
||||||
|
return persistedPreLoginUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio
|
|||||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||||
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
||||||
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
||||||
|
import { AcceptEmergencyComponent } from "./auth/emergency-access/accept/accept-emergency.component";
|
||||||
|
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||||
import { HintComponent } from "./auth/hint.component";
|
import { HintComponent } from "./auth/hint.component";
|
||||||
import { LockComponent } from "./auth/lock.component";
|
import { LockComponent } from "./auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
||||||
@ -113,16 +115,19 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "lock",
|
path: "lock",
|
||||||
component: LockComponent,
|
component: LockComponent,
|
||||||
canActivate: [lockGuard()],
|
canActivate: [deepLinkGuard(), lockGuard()],
|
||||||
},
|
},
|
||||||
{ path: "verify-email", component: VerifyEmailTokenComponent },
|
{ path: "verify-email", component: VerifyEmailTokenComponent },
|
||||||
{
|
{
|
||||||
path: "accept-organization",
|
path: "accept-organization",
|
||||||
component: AcceptOrganizationComponent,
|
component: AcceptOrganizationComponent,
|
||||||
|
canActivate: [deepLinkGuard()],
|
||||||
data: { titleId: "joinOrganization", doNotSaveUrl: false },
|
data: { titleId: "joinOrganization", doNotSaveUrl: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "accept-emergency",
|
path: "accept-emergency",
|
||||||
|
component: AcceptEmergencyComponent,
|
||||||
|
canActivate: [deepLinkGuard()],
|
||||||
data: { titleId: "acceptEmergency", doNotSaveUrl: false },
|
data: { titleId: "acceptEmergency", doNotSaveUrl: false },
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import("./auth/emergency-access/accept/accept-emergency.component").then(
|
import("./auth/emergency-access/accept/accept-emergency.component").then(
|
||||||
@ -132,6 +137,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "accept-families-for-enterprise",
|
path: "accept-families-for-enterprise",
|
||||||
component: AcceptFamilySponsorshipComponent,
|
component: AcceptFamilySponsorshipComponent,
|
||||||
|
canActivate: [deepLinkGuard()],
|
||||||
data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false },
|
data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false },
|
||||||
},
|
},
|
||||||
{ path: "recover", pathMatch: "full", redirectTo: "recover-2fa" },
|
{ path: "recover", pathMatch: "full", redirectTo: "recover-2fa" },
|
||||||
@ -188,7 +194,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: UserLayoutComponent,
|
component: UserLayoutComponent,
|
||||||
canActivate: [AuthGuard],
|
canActivate: [deepLinkGuard(), AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "vault",
|
path: "vault",
|
||||||
|
@ -430,8 +430,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
||||||
getEmergencyAccessInvitation: (options?: StorageOptions) => Promise<any>;
|
|
||||||
setEmergencyAccessInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Do not call this directly, use OrganizationService
|
* @deprecated Do not call this directly, use OrganizationService
|
||||||
*/
|
*/
|
||||||
@ -532,4 +530,17 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
value: Record<string, Record<string, boolean>>,
|
value: Record<string, Record<string, boolean>>,
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* fetches string value of URL user tried to navigate to while unauthenticated.
|
||||||
|
* @param options Defines the storage options for the URL; Defaults to session Storage.
|
||||||
|
* @returns route called prior to successful login.
|
||||||
|
*/
|
||||||
|
getDeepLinkRedirectUrl: (options?: StorageOptions) => Promise<string>;
|
||||||
|
/**
|
||||||
|
* Store URL in session storage by default, but can be configured. Developed to handle
|
||||||
|
* unauthN interrupted navigation.
|
||||||
|
* @param url URL of route
|
||||||
|
* @param options Defines the storage options for the URL; Defaults to session Storage.
|
||||||
|
*/
|
||||||
|
setDeepLinkRedirectUrl: (url: string, options?: StorageOptions) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ export class GlobalState {
|
|||||||
installedVersion?: string;
|
installedVersion?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
organizationInvitation?: any;
|
organizationInvitation?: any;
|
||||||
emergencyAccessInvitation?: any;
|
|
||||||
ssoCodeVerifier?: string;
|
ssoCodeVerifier?: string;
|
||||||
ssoOrganizationIdentifier?: string;
|
ssoOrganizationIdentifier?: string;
|
||||||
ssoState?: string;
|
ssoState?: string;
|
||||||
@ -41,4 +40,5 @@ export class GlobalState {
|
|||||||
disableChangedPasswordNotification?: boolean;
|
disableChangedPasswordNotification?: boolean;
|
||||||
disableContextMenuItem?: boolean;
|
disableContextMenuItem?: boolean;
|
||||||
autoFillOverlayVisibility?: number;
|
autoFillOverlayVisibility?: number;
|
||||||
|
deepLinkRedirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
@ -2386,23 +2386,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEmergencyAccessInvitation(options?: StorageOptions): Promise<any> {
|
|
||||||
return (
|
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.emergencyAccessInvitation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEmergencyAccessInvitation(value: any, options?: StorageOptions): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
|
||||||
);
|
|
||||||
globals.emergencyAccessInvitation = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Do not call this directly, use OrganizationService
|
* @deprecated Do not call this directly, use OrganizationService
|
||||||
*/
|
*/
|
||||||
@ -2884,6 +2867,23 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDeepLinkRedirectUrl(options?: StorageOptions): Promise<string> {
|
||||||
|
return (
|
||||||
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.deepLinkRedirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDeepLinkRedirectUrl(url: string, options?: StorageOptions): Promise<void> {
|
||||||
|
const globals = await this.getGlobals(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
globals.deepLinkRedirectUrl = url;
|
||||||
|
await this.saveGlobals(
|
||||||
|
globals,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||||
let globals: TGlobalState;
|
let globals: TGlobalState;
|
||||||
if (this.useMemory(options.storageLocation)) {
|
if (this.useMemory(options.storageLocation)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user