mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 20:50:28 +00:00
Finalize action button open by using temporarily duplicated service. UI fixes.
This commit is contained in:
@@ -57,6 +57,7 @@ import {
|
||||
import { CollectionView } from "../content/components/common-types";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
@@ -393,9 +394,11 @@ export default class NotificationBackground {
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
const { activeUserId, securityTask, uri } = message.data;
|
||||
const { activeUserId, securityTask, cipher } = message.data;
|
||||
const domain = Utils.getDomain(sender.tab.url);
|
||||
const passwordChangeUri =
|
||||
await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher);
|
||||
|
||||
const domain = Utils.getDomain(uri);
|
||||
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
|
||||
const wasVaultLocked = AuthenticationStatus.Locked && addLoginIsEnabled;
|
||||
|
||||
@@ -411,7 +414,7 @@ export default class NotificationBackground {
|
||||
domain,
|
||||
wasVaultLocked,
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
passwordChangeUri: domain,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: sender.tab,
|
||||
launchTimestamp,
|
||||
|
||||
@@ -519,9 +519,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
const cipher = ciphers.find((cipher) => cipher.id === securityTask.cipherId);
|
||||
|
||||
return { securityTask, cipher, uri: modifyLoginData.uri };
|
||||
// see at-risk-password-component launchChangePassword
|
||||
// DefaultChangeLoginPasswordService
|
||||
// this can be implemented as a provider in the view.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,11 +10,13 @@ export function ActionButton({
|
||||
disabled = false,
|
||||
theme,
|
||||
handleClick,
|
||||
fullWidth = true,
|
||||
}: {
|
||||
buttonText: string | TemplateResult;
|
||||
disabled?: boolean;
|
||||
theme: Theme;
|
||||
handleClick: (e: Event) => void;
|
||||
fullWidth?: boolean;
|
||||
}) {
|
||||
const handleButtonClick = (event: Event) => {
|
||||
if (!disabled) {
|
||||
@@ -24,7 +26,7 @@ export function ActionButton({
|
||||
|
||||
return html`
|
||||
<button
|
||||
class=${actionButtonStyles({ disabled, theme })}
|
||||
class=${actionButtonStyles({ disabled, theme, fullWidth })}
|
||||
title=${buttonText}
|
||||
type="button"
|
||||
@click=${handleButtonClick}
|
||||
@@ -34,14 +36,22 @@ export function ActionButton({
|
||||
`;
|
||||
}
|
||||
|
||||
const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css`
|
||||
const actionButtonStyles = ({
|
||||
disabled,
|
||||
theme,
|
||||
fullWidth,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
theme: Theme;
|
||||
fullWidth: boolean;
|
||||
}) => css`
|
||||
${typography.body2}
|
||||
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${border.radius.full};
|
||||
padding: ${spacing["1"]} ${spacing["3"]};
|
||||
width: 100%;
|
||||
width: ${fullWidth ? "100%" : "auto"};
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { NotificationCipherData } from "../cipher/types";
|
||||
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
|
||||
import { ItemRow } from "../rows/item-row";
|
||||
|
||||
import { NotificationConfirmationBody } from "./confirmation/body";
|
||||
|
||||
export const componentClassPrefix = "notification-body";
|
||||
|
||||
const { css } = createEmotion({
|
||||
@@ -41,13 +43,18 @@ export function NotificationBody({
|
||||
|
||||
switch (notificationType) {
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
return html`
|
||||
<div class=${notificationBodyStyles({ isSafari, theme })}>
|
||||
${passwordChangeUri
|
||||
? chrome.i18n.getMessage("atRiskChangePrompt", organizationName)
|
||||
: chrome.i18n.getMessage("atRiskNavigatePrompt", organizationName)}
|
||||
</div>
|
||||
`;
|
||||
return NotificationConfirmationBody({
|
||||
error: "At risk password",
|
||||
theme,
|
||||
tasksAreComplete: false,
|
||||
itemName: "",
|
||||
handleOpenVault: () => {},
|
||||
buttonText: "",
|
||||
confirmationMessage: chrome.i18n.getMessage(
|
||||
passwordChangeUri ? "atRiskChangePrompt" : "atRiskNavigatePrompt",
|
||||
organizationName,
|
||||
),
|
||||
});
|
||||
default:
|
||||
return html`
|
||||
<div class=${notificationBodyStyles({ isSafari, theme })}>
|
||||
|
||||
@@ -42,7 +42,13 @@ const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => cs
|
||||
}
|
||||
`;
|
||||
|
||||
function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) {
|
||||
export function AdditionalTasksButtonContent({
|
||||
buttonText,
|
||||
theme,
|
||||
}: {
|
||||
buttonText: string;
|
||||
theme: Theme;
|
||||
}) {
|
||||
return html`
|
||||
<div class=${additionalTasksButtonContentStyles({ theme })}>
|
||||
<span>${buttonText}</span>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function NotificationConfirmationMessage({
|
||||
handleClick,
|
||||
theme,
|
||||
}: NotificationConfirmationMessageProps) {
|
||||
const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]);
|
||||
const buttonAria = chrome?.i18n?.getMessage("notificationViewAria", [itemName]);
|
||||
|
||||
return html`
|
||||
<div>
|
||||
|
||||
@@ -78,6 +78,7 @@ export function NotificationContainer({
|
||||
organizations,
|
||||
personalVaultIsAllowed,
|
||||
theme,
|
||||
passwordChangeUri: params?.passwordChangeUri,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { OrgView, FolderView, CollectionView } from "../common-types";
|
||||
import { spacing, themes } from "../constants/styles";
|
||||
|
||||
import { NotificationButtonRow } from "./button-row";
|
||||
import { AdditionalTasksButtonContent } from "./confirmation/footer";
|
||||
|
||||
export type NotificationFooterProps = {
|
||||
collections?: CollectionView[];
|
||||
@@ -22,6 +23,7 @@ export type NotificationFooterProps = {
|
||||
personalVaultIsAllowed: boolean;
|
||||
theme: Theme;
|
||||
handleSaveAction: (e: Event) => void;
|
||||
passwordChangeUri?: string;
|
||||
};
|
||||
|
||||
export function NotificationFooter({
|
||||
@@ -32,6 +34,7 @@ export function NotificationFooter({
|
||||
organizations,
|
||||
personalVaultIsAllowed,
|
||||
theme,
|
||||
passwordChangeUri,
|
||||
handleSaveAction,
|
||||
}: NotificationFooterProps) {
|
||||
const isChangeNotification = notificationType === NotificationTypes.Change;
|
||||
@@ -39,10 +42,14 @@ export function NotificationFooter({
|
||||
|
||||
if (notificationType === NotificationTypes.AtRiskPassword) {
|
||||
return html`<div class=${notificationFooterStyles({ theme })}>
|
||||
${ActionButton({
|
||||
handleClick: () => {},
|
||||
buttonText: i18n.changePassword,
|
||||
${passwordChangeUri &&
|
||||
ActionButton({
|
||||
handleClick: () => {
|
||||
open("https://" + passwordChangeUri, "_blank");
|
||||
},
|
||||
buttonText: AdditionalTasksButtonContent({ buttonText: i18n.changePassword, theme }),
|
||||
theme,
|
||||
fullWidth: false,
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type NotificationBarIframeInitData = {
|
||||
organizations?: OrgView[];
|
||||
removeIndividualVault?: boolean;
|
||||
theme?: Theme;
|
||||
type?: NotificationType; // @TODO use `NotificationType`
|
||||
type?: NotificationType;
|
||||
passwordChangeUri?: string;
|
||||
params?: NotificationMessageParams;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
// Duplicates Default Change Login Password Service, for now
|
||||
// Since the former is an Angular injectable service, and we
|
||||
// need to use the function inside of lit components.
|
||||
// If primary service can be abstracted, that would be ideal.
|
||||
|
||||
export class TemporaryNotificationChangeLoginService {
|
||||
async getChangePasswordUrl(cipher: CipherView, fallback = false): Promise<string | null> {
|
||||
// Ensure we have a cipher with at least one URI
|
||||
if (cipher.type !== CipherType.Login || cipher.login == null || !cipher.login.hasUris) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter for valid URLs that are HTTP(S)
|
||||
const urls = cipher.login.uris
|
||||
.map((m) => Utils.getUrl(m.uri))
|
||||
.filter((m) => m != null && (m.protocol === "http:" || m.protocol === "https:"));
|
||||
|
||||
if (urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
const [reliable, wellKnownChangeUrl] = await Promise.all([
|
||||
this.hasReliableHttpStatusCode(url.origin),
|
||||
this.getWellKnownChangePasswordUrl(url.origin),
|
||||
]);
|
||||
|
||||
// Some servers return a 200 OK for a resource that should not exist
|
||||
// Which means we cannot trust the well-known URL is valid, so we skip it
|
||||
// to avoid potentially sending users to a 404 page
|
||||
if (reliable && wellKnownChangeUrl != null) {
|
||||
return wellKnownChangeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// No reliable well-known URL found, fallback to the first URL
|
||||
|
||||
// @TODO reimplement option in original service to indicate if no URL found.
|
||||
// return urls[0].href; (originally)
|
||||
return fallback ? urls[0].href : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server returns a non-200 status code for a resource that should not exist.
|
||||
* See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics
|
||||
* @param urlOrigin The origin of the URL to check
|
||||
*/
|
||||
private async hasReliableHttpStatusCode(urlOrigin: string): Promise<boolean> {
|
||||
try {
|
||||
const url = new URL(
|
||||
"./.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200",
|
||||
urlOrigin,
|
||||
);
|
||||
|
||||
const request = new Request(url, {
|
||||
method: "GET",
|
||||
mode: "same-origin",
|
||||
credentials: "omit",
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
const response = await fetch(request);
|
||||
return !response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response
|
||||
* is returned. Returns null if the request throws or the response is not 200 OK.
|
||||
* See https://w3c.github.io/webappsec-change-password-url/
|
||||
* @param urlOrigin The origin of the URL to check
|
||||
*/
|
||||
private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise<string | null> {
|
||||
try {
|
||||
const url = new URL("./.well-known/change-password", urlOrigin);
|
||||
|
||||
const request = new Request(url, {
|
||||
method: "GET",
|
||||
mode: "same-origin",
|
||||
credentials: "omit",
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
const response = await fetch(request);
|
||||
|
||||
return response.ok ? url.toString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user