1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 19:53:59 +00:00

implement send access trampoline

This commit is contained in:
✨ Audrey ✨
2025-07-07 16:33:36 -04:00
parent b6a43cd37c
commit 29e9739975
7 changed files with 193 additions and 2 deletions

View File

@@ -1,2 +1,4 @@
export { AccessComponent } from "./access.component";
export { SendAccessExplainerComponent } from "./send-access-explainer.component";
export { SendAccessRoutes } from "./routes";

View File

@@ -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,
},
],
},
];

View File

@@ -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<void> {
await this.state.getGlobal(TOKEN_KEY).update(() => null);
await this.state.getGlobal(SEND_KEY_KEY).update(() => null);
}
}

View File

@@ -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$);
};

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
import { svgIcon } from "@bitwarden/components";
export const SendCreatedIcon = svgIcon`
export const ActiveSendIcon = svgIcon`
<svg width="96" height="95" viewBox="0 0 96 95" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-stroke-art-primary" d="M89.4998 48.3919C89.4998 70.5749 70.9198 88.5573 47.9998 88.5573C46.0374 88.5573 44.1068 88.4257 42.217 88.1707M6.49976 48.3919C6.49976 26.2092 25.08 8.22656 47.9998 8.22656C51.8283 8.22656 55.5353 8.72824 59.0553 9.66744" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5.47085 67.8617C2.60335 61.9801 1 55.4075 1 48.4729C1 23.3503 22.0426 2.98438 48 2.98438C52.3355 2.98438 56.534 3.55257 60.5205 4.61618M92.211 32.9993C94.016 37.8295 95 43.0399 95 48.4729C95 73.5956 73.9575 93.9614 48 93.9614C45.7775 93.9614 43.5911 93.8119 41.4508 93.5235" />

View File

@@ -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";