mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-16 01:21:48 +01:00
[EC-475] Auto-save password prompt enhancements (#4808)
* [EC-1062] Convert bar.js to TS and refactor (#4623) * [EC-476 / EC-478] Add notificationBar edit flow (#4626) * [EC-477] Enable auto-save for users without individual vault (#4760) * [EC-1057] Add data loss warning to notificationBar edit flow (#4761) * [AC-1173] Fix state bugs in auto-save edit flow (#4936) --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
parent
cafd2d2561
commit
f592963191
@ -10,8 +10,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|
||||||
|
|
||||||
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
|
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
|
||||||
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
|
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
|
||||||
@ -95,7 +93,7 @@ export default class NotificationBackground {
|
|||||||
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
|
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
|
await this.saveOrUpdateCredentials(sender.tab, msg.edit, msg.folder);
|
||||||
break;
|
break;
|
||||||
case "bgNeverSave":
|
case "bgNeverSave":
|
||||||
await this.saveNever(sender.tab);
|
await this.saveNever(sender.tab);
|
||||||
@ -168,6 +166,7 @@ export default class NotificationBackground {
|
|||||||
typeData: {
|
typeData: {
|
||||||
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
|
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
|
||||||
theme: await this.getCurrentTheme(),
|
theme: await this.getCurrentTheme(),
|
||||||
|
removeIndividualVault: await this.removeIndividualVault(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
|
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
|
||||||
@ -225,10 +224,6 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.allowPersonalOwnership())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
|
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -242,10 +237,6 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.allowPersonalOwnership())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
|
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
|
||||||
} else if (
|
} else if (
|
||||||
usernameMatches.length === 1 &&
|
usernameMatches.length === 1 &&
|
||||||
@ -332,14 +323,10 @@ export default class NotificationBackground {
|
|||||||
await this.checkNotificationQueue(tab);
|
await this.checkNotificationQueue(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
|
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) {
|
||||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||||
const queueMessage = this.notificationQueue[i];
|
const queueMessage = this.notificationQueue[i];
|
||||||
if (
|
if (queueMessage.tabId !== tab.id || !(queueMessage.type in NotificationQueueMessageType)) {
|
||||||
queueMessage.tabId !== tab.id ||
|
|
||||||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
|
|
||||||
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,63 +339,79 @@ export default class NotificationBackground {
|
|||||||
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
|
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
|
||||||
|
|
||||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||||
const changePasswordMessage = queueMessage as AddChangePasswordQueueMessage;
|
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId);
|
||||||
const cipher = await this.getDecryptedCipherById(changePasswordMessage.cipherId);
|
await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab);
|
||||||
if (cipher == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.updateCipher(cipher, changePasswordMessage.newPassword);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
|
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
|
||||||
if (!queueMessage.wasVaultLocked) {
|
|
||||||
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
|
|
||||||
BrowserApi.tabSendMessageData(tab, "addedCipher");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the vault was locked, check if a cipher needs updating instead of creating a new one
|
// If the vault was locked, check if a cipher needs updating instead of creating a new one
|
||||||
const addLoginMessage = queueMessage as AddLoginQueueMessage;
|
if (queueMessage.wasVaultLocked) {
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(addLoginMessage.uri);
|
const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri);
|
||||||
const usernameMatches = ciphers.filter(
|
const existingCipher = allCiphers.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.login.username != null && c.login.username.toLowerCase() === addLoginMessage.username
|
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username
|
||||||
);
|
);
|
||||||
|
|
||||||
if (usernameMatches.length >= 1) {
|
if (existingCipher != null) {
|
||||||
await this.updateCipher(usernameMatches[0], addLoginMessage.password);
|
await this.updatePassword(existingCipher, queueMessage.password, edit, tab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folderId = (await this.folderExists(folderId)) ? folderId : null;
|
||||||
|
const newCipher = AddLoginQueueMessage.toCipherView(queueMessage, folderId);
|
||||||
|
|
||||||
|
if (edit) {
|
||||||
|
await this.editItem(newCipher, tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createNewCipher(addLoginMessage, folderId);
|
const cipher = await this.cipherService.encrypt(newCipher);
|
||||||
|
await this.cipherService.createWithServer(cipher);
|
||||||
BrowserApi.tabSendMessageData(tab, "addedCipher");
|
BrowserApi.tabSendMessageData(tab, "addedCipher");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
|
private async updatePassword(
|
||||||
const loginModel = new LoginView();
|
cipherView: CipherView,
|
||||||
const loginUri = new LoginUriView();
|
newPassword: string,
|
||||||
loginUri.uri = queueMessage.uri;
|
edit: boolean,
|
||||||
loginModel.uris = [loginUri];
|
tab: chrome.tabs.Tab
|
||||||
loginModel.username = queueMessage.username;
|
) {
|
||||||
loginModel.password = queueMessage.password;
|
cipherView.login.password = newPassword;
|
||||||
const model = new CipherView();
|
|
||||||
model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
|
|
||||||
model.name = model.name.replace(/^www\./, "");
|
|
||||||
model.type = CipherType.Login;
|
|
||||||
model.login = loginModel;
|
|
||||||
|
|
||||||
if (!Utils.isNullOrWhitespace(folderId)) {
|
if (edit) {
|
||||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
await this.editItem(cipherView, tab);
|
||||||
if (folders.some((x) => x.id === folderId)) {
|
BrowserApi.tabSendMessage(tab, "editedCipher");
|
||||||
model.folderId = folderId;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipher = await this.cipherService.encrypt(model);
|
const cipher = await this.cipherService.encrypt(cipherView);
|
||||||
await this.cipherService.createWithServer(cipher);
|
await this.cipherService.updateWithServer(cipher);
|
||||||
|
// We've only updated the password, no need to broadcast editedCipher message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
|
||||||
|
await this.stateService.setAddEditCipherInfo({
|
||||||
|
cipher: cipherView,
|
||||||
|
collectionIds: cipherView.collectionIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
await BrowserApi.tabSendMessageData(senderTab, "openAddEditCipher", {
|
||||||
|
cipherId: cipherView.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async folderExists(folderId: string) {
|
||||||
|
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await firstValueFrom(this.folderService.folderViews$);
|
||||||
|
return folders.some((x) => x.id === folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDecryptedCipherById(cipherId: string) {
|
private async getDecryptedCipherById(cipherId: string) {
|
||||||
@ -419,14 +422,6 @@ export default class NotificationBackground {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateCipher(cipher: CipherView, newPassword: string) {
|
|
||||||
if (cipher != null && cipher.type === CipherType.Login) {
|
|
||||||
cipher.login.password = newPassword;
|
|
||||||
const newCipher = await this.cipherService.encrypt(cipher);
|
|
||||||
await this.cipherService.updateWithServer(newCipher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveNever(tab: chrome.tabs.Tab) {
|
private async saveNever(tab: chrome.tabs.Tab) {
|
||||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||||
const queueMessage = this.notificationQueue[i];
|
const queueMessage = this.notificationQueue[i];
|
||||||
@ -459,9 +454,9 @@ export default class NotificationBackground {
|
|||||||
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
|
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async allowPersonalOwnership(): Promise<boolean> {
|
private async removeIndividualVault(): Promise<boolean> {
|
||||||
return !(await firstValueFrom(
|
return await firstValueFrom(
|
||||||
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ const forwardCommands = [
|
|||||||
"addToLockedVaultPendingNotifications",
|
"addToLockedVaultPendingNotifications",
|
||||||
"unlockCompleted",
|
"unlockCompleted",
|
||||||
"addedCipher",
|
"addedCipher",
|
||||||
|
"openAddEditCipher",
|
||||||
];
|
];
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((event) => {
|
chrome.runtime.onMessage.addListener((event) => {
|
||||||
|
@ -502,6 +502,7 @@ document.addEventListener("DOMContentLoaded", (event) => {
|
|||||||
type,
|
type,
|
||||||
isVaultLocked: typeData.isVaultLocked,
|
isVaultLocked: typeData.isVaultLocked,
|
||||||
theme: typeData.theme,
|
theme: typeData.theme,
|
||||||
|
removeIndividualVault: typeData.removeIndividualVault,
|
||||||
};
|
};
|
||||||
const barQueryString = new URLSearchParams(barQueryParams).toString();
|
const barQueryString = new URLSearchParams(barQueryParams).toString();
|
||||||
const barPage = "notification/bar.html?" + barQueryString;
|
const barPage = "notification/bar.html?" + barQueryString;
|
||||||
|
@ -28,21 +28,27 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="templates" style="display: none">
|
</body>
|
||||||
<div class="inner-wrapper" id="template-add">
|
|
||||||
<div class="add-text"></div>
|
<template id="template-add">
|
||||||
<div class="add-buttons">
|
<div class="inner-wrapper">
|
||||||
<button type="button" class="never-save link"></button>
|
<div id="add-text"></div>
|
||||||
<select class="select-folder" isVaultLocked="false"></select>
|
<div>
|
||||||
<button type="button" class="add-save"></button>
|
<button type="button" id="never-save" class="link"></button>
|
||||||
</div>
|
<select id="select-folder"></select>
|
||||||
</div>
|
<button type="button" id="add-edit" class="secondary"></button>
|
||||||
<div class="inner-wrapper" id="template-change">
|
<button type="button" id="add-save" class="primary"></button>
|
||||||
<div class="change-text"></div>
|
|
||||||
<div class="change-buttons">
|
|
||||||
<button type="button" class="change-save"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</template>
|
||||||
|
|
||||||
|
<template id="template-change">
|
||||||
|
<div class="inner-wrapper">
|
||||||
|
<div id="change-text"></div>
|
||||||
|
<div>
|
||||||
|
<button type="button" id="change-edit" class="secondary"></button>
|
||||||
|
<button type="button" id="change-save" class="primary"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
// eslint-disable-next-line
|
|
||||||
require("./bar.scss");
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const theme = getQueryVariable("theme");
|
|
||||||
document.documentElement.classList.add("theme_" + theme);
|
|
||||||
|
|
||||||
let i18n = {};
|
|
||||||
let lang = window.navigator.language;
|
|
||||||
|
|
||||||
i18n.appName = chrome.i18n.getMessage("appName");
|
|
||||||
i18n.close = chrome.i18n.getMessage("close");
|
|
||||||
i18n.never = chrome.i18n.getMessage("never");
|
|
||||||
i18n.folder = chrome.i18n.getMessage("folder");
|
|
||||||
i18n.notificationAddSave = chrome.i18n.getMessage("notificationAddSave");
|
|
||||||
i18n.notificationAddDesc = chrome.i18n.getMessage("notificationAddDesc");
|
|
||||||
i18n.notificationChangeSave = chrome.i18n.getMessage("notificationChangeSave");
|
|
||||||
i18n.notificationChangeDesc = chrome.i18n.getMessage("notificationChangeDesc");
|
|
||||||
lang = chrome.i18n.getUILanguage(); // eslint-disable-line
|
|
||||||
|
|
||||||
// delay 50ms so that we get proper body dimensions
|
|
||||||
setTimeout(load, 50);
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
const isVaultLocked = getQueryVariable("isVaultLocked") == "true";
|
|
||||||
document.getElementById("logo").src = isVaultLocked
|
|
||||||
? chrome.runtime.getURL("images/icon38_locked.png")
|
|
||||||
: chrome.runtime.getURL("images/icon38.png");
|
|
||||||
|
|
||||||
document.getElementById("logo-link").title = i18n.appName;
|
|
||||||
|
|
||||||
var neverButton = document.querySelector("#template-add .never-save");
|
|
||||||
neverButton.textContent = i18n.never;
|
|
||||||
|
|
||||||
var selectFolder = document.querySelector("#template-add .select-folder");
|
|
||||||
selectFolder.setAttribute("aria-label", i18n.folder);
|
|
||||||
selectFolder.setAttribute("isVaultLocked", isVaultLocked.toString());
|
|
||||||
|
|
||||||
var addButton = document.querySelector("#template-add .add-save");
|
|
||||||
addButton.textContent = i18n.notificationAddSave;
|
|
||||||
|
|
||||||
var changeButton = document.querySelector("#template-change .change-save");
|
|
||||||
changeButton.textContent = i18n.notificationChangeSave;
|
|
||||||
|
|
||||||
var closeButton = document.getElementById("close-button");
|
|
||||||
closeButton.title = i18n.close;
|
|
||||||
closeButton.setAttribute("aria-label", i18n.close);
|
|
||||||
|
|
||||||
document.querySelector("#template-add .add-text").textContent = i18n.notificationAddDesc;
|
|
||||||
document.querySelector("#template-change .change-text").textContent =
|
|
||||||
i18n.notificationChangeDesc;
|
|
||||||
|
|
||||||
if (getQueryVariable("type") === "add") {
|
|
||||||
handleTypeAdd(isVaultLocked);
|
|
||||||
} else if (getQueryVariable("type") === "change") {
|
|
||||||
handleTypeChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeButton.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPlatformMessage({
|
|
||||||
command: "bgCloseNotificationBar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("resize", adjustHeight);
|
|
||||||
adjustHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueryVariable(variable) {
|
|
||||||
var query = window.location.search.substring(1);
|
|
||||||
var vars = query.split("&");
|
|
||||||
|
|
||||||
for (var i = 0; i < vars.length; i++) {
|
|
||||||
var pair = vars[i].split("=");
|
|
||||||
if (pair[0] === variable) {
|
|
||||||
return pair[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTypeAdd(isVaultLocked) {
|
|
||||||
setContent(document.getElementById("template-add"));
|
|
||||||
|
|
||||||
var addButton = document.querySelector("#template-add-clone .add-save"), // eslint-disable-line
|
|
||||||
neverButton = document.querySelector("#template-add-clone .never-save"); // eslint-disable-line
|
|
||||||
|
|
||||||
addButton.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const folderId = document.querySelector("#template-add-clone .select-folder").value;
|
|
||||||
|
|
||||||
const bgAddSaveMessage = {
|
|
||||||
command: "bgAddSave",
|
|
||||||
folder: folderId,
|
|
||||||
};
|
|
||||||
sendPlatformMessage(bgAddSaveMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
neverButton.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPlatformMessage({
|
|
||||||
command: "bgNeverSave",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isVaultLocked) {
|
|
||||||
const responseFoldersCommand = "notificationBarGetFoldersList";
|
|
||||||
chrome.runtime.onMessage.addListener((msg) => {
|
|
||||||
if (msg.command === responseFoldersCommand && msg.data) {
|
|
||||||
fillSelectorWithFolders(msg.data.folders);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sendPlatformMessage({
|
|
||||||
command: "bgGetDataForTab",
|
|
||||||
responseCommand: responseFoldersCommand,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTypeChange() {
|
|
||||||
setContent(document.getElementById("template-change"));
|
|
||||||
var changeButton = document.querySelector("#template-change-clone .change-save"); // eslint-disable-line
|
|
||||||
changeButton.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const bgChangeSaveMessage = {
|
|
||||||
command: "bgChangeSave",
|
|
||||||
};
|
|
||||||
sendPlatformMessage(bgChangeSaveMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setContent(element) {
|
|
||||||
const content = document.getElementById("content");
|
|
||||||
while (content.firstChild) {
|
|
||||||
content.removeChild(content.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newElement = element.cloneNode(true);
|
|
||||||
newElement.id = newElement.id + "-clone";
|
|
||||||
content.appendChild(newElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendPlatformMessage(msg) {
|
|
||||||
chrome.runtime.sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillSelectorWithFolders(folders) {
|
|
||||||
const select = document.querySelector("#template-add-clone .select-folder");
|
|
||||||
select.appendChild(new Option(chrome.i18n.getMessage("selectFolder"), null, true));
|
|
||||||
folders.forEach((folder) => {
|
|
||||||
//Select "No Folder" (id=null) folder by default
|
|
||||||
select.appendChild(new Option(folder.name, folder.id || "", false));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustHeight() {
|
|
||||||
sendPlatformMessage({
|
|
||||||
command: "bgAdjustNotificationBar",
|
|
||||||
data: {
|
|
||||||
height: document.querySelector("body").scrollHeight,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -80,7 +80,7 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(.neutral):not(.link) {
|
button.primary:not(.neutral) {
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
background-color: themed("primaryColor");
|
background-color: themed("primaryColor");
|
||||||
color: themed("textContrast");
|
color: themed("textContrast");
|
||||||
@ -95,6 +95,21 @@ button:not(.neutral):not(.link) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.secondary:not(.neutral) {
|
||||||
|
@include themify($themes) {
|
||||||
|
background-color: themed("backgroundColor");
|
||||||
|
color: themed("mutedTextColor");
|
||||||
|
border-color: themed("mutedTextColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include themify($themes) {
|
||||||
|
background-color: darken(themed("backgroundColor"), 1.5%);
|
||||||
|
color: darken(themed("mutedTextColor"), 6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button.link,
|
button.link,
|
||||||
button.neutral {
|
button.neutral {
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
@ -130,12 +145,8 @@ button {
|
|||||||
font-family: $font-family-sans-serif;
|
font-family: $font-family-sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-folder[isVaultLocked="true"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.select-folder {
|
#select-folder {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
218
apps/browser/src/autofill/notification/bar.ts
Normal file
218
apps/browser/src/autofill/notification/bar.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import type { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
|
require("./bar.scss");
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// delay 50ms so that we get proper body dimensions
|
||||||
|
setTimeout(load, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
const theme = getQueryVariable("theme");
|
||||||
|
document.documentElement.classList.add("theme_" + theme);
|
||||||
|
|
||||||
|
const isVaultLocked = getQueryVariable("isVaultLocked") == "true";
|
||||||
|
(document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
|
||||||
|
? chrome.runtime.getURL("images/icon38_locked.png")
|
||||||
|
: chrome.runtime.getURL("images/icon38.png");
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
appName: chrome.i18n.getMessage("appName"),
|
||||||
|
close: chrome.i18n.getMessage("close"),
|
||||||
|
never: chrome.i18n.getMessage("never"),
|
||||||
|
folder: chrome.i18n.getMessage("folder"),
|
||||||
|
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
||||||
|
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
|
||||||
|
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||||
|
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
|
||||||
|
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("logo-link").title = i18n.appName;
|
||||||
|
|
||||||
|
// i18n for "Add" template
|
||||||
|
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
|
||||||
|
|
||||||
|
const neverButton = addTemplate.content.getElementById("never-save");
|
||||||
|
neverButton.textContent = i18n.never;
|
||||||
|
|
||||||
|
const selectFolder = addTemplate.content.getElementById("select-folder");
|
||||||
|
selectFolder.hidden = isVaultLocked || removeIndividualVault();
|
||||||
|
selectFolder.setAttribute("aria-label", i18n.folder);
|
||||||
|
|
||||||
|
const addButton = addTemplate.content.getElementById("add-save");
|
||||||
|
addButton.textContent = i18n.notificationAddSave;
|
||||||
|
|
||||||
|
const addEditButton = addTemplate.content.getElementById("add-edit");
|
||||||
|
// If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
|
||||||
|
addEditButton.hidden = removeIndividualVault();
|
||||||
|
addEditButton.textContent = i18n.notificationEdit;
|
||||||
|
|
||||||
|
addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
|
||||||
|
|
||||||
|
// i18n for "Change" (update password) template
|
||||||
|
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
|
||||||
|
|
||||||
|
const changeButton = changeTemplate.content.getElementById("change-save");
|
||||||
|
changeButton.textContent = i18n.notificationChangeSave;
|
||||||
|
|
||||||
|
const changeEditButton = changeTemplate.content.getElementById("change-edit");
|
||||||
|
changeEditButton.textContent = i18n.notificationEdit;
|
||||||
|
|
||||||
|
changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
|
||||||
|
|
||||||
|
// i18n for body content
|
||||||
|
const closeButton = document.getElementById("close-button");
|
||||||
|
closeButton.title = i18n.close;
|
||||||
|
|
||||||
|
if (getQueryVariable("type") === "add") {
|
||||||
|
handleTypeAdd();
|
||||||
|
} else if (getQueryVariable("type") === "change") {
|
||||||
|
handleTypeChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgCloseNotificationBar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", adjustHeight);
|
||||||
|
adjustHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryVariable(variable: string) {
|
||||||
|
const query = window.location.search.substring(1);
|
||||||
|
const vars = query.split("&");
|
||||||
|
|
||||||
|
for (let i = 0; i < vars.length; i++) {
|
||||||
|
const pair = vars[i].split("=");
|
||||||
|
if (pair[0] === variable) {
|
||||||
|
return pair[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTypeAdd() {
|
||||||
|
setContent(document.getElementById("template-add") as HTMLTemplateElement);
|
||||||
|
|
||||||
|
const addButton = document.getElementById("add-save");
|
||||||
|
addButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// If Remove Individual Vault policy applies, "Add" opens the edit tab
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgAddSave",
|
||||||
|
folder: getSelectedFolder(),
|
||||||
|
edit: removeIndividualVault(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeIndividualVault()) {
|
||||||
|
// Everything past this point is only required if user has an individual vault
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editButton = document.getElementById("add-edit");
|
||||||
|
editButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgAddSave",
|
||||||
|
folder: getSelectedFolder(),
|
||||||
|
edit: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const neverButton = document.getElementById("never-save");
|
||||||
|
neverButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgNeverSave",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadFolderSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTypeChange() {
|
||||||
|
setContent(document.getElementById("template-change") as HTMLTemplateElement);
|
||||||
|
const changeButton = document.getElementById("change-save");
|
||||||
|
changeButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgChangeSave",
|
||||||
|
edit: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButton = document.getElementById("change-edit");
|
||||||
|
editButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgChangeSave",
|
||||||
|
edit: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContent(template: HTMLTemplateElement) {
|
||||||
|
const content = document.getElementById("content");
|
||||||
|
while (content.firstChild) {
|
||||||
|
content.removeChild(content.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newElement = template.content.cloneNode(true) as HTMLElement;
|
||||||
|
content.appendChild(newElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPlatformMessage(msg: Record<string, unknown>) {
|
||||||
|
chrome.runtime.sendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFolderSelector() {
|
||||||
|
const responseFoldersCommand = "notificationBarGetFoldersList";
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.command !== responseFoldersCommand || msg.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = msg.data.folders as Jsonify<FolderView[]>;
|
||||||
|
const select = document.getElementById("select-folder");
|
||||||
|
select.appendChild(new Option(chrome.i18n.getMessage("selectFolder"), null, true));
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
// Select "No Folder" (id=null) folder by default
|
||||||
|
select.appendChild(new Option(folder.name, folder.id || "", false));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgGetDataForTab",
|
||||||
|
responseCommand: responseFoldersCommand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedFolder(): string {
|
||||||
|
return (document.getElementById("select-folder") as HTMLSelectElement).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIndividualVault(): boolean {
|
||||||
|
return getQueryVariable("removeIndividualVault") == "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustHeight() {
|
||||||
|
sendPlatformMessage({
|
||||||
|
command: "bgAdjustNotificationBar",
|
||||||
|
data: {
|
||||||
|
height: document.querySelector("body").scrollHeight,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -10,6 +10,7 @@ $brand-primary: #175ddc;
|
|||||||
|
|
||||||
$background-color: #f0f0f0;
|
$background-color: #f0f0f0;
|
||||||
|
|
||||||
|
$solarizedDarkBase0: #839496;
|
||||||
$solarizedDarkBase03: #002b36;
|
$solarizedDarkBase03: #002b36;
|
||||||
$solarizedDarkBase02: #073642;
|
$solarizedDarkBase02: #073642;
|
||||||
$solarizedDarkBase01: #586e75;
|
$solarizedDarkBase01: #586e75;
|
||||||
@ -20,6 +21,7 @@ $solarizedDarkGreen: #859900;
|
|||||||
$themes: (
|
$themes: (
|
||||||
light: (
|
light: (
|
||||||
textColor: $text-color,
|
textColor: $text-color,
|
||||||
|
mutedTextColor: #6d757e,
|
||||||
backgroundColor: $background-color,
|
backgroundColor: $background-color,
|
||||||
primaryColor: $brand-primary,
|
primaryColor: $brand-primary,
|
||||||
buttonPrimaryColor: $brand-primary,
|
buttonPrimaryColor: $brand-primary,
|
||||||
@ -29,6 +31,7 @@ $themes: (
|
|||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
textColor: #ffffff,
|
textColor: #ffffff,
|
||||||
|
mutedTextColor: #bac0ce,
|
||||||
backgroundColor: #2f343d,
|
backgroundColor: #2f343d,
|
||||||
buttonPrimaryColor: #6f9df1,
|
buttonPrimaryColor: #6f9df1,
|
||||||
primaryColor: #6f9df1,
|
primaryColor: #6f9df1,
|
||||||
@ -38,6 +41,7 @@ $themes: (
|
|||||||
),
|
),
|
||||||
nord: (
|
nord: (
|
||||||
textColor: $nord5,
|
textColor: $nord5,
|
||||||
|
mutedTextColor: $nord4,
|
||||||
backgroundColor: $nord1,
|
backgroundColor: $nord1,
|
||||||
buttonPrimaryColor: $nord8,
|
buttonPrimaryColor: $nord8,
|
||||||
primaryColor: $nord9,
|
primaryColor: $nord9,
|
||||||
@ -47,6 +51,8 @@ $themes: (
|
|||||||
),
|
),
|
||||||
solarizedDark: (
|
solarizedDark: (
|
||||||
textColor: $solarizedDarkBase2,
|
textColor: $solarizedDarkBase2,
|
||||||
|
// Muted uses main text color to avoid contrast issues
|
||||||
|
mutedTextColor: $solarizedDarkBase2,
|
||||||
backgroundColor: $solarizedDarkBase03,
|
backgroundColor: $solarizedDarkBase03,
|
||||||
buttonPrimaryColor: $solarizedDarkCyan,
|
buttonPrimaryColor: $solarizedDarkCyan,
|
||||||
primaryColor: $solarizedDarkGreen,
|
primaryColor: $solarizedDarkGreen,
|
||||||
|
@ -96,7 +96,6 @@ import { SafariApp } from "../browser/safariApp";
|
|||||||
import { flagEnabled } from "../flags";
|
import { flagEnabled } from "../flags";
|
||||||
import { UpdateBadge } from "../listeners/update-badge";
|
import { UpdateBadge } from "../listeners/update-badge";
|
||||||
import { Account } from "../models/account";
|
import { Account } from "../models/account";
|
||||||
import { PopupUtilsService } from "../popup/services/popup-utils.service";
|
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../services/abstractions/browser-state.service";
|
import { BrowserStateService as StateServiceAbstraction } from "../services/abstractions/browser-state.service";
|
||||||
import { BrowserEnvironmentService } from "../services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../services/browser-environment.service";
|
||||||
import { BrowserI18nService } from "../services/browser-i18n.service";
|
import { BrowserI18nService } from "../services/browser-i18n.service";
|
||||||
@ -157,7 +156,6 @@ export default class MainBackground {
|
|||||||
eventCollectionService: EventCollectionServiceAbstraction;
|
eventCollectionService: EventCollectionServiceAbstraction;
|
||||||
eventUploadService: EventUploadServiceAbstraction;
|
eventUploadService: EventUploadServiceAbstraction;
|
||||||
policyService: InternalPolicyServiceAbstraction;
|
policyService: InternalPolicyServiceAbstraction;
|
||||||
popupUtilsService: PopupUtilsService;
|
|
||||||
sendService: SendServiceAbstraction;
|
sendService: SendServiceAbstraction;
|
||||||
fileUploadService: FileUploadServiceAbstraction;
|
fileUploadService: FileUploadServiceAbstraction;
|
||||||
organizationService: InternalOrganizationServiceAbstraction;
|
organizationService: InternalOrganizationServiceAbstraction;
|
||||||
@ -358,7 +356,7 @@ export default class MainBackground {
|
|||||||
// AuthService should send the messages to the background not popup.
|
// AuthService should send the messages to the background not popup.
|
||||||
send = (subscriber: string, arg: any = {}) => {
|
send = (subscriber: string, arg: any = {}) => {
|
||||||
const message = Object.assign({}, { command: subscriber }, arg);
|
const message = Object.assign({}, { command: subscriber }, arg);
|
||||||
that.runtimeBackground.processMessage(message, that, null);
|
that.runtimeBackground.processMessage(message, that as any, null);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
this.authService = new AuthService(
|
this.authService = new AuthService(
|
||||||
@ -463,7 +461,6 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.messagingService
|
this.messagingService
|
||||||
);
|
);
|
||||||
this.popupUtilsService = new PopupUtilsService(isPrivateMode);
|
|
||||||
|
|
||||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import NotificationQueueMessage from "./notificationQueueMessage";
|
import NotificationQueueMessage from "./notificationQueueMessage";
|
||||||
|
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
|
||||||
|
|
||||||
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||||
|
type: NotificationQueueMessageType.ChangePassword;
|
||||||
cipherId: string;
|
cipherId: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,33 @@
|
|||||||
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
|
|
||||||
import NotificationQueueMessage from "./notificationQueueMessage";
|
import NotificationQueueMessage from "./notificationQueueMessage";
|
||||||
|
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
|
||||||
|
|
||||||
export default class AddLoginQueueMessage extends NotificationQueueMessage {
|
export default class AddLoginQueueMessage extends NotificationQueueMessage {
|
||||||
|
type: NotificationQueueMessageType.AddLogin;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
|
|
||||||
|
static toCipherView(message: AddLoginQueueMessage, folderId?: string): CipherView {
|
||||||
|
const uriView = new LoginUriView();
|
||||||
|
uriView.uri = message.uri;
|
||||||
|
|
||||||
|
const loginView = new LoginView();
|
||||||
|
loginView.uris = [uriView];
|
||||||
|
loginView.username = message.username;
|
||||||
|
loginView.password = message.password;
|
||||||
|
|
||||||
|
const cipherView = new CipherView();
|
||||||
|
cipherView.name = (Utils.getHostname(message.uri) || message.domain).replace(/^www\./, "");
|
||||||
|
cipherView.folderId = folderId;
|
||||||
|
cipherView.type = CipherType.Login;
|
||||||
|
cipherView.login = loginView;
|
||||||
|
|
||||||
|
return cipherView;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,19 +56,15 @@ export default class RuntimeBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processMessage(msg: any, sender: any, sendResponse: any) {
|
async processMessage(msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) {
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case "loggedIn":
|
case "loggedIn":
|
||||||
case "unlocked": {
|
case "unlocked": {
|
||||||
let item: LockedVaultPendingNotificationsItem;
|
let item: LockedVaultPendingNotificationsItem;
|
||||||
|
|
||||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||||
await BrowserApi.closeLoginTab();
|
|
||||||
|
|
||||||
item = this.lockedVaultPendingNotifications.pop();
|
item = this.lockedVaultPendingNotifications.pop();
|
||||||
if (item.commandToRetry.sender?.tab?.id) {
|
BrowserApi.closeBitwardenExtensionTab();
|
||||||
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.main.refreshBadge();
|
await this.main.refreshBadge();
|
||||||
@ -104,7 +100,21 @@ export default class RuntimeBackground {
|
|||||||
await this.main.openPopup();
|
await this.main.openPopup();
|
||||||
break;
|
break;
|
||||||
case "promptForLogin":
|
case "promptForLogin":
|
||||||
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
|
BrowserApi.openBitwardenExtensionTab("popup/index.html", true, sender.tab);
|
||||||
|
break;
|
||||||
|
case "openAddEditCipher": {
|
||||||
|
const addEditCipherUrl =
|
||||||
|
msg.data?.cipherId == null
|
||||||
|
? "popup/index.html#/edit-cipher"
|
||||||
|
: "popup/index.html#/edit-cipher?cipherId=" + msg.data.cipherId;
|
||||||
|
|
||||||
|
BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true, sender.tab);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "closeTab":
|
||||||
|
setTimeout(() => {
|
||||||
|
BrowserApi.closeBitwardenExtensionTab();
|
||||||
|
}, msg.delay ?? 0);
|
||||||
break;
|
break;
|
||||||
case "showDialogResolve":
|
case "showDialogResolve":
|
||||||
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
|
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
|
||||||
@ -183,11 +193,7 @@ export default class RuntimeBackground {
|
|||||||
const params =
|
const params =
|
||||||
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
|
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
|
||||||
`remember=${encodeURIComponent(msg.remember)}`;
|
`remember=${encodeURIComponent(msg.remember)}`;
|
||||||
BrowserApi.createNewTab(
|
BrowserApi.openBitwardenExtensionTab(`popup/index.html#/2fa;${params}`, false);
|
||||||
`popup/index.html?uilocation=popout#/2fa;${params}`,
|
|
||||||
undefined,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "reloadPopup":
|
case "reloadPopup":
|
||||||
|
@ -127,8 +127,44 @@ export class BrowserApi {
|
|||||||
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
|
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createNewTab(url: string, extensionPage = false, active = true) {
|
static createNewTab(url: string, active = true, openerTab?: chrome.tabs.Tab) {
|
||||||
chrome.tabs.create({ url: url, active: active });
|
chrome.tabs.create({ url: url, active: active, openerTabId: openerTab?.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
static openBitwardenExtensionTab(
|
||||||
|
relativeUrl: string,
|
||||||
|
active = true,
|
||||||
|
openerTab?: chrome.tabs.Tab
|
||||||
|
) {
|
||||||
|
if (relativeUrl.includes("uilocation=tab")) {
|
||||||
|
this.createNewTab(relativeUrl, active, openerTab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = chrome.extension.getURL(relativeUrl);
|
||||||
|
const parsedUrl = new URL(fullUrl);
|
||||||
|
parsedUrl.searchParams.set("uilocation", "tab");
|
||||||
|
this.createNewTab(parsedUrl.toString(), active, openerTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async closeBitwardenExtensionTab() {
|
||||||
|
const tabs = await BrowserApi.tabsQuery({
|
||||||
|
active: true,
|
||||||
|
title: "Bitwarden",
|
||||||
|
windowType: "normal",
|
||||||
|
currentWindow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabToClose = tabs[tabs.length - 1];
|
||||||
|
chrome.tabs.remove(tabToClose.id);
|
||||||
|
|
||||||
|
if (tabToClose.openerTabId) {
|
||||||
|
this.focusTab(tabToClose.openerTabId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static messageListener(
|
static messageListener(
|
||||||
@ -147,23 +183,7 @@ export class BrowserApi {
|
|||||||
return chrome.runtime.sendMessage(message);
|
return chrome.runtime.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async closeLoginTab() {
|
static async focusTab(tabId: number) {
|
||||||
const tabs = await BrowserApi.tabsQuery({
|
|
||||||
active: true,
|
|
||||||
title: "Bitwarden",
|
|
||||||
windowType: "normal",
|
|
||||||
currentWindow: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabToClose = tabs[tabs.length - 1].id;
|
|
||||||
chrome.tabs.remove(tabToClose);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async focusSpecifiedTab(tabId: number) {
|
|
||||||
chrome.tabs.update(tabId, { active: true, highlighted: true });
|
chrome.tabs.update(tabId, { active: true, highlighted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,13 +10,14 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
|||||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service";
|
import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-generator",
|
selector: "app-generator",
|
||||||
templateUrl: "generator.component.html",
|
templateUrl: "generator.component.html",
|
||||||
})
|
})
|
||||||
export class GeneratorComponent extends BaseGeneratorComponent {
|
export class GeneratorComponent extends BaseGeneratorComponent {
|
||||||
private addEditCipherInfo: any;
|
private addEditCipherInfo: AddEditCipherInfo;
|
||||||
private cipherState: CipherView;
|
private cipherState: CipherView;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -408,28 +408,10 @@ app-root {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds padding on each side of the content if opened in a tab
|
||||||
@media only screen and (min-width: 601px) {
|
@media only screen and (min-width: 601px) {
|
||||||
app-login header {
|
header,
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
main {
|
||||||
}
|
|
||||||
|
|
||||||
app-login main {
|
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
app-two-factor header {
|
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
app-two-factor main {
|
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
app-lock header {
|
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
app-lock main {
|
|
||||||
padding: 0 calc((100% - 500px) / 2);
|
padding: 0 calc((100% - 500px) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
import { fromEvent, Subscription } from "rxjs";
|
||||||
|
|
||||||
import { BrowserApi } from "../../browser/browserApi";
|
import { BrowserApi } from "../../browser/browserApi";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PopupUtilsService {
|
export class PopupUtilsService {
|
||||||
|
private unloadSubscription: Subscription;
|
||||||
|
|
||||||
constructor(private privateMode: boolean = false) {}
|
constructor(private privateMode: boolean = false) {}
|
||||||
|
|
||||||
inSidebar(win: Window): boolean {
|
inSidebar(win: Window): boolean {
|
||||||
@ -80,4 +83,36 @@ export class PopupUtilsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables a pop-up warning before the user exits the window/tab, or navigates away.
|
||||||
|
* This warns the user that they may lose unsaved data if they leave the page.
|
||||||
|
* (Note: navigating within the Angular app will not trigger it because it's an SPA.)
|
||||||
|
* Make sure you call `disableTabCloseWarning` when it is no longer relevant.
|
||||||
|
*/
|
||||||
|
enableCloseTabWarning() {
|
||||||
|
this.disableCloseTabWarning();
|
||||||
|
|
||||||
|
this.unloadSubscription = fromEvent(window, "beforeunload").subscribe(
|
||||||
|
(e: BeforeUnloadEvent) => {
|
||||||
|
// Recommended method but not widely supported
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Modern browsers do not display this message, it just needs to be a non-nullish value
|
||||||
|
// Exact wording is determined by the browser
|
||||||
|
const confirmationMessage = "";
|
||||||
|
|
||||||
|
// Older methods with better support
|
||||||
|
e.returnValue = confirmationMessage;
|
||||||
|
return confirmationMessage;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the warning enabled by enableCloseTabWarning.
|
||||||
|
*/
|
||||||
|
disableCloseTabWarning() {
|
||||||
|
this.unloadSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,15 +130,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
|
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(() => {
|
this.setFocus();
|
||||||
if (!this.editMode) {
|
|
||||||
if (this.cipher.name != null && this.cipher.name !== "") {
|
if (this.popupUtilsService.inTab(window)) {
|
||||||
document.getElementById("loginUsername").focus();
|
this.popupUtilsService.enableCloseTabWarning();
|
||||||
} else {
|
}
|
||||||
document.getElementById("name").focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -149,16 +145,23 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submit(): Promise<boolean> {
|
async submit(): Promise<boolean> {
|
||||||
if (await super.submit()) {
|
const success = await super.submit();
|
||||||
if (this.cloneMode) {
|
if (!success) {
|
||||||
this.router.navigate(["/tabs/vault"]);
|
return false;
|
||||||
} else {
|
}
|
||||||
this.location.back();
|
|
||||||
}
|
if (this.popupUtilsService.inTab(window)) {
|
||||||
|
this.popupUtilsService.disableCloseTabWarning();
|
||||||
|
this.messagingService.send("closeTab", { delay: 1000 });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if (this.cloneMode) {
|
||||||
|
this.router.navigate(["/tabs/vault"]);
|
||||||
|
} else {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachments() {
|
attachments() {
|
||||||
@ -184,6 +187,12 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
super.cancel();
|
super.cancel();
|
||||||
|
|
||||||
|
if (this.popupUtilsService.inTab(window)) {
|
||||||
|
this.messagingService.send("closeTab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,4 +244,18 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
|
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setFocus() {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (this.editMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher.name != null && this.cipher.name !== "") {
|
||||||
|
document.getElementById("loginUsername").focus();
|
||||||
|
} else {
|
||||||
|
document.getElementById("name").focus();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ const mainConfig = {
|
|||||||
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
|
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
|
||||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||||
"content/message_handler": "./src/autofill/content/message_handler.ts",
|
"content/message_handler": "./src/autofill/content/message_handler.ts",
|
||||||
"notification/bar": "./src/autofill/notification/bar.js",
|
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||||
"encrypt-worker": "../../libs/common/src/services/cryptography/encrypt.worker.ts",
|
"encrypt-worker": "../../libs/common/src/services/cryptography/encrypt.worker.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
|
@ -202,7 +202,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!this.allowPersonal) {
|
if (!this.allowPersonal) {
|
||||||
this.organizationId = this.ownershipOptions[0].value;
|
this.organizationId = this.defaultOwnerId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,12 +220,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
this.title = this.i18nService.t("addItem");
|
this.title = this.i18nService.t("addItem");
|
||||||
}
|
}
|
||||||
|
|
||||||
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
|
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
|
||||||
if (addEditCipherInfo != null) {
|
|
||||||
this.cipher = addEditCipherInfo.cipher;
|
|
||||||
this.collectionIds = addEditCipherInfo.collectionIds;
|
|
||||||
}
|
|
||||||
await this.stateService.setAddEditCipherInfo(null);
|
|
||||||
|
|
||||||
if (this.cipher == null) {
|
if (this.cipher == null) {
|
||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
@ -255,7 +250,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) {
|
if (this.cipher != null && (!this.editMode || loadedAddEditCipherInfo || this.cloneMode)) {
|
||||||
await this.organizationChanged();
|
await this.organizationChanged();
|
||||||
if (
|
if (
|
||||||
this.collectionIds != null &&
|
this.collectionIds != null &&
|
||||||
@ -618,4 +613,27 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
protected restoreCipher() {
|
protected restoreCipher() {
|
||||||
return this.cipherService.restoreWithServer(this.cipher.id);
|
return this.cipherService.restoreWithServer(this.cipher.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get defaultOwnerId(): string | null {
|
||||||
|
return this.ownershipOptions[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAddEditCipherInfo(): Promise<boolean> {
|
||||||
|
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
|
||||||
|
const loadedSavedInfo = addEditCipherInfo != null;
|
||||||
|
|
||||||
|
if (loadedSavedInfo) {
|
||||||
|
this.cipher = addEditCipherInfo.cipher;
|
||||||
|
this.collectionIds = addEditCipherInfo.collectionIds;
|
||||||
|
|
||||||
|
if (!this.editMode && !this.allowPersonal && this.cipher.organizationId == null) {
|
||||||
|
// This is a new cipher and personal ownership isn't allowed, so we need to set the default owner
|
||||||
|
this.cipher.organizationId = this.defaultOwnerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateService.setAddEditCipherInfo(null);
|
||||||
|
|
||||||
|
return loadedSavedInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import { CipherData } from "../vault/models/data/cipher.data";
|
|||||||
import { FolderData } from "../vault/models/data/folder.data";
|
import { FolderData } from "../vault/models/data/folder.data";
|
||||||
import { LocalData } from "../vault/models/data/local.data";
|
import { LocalData } from "../vault/models/data/local.data";
|
||||||
import { CipherView } from "../vault/models/view/cipher.view";
|
import { CipherView } from "../vault/models/view/cipher.view";
|
||||||
|
import { AddEditCipherInfo } from "../vault/types/add-edit-cipher-info";
|
||||||
|
|
||||||
export abstract class StateService<T extends Account = Account> {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
accounts$: Observable<{ [userId: string]: T }>;
|
accounts$: Observable<{ [userId: string]: T }>;
|
||||||
@ -39,8 +40,8 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
|
|
||||||
getAccessToken: (options?: StorageOptions) => Promise<string>;
|
getAccessToken: (options?: StorageOptions) => Promise<string>;
|
||||||
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
|
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getAddEditCipherInfo: (options?: StorageOptions) => Promise<any>;
|
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
|
||||||
setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise<void>;
|
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
|
||||||
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
|
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;
|
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;
|
||||||
|
@ -45,6 +45,7 @@ import { CipherData } from "../vault/models/data/cipher.data";
|
|||||||
import { FolderData } from "../vault/models/data/folder.data";
|
import { FolderData } from "../vault/models/data/folder.data";
|
||||||
import { LocalData } from "../vault/models/data/local.data";
|
import { LocalData } from "../vault/models/data/local.data";
|
||||||
import { CipherView } from "../vault/models/view/cipher.view";
|
import { CipherView } from "../vault/models/view/cipher.view";
|
||||||
|
import { AddEditCipherInfo } from "../vault/types/add-edit-cipher-info";
|
||||||
|
|
||||||
const keys = {
|
const keys = {
|
||||||
state: "state",
|
state: "state",
|
||||||
@ -222,13 +223,13 @@ export class StateService<
|
|||||||
await this.saveAccount(account, options);
|
await this.saveAccount(account, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAddEditCipherInfo(options?: StorageOptions): Promise<any> {
|
async getAddEditCipherInfo(options?: StorageOptions): Promise<AddEditCipherInfo> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
)?.data?.addEditCipherInfo;
|
)?.data?.addEditCipherInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAddEditCipherInfo(value: any, options?: StorageOptions): Promise<void> {
|
async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
|
12
libs/common/src/vault/types/add-edit-cipher-info.ts
Normal file
12
libs/common/src/vault/types/add-edit-cipher-info.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to temporarily save the state of the AddEditComponent, e.g. when the user navigates away to the Generator page.
|
||||||
|
* @property cipher The unsaved item being added or edited
|
||||||
|
* @property collectionIds The collections that are selected for the item (currently these are not mapped back to
|
||||||
|
* cipher.collectionIds until the item is saved)
|
||||||
|
*/
|
||||||
|
export type AddEditCipherInfo = {
|
||||||
|
cipher: CipherView;
|
||||||
|
collectionIds?: string[];
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user