From 29e97399750bd364726c2ea76218ea2f5717bbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 7 Jul 2025 16:33:36 -0400 Subject: [PATCH] implement send access trampoline --- .../src/app/tools/send/send-access/index.ts | 2 + .../src/app/tools/send/send-access/routes.ts | 62 ++++++++++++ .../send-access-authentication.service.ts | 99 +++++++++++++++++++ .../send/send-access/try-send-access.guard.ts | 19 ++++ .../src/app/tools/send/send-access/util.ts | 5 + ...nd-created.icon.ts => active-send.icon.ts} | 2 +- libs/tools/send/send-ui/src/icons/index.ts | 6 +- 7 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/tools/send/send-access/routes.ts create mode 100644 apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts create mode 100644 apps/web/src/app/tools/send/send-access/try-send-access.guard.ts create mode 100644 apps/web/src/app/tools/send/send-access/util.ts rename libs/tools/send/send-ui/src/icons/{send-created.icon.ts => active-send.icon.ts} (98%) diff --git a/apps/web/src/app/tools/send/send-access/index.ts b/apps/web/src/app/tools/send/send-access/index.ts index c9df5ce5193..4bef65f468b 100644 --- a/apps/web/src/app/tools/send/send-access/index.ts +++ b/apps/web/src/app/tools/send/send-access/index.ts @@ -1,2 +1,4 @@ export { AccessComponent } from "./access.component"; export { SendAccessExplainerComponent } from "./send-access-explainer.component"; + +export { SendAccessRoutes } from "./routes"; diff --git a/apps/web/src/app/tools/send/send-access/routes.ts b/apps/web/src/app/tools/send/send-access/routes.ts new file mode 100644 index 00000000000..5e07889457d --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/routes.ts @@ -0,0 +1,62 @@ +import { Routes } from "@angular/router"; + +import { AnonLayoutWrapperData } from "@bitwarden/components"; +import { ActiveSendIcon } from "@bitwarden/send-ui"; + +import { RouteDataProperties } from "../../../core"; + +import { SendAccessExplainerComponent } from "./send-access-explainer.component"; +import { SendAccessPasswordComponent } from "./send-access-password.component"; +import { trySendAccess } from "./try-send-access.guard"; +import { ViewContentComponent } from "./view-content.component"; + +/** Routes to reach send access screens */ +export const SendAccessRoutes: Routes = [ + { + path: "send/:sendId", + // there are no child pages because `trySendAccess` always performs a redirect + canActivate: [trySendAccess], + }, + { + path: "send/password/:sendId", + data: { + pageTitle: { + key: "sendAccessPasswordTitle", + }, + pageIcon: ActiveSendIcon, + showReadonlyHostname: true, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: SendAccessPasswordComponent, + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, + { + path: "send/content/:sendId", + data: { + pageTitle: { + key: "sendAccessContentTitle", + }, + pageIcon: ActiveSendIcon, + showReadonlyHostname: true, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "send/password/:sendId/:key", + component: ViewContentComponent, + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, +]; diff --git a/apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts b/apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts new file mode 100644 index 00000000000..fdb228ea6bf --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from "@angular/core"; +import { Router, UrlTree } from "@angular/router"; +import { map, of, from, catchError, timeout } from "rxjs"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers.js"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; + +import { TOKEN_KEY, SEND_KEY_KEY } from "./send-access-memory"; +import { isErrorResponse } from "./util"; + +const TEN_SECONDS = 10_000; + +@Injectable({ providedIn: "root" }) +export class SendAccessAuthenticationService { + private readonly logger: SemanticLogger; + + constructor( + private readonly state: StateProvider, + private readonly api: SendApiService, + private readonly router: Router, + system: SystemServiceProvider, + private configuration = { + tryAccessTimeoutMs: TEN_SECONDS, + }, + ) { + this.logger = system.log({ type: "SendAccessAuthenticationService" }); + } + + redirect$(sendId: string) { + const response$ = from(this.api.postSendAccess(sendId, new SendAccessRequest())); + + const redirect$ = response$.pipe( + timeout({ first: this.configuration.tryAccessTimeoutMs }), + map((_response) => { + this.logger.info("public send detected; redirecting to send access with token."); + const url = this.toViewRedirect(sendId); + + return url; + }), + catchError((error: unknown) => { + let processed: UrlTree | undefined = undefined; + + if (isErrorResponse(error)) { + processed = this.toErrorRedirect(sendId, error); + } + + if (processed) { + return of(processed); + } + + throw error; + }), + ); + + return redirect$; + } + + private toViewRedirect(sendId: string) { + return this.router.createUrlTree(["send", "content", sendId]); + } + + private toErrorRedirect(sendId: string, response: ErrorResponse) { + let url: UrlTree | undefined = undefined; + + switch (response.statusCode) { + case 401: + this.logger.debug(response, "redirecting to password flow"); + url = this.router.createUrlTree(["send/", sendId]); + break; + + case 404: + this.logger.debug(response, "redirecting to unavailable page"); + url = this.router.parseUrl("/404.html"); + break; + + default: + this.logger.warn(response, "received unexpected error response"); + } + + return url; + } + + async setToken(token: string) { + return this.state.getGlobal(TOKEN_KEY).update(() => token); + } + + async setKey(key: string) { + return this.state.getGlobal(SEND_KEY_KEY).update(() => key); + } + + async clear(): Promise { + await this.state.getGlobal(TOKEN_KEY).update(() => null); + await this.state.getGlobal(SEND_KEY_KEY).update(() => null); + } +} diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts new file mode 100644 index 00000000000..46a92e5e4d3 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts @@ -0,0 +1,19 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; +import { from, ignoreElements, concat } from "rxjs"; + +import { SendAccessAuthenticationService } from "./send-access-authentication.service"; + +export const trySendAccess: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + const sendAccess = inject(SendAccessAuthenticationService); + + const { sendId, key } = route.params; + + const setKey$ = from(sendAccess.setKey(key)).pipe(ignoreElements()); + const redirect$ = sendAccess.redirect$(sendId); + + return concat(setKey$, redirect$); +}; diff --git a/apps/web/src/app/tools/send/send-access/util.ts b/apps/web/src/app/tools/send/send-access/util.ts new file mode 100644 index 00000000000..4618f9703d0 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/util.ts @@ -0,0 +1,5 @@ +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; + +export function isErrorResponse(value: unknown): value is ErrorResponse { + return value instanceof ErrorResponse; +} diff --git a/libs/tools/send/send-ui/src/icons/send-created.icon.ts b/libs/tools/send/send-ui/src/icons/active-send.icon.ts similarity index 98% rename from libs/tools/send/send-ui/src/icons/send-created.icon.ts rename to libs/tools/send/send-ui/src/icons/active-send.icon.ts index 099baebb9ad..da5932bf95c 100644 --- a/libs/tools/send/send-ui/src/icons/send-created.icon.ts +++ b/libs/tools/send/send-ui/src/icons/active-send.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const SendCreatedIcon = svgIcon` +export const ActiveSendIcon = svgIcon` diff --git a/libs/tools/send/send-ui/src/icons/index.ts b/libs/tools/send/send-ui/src/icons/index.ts index 4460070f43b..f5239f1acaf 100644 --- a/libs/tools/send/send-ui/src/icons/index.ts +++ b/libs/tools/send/send-ui/src/icons/index.ts @@ -1,3 +1,7 @@ export { ExpiredSendIcon } from "./expired-send.icon"; export { NoSendsIcon } from "./no-send.icon"; -export { SendCreatedIcon } from "./send-created.icon"; +export { + ActiveSendIcon, + /** @deprecated use {@link ActiveSendIcon} instead */ + ActiveSendIcon as SendCreatedIcon, +} from "./active-send.icon";