Apply Prettier (#2238)

This commit is contained in:
Oscar Hinton 2021-12-21 15:43:35 +01:00 committed by GitHub
parent cebee8aa81
commit 8fe821b9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 17599 additions and 14766 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -6,7 +6,7 @@ body:
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests. Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: textarea - type: textarea
id: reproduce id: reproduce

View File

@ -1,4 +1,5 @@
## Type of change ## Type of change
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature development - [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
@ -6,27 +7,26 @@
- [ ] Other - [ ] Other
## Objective ## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding--> <!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes ## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes--> <!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories--> <!--Also refer to any related changes or PRs in other repositories-->
* **file.ext:** Description of what was changed and why - **file.ext:** Description of what was changed and why
## Screenshots ## Screenshots
<!--Required for any UI changes. Delete if not applicable--> <!--Required for any UI changes. Delete if not applicable-->
## Testing requirements ## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing--> <!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit ## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required) - [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] This change requires a **documentation update** (notify the documentation team) - [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team) - [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@ -104,8 +104,8 @@ jobs:
npm run dist npm run dist
npm run test npm run test
# - name: Run linter - name: Run linter
# run: npm run lint run: npm run lint
- name: Gulp - name: Gulp
run: gulp ci run: gulp ci

View File

@ -6,17 +6,12 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
Here is how you can get involved: Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one - **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code - **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Report a bug or submit a bugfix:** Use Github issues and pull requests - **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
- **Translate:** See the localization (i10n) section below
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (i10n) section below
## Contributor Agreement ## Contributor Agreement
@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/brows
## Pull Request Guidelines ## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request - use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch - commit any pull requests against the `master` branch
* include a link to your Community Forums post - include a link to your Community Forums post
# Localization (l10n) # Localization (l10n)

View File

@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue. effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate. third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or - Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder. account holder.

View File

@ -1,223 +1,242 @@
const gulp = require('gulp'), const gulp = require("gulp"),
gulpif = require('gulp-if'), gulpif = require("gulp-if"),
filter = require('gulp-filter'), filter = require("gulp-filter"),
replace = require('gulp-replace'), replace = require("gulp-replace"),
jeditor = require("gulp-json-editor"), jeditor = require("gulp-json-editor"),
child = require('child_process'), child = require("child_process"),
zip = require('gulp-zip'), zip = require("gulp-zip"),
manifest = require('./src/manifest.json'), manifest = require("./src/manifest.json"),
del = require('del'), del = require("del"),
fs = require('fs'); fs = require("fs");
const paths = { const paths = {
build: './build/', build: "./build/",
dist: './dist/', dist: "./dist/",
coverage: './coverage/', coverage: "./coverage/",
node_modules: './node_modules/', node_modules: "./node_modules/",
popupDir: './src/popup/', popupDir: "./src/popup/",
cssDir: './src/popup/css/', cssDir: "./src/popup/css/",
safari: './src/safari/' safari: "./src/safari/",
}; };
const filters = { const filters = {
fonts: [ fonts: [
'!build/popup/fonts/*', "!build/popup/fonts/*",
'build/popup/fonts/Open_Sans*.woff', "build/popup/fonts/Open_Sans*.woff",
'build/popup/fonts/fontawesome*.woff2', "build/popup/fonts/fontawesome*.woff2",
'build/popup/fonts/fontawesome*.woff' "build/popup/fonts/fontawesome*.woff",
], ],
safari: [ safari: ["!build/safari/**/*"],
'!build/safari/**/*'
],
}; };
function buildString() { function buildString() {
var build = ''; var build = "";
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== '') { if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`; build = `-${process.env.BUILD_NUMBER}`;
} }
return build; return build;
} }
function distFileName(browserName, ext) { function distFileName(browserName, ext) {
return `dist-${browserName}${buildString()}.${ext}`; return `dist-${browserName}${buildString()}.${ext}`;
} }
function dist(browserName, manifest) { function dist(browserName, manifest) {
return gulp.src(paths.build + '**/*') return gulp
.pipe(filter(['**'].concat(filters.fonts).concat(filters.safari))) .src(paths.build + "**/*")
.pipe(gulpif('popup/index.html', replace('__BROWSER__', 'browser_' + browserName))) .pipe(filter(["**"].concat(filters.fonts).concat(filters.safari)))
.pipe(gulpif('manifest.json', jeditor(manifest))) .pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_" + browserName)))
.pipe(zip(distFileName(browserName, 'zip'))) .pipe(gulpif("manifest.json", jeditor(manifest)))
.pipe(gulp.dest(paths.dist)); .pipe(zip(distFileName(browserName, "zip")))
.pipe(gulp.dest(paths.dist));
} }
function distFirefox() { function distFirefox() {
return dist('firefox', (manifest) => { return dist("firefox", (manifest) => {
delete manifest.content_security_policy; delete manifest.content_security_policy;
removeShortcuts(manifest); removeShortcuts(manifest);
return manifest; return manifest;
}); });
} }
function distOpera() { function distOpera() {
return dist('opera', (manifest) => { return dist("opera", (manifest) => {
delete manifest.applications; delete manifest.applications;
delete manifest.content_security_policy; delete manifest.content_security_policy;
removeShortcuts(manifest); removeShortcuts(manifest);
return manifest; return manifest;
}); });
} }
function distChrome() { function distChrome() {
return dist('chrome', (manifest) => { return dist("chrome", (manifest) => {
delete manifest.applications; delete manifest.applications;
delete manifest.content_security_policy; delete manifest.content_security_policy;
delete manifest.sidebar_action; delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
return manifest; return manifest;
}); });
} }
function distEdge() { function distEdge() {
return dist('edge', (manifest) => { return dist("edge", (manifest) => {
delete manifest.applications; delete manifest.applications;
delete manifest.content_security_policy; delete manifest.content_security_policy;
delete manifest.sidebar_action; delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
return manifest; return manifest;
}); });
} }
function removeShortcuts(manifest) { function removeShortcuts(manifest) {
if (manifest.content_scripts && manifest.content_scripts.length > 1) { if (manifest.content_scripts && manifest.content_scripts.length > 1) {
const shortcutsScript = manifest.content_scripts[1]; const shortcutsScript = manifest.content_scripts[1];
if (shortcutsScript.js.indexOf('content/shortcuts.js') > -1) { if (shortcutsScript.js.indexOf("content/shortcuts.js") > -1) {
manifest.content_scripts.splice(1, 1); manifest.content_scripts.splice(1, 1);
}
} }
}
} }
function distSafariMas(cb) { function distSafariMas(cb) {
return distSafariApp(cb, 'mas'); return distSafariApp(cb, "mas");
} }
function distSafariMasDev(cb) { function distSafariMasDev(cb) {
return distSafariApp(cb, 'masdev'); return distSafariApp(cb, "masdev");
} }
function distSafariDmg(cb) { function distSafariDmg(cb) {
return distSafariApp(cb, 'dmg'); return distSafariApp(cb, "dmg");
} }
function distSafariApp(cb, subBuildPath) { function distSafariApp(cb, subBuildPath) {
const buildPath = paths.dist + 'Safari/' + subBuildPath + '/'; const buildPath = paths.dist + "Safari/" + subBuildPath + "/";
const builtAppexPath = buildPath + 'build/Release/safari.appex'; const builtAppexPath = buildPath + "build/Release/safari.appex";
const builtAppexFrameworkPath = buildPath + 'build/Release/safari.appex/Contents/Frameworks/'; const builtAppexFrameworkPath = buildPath + "build/Release/safari.appex/Contents/Frameworks/";
const entitlementsPath = paths.safari + 'safari/safari.entitlements'; const entitlementsPath = paths.safari + "safari/safari.entitlements";
var args = [ var args = [
'--verbose', "--verbose",
'--force', "--force",
'-o', "-o",
'runtime', "runtime",
'--sign', "--sign",
'Developer ID Application: 8bit Solutions LLC', "Developer ID Application: 8bit Solutions LLC",
'--entitlements', "--entitlements",
entitlementsPath entitlementsPath,
];
if (subBuildPath !== "dmg") {
args = [
"--verbose",
"--force",
"--sign",
subBuildPath === "mas"
? "3rd Party Mac Developer Application: 8bit Solutions LLC"
: "6B287DD81FF922D86FD836128B0F62F358B38726",
"--entitlements",
entitlementsPath,
]; ];
if (subBuildPath !== 'dmg') { }
args = [
'--verbose',
'--force',
'--sign',
subBuildPath === 'mas' ? '3rd Party Mac Developer Application: 8bit Solutions LLC' :
'6B287DD81FF922D86FD836128B0F62F358B38726',
'--entitlements',
entitlementsPath
];
}
return del([buildPath + '**/*']) return del([buildPath + "**/*"])
.then(() => safariCopyAssets(paths.safari + '**/*', buildPath)) .then(() => safariCopyAssets(paths.safari + "**/*", buildPath))
.then(() => safariCopyBuild(paths.build + '**/*', buildPath + 'safari/app')) .then(() => safariCopyBuild(paths.build + "**/*", buildPath + "safari/app"))
.then(() => { .then(() => {
const proc = child.spawn('xcodebuild', [ const proc = child.spawn("xcodebuild", [
'-project', "-project",
buildPath + 'desktop.xcodeproj', buildPath + "desktop.xcodeproj",
'-alltargets', "-alltargets",
'-configuration', "-configuration",
'Release']); "Release",
stdOutProc(proc); ]);
return new Promise((resolve) => proc.on('close', resolve)); stdOutProc(proc);
}).then(() => { return new Promise((resolve) => proc.on("close", resolve));
const libs = fs.readdirSync(builtAppexFrameworkPath).filter((p) => p.endsWith('.dylib')) })
.map((p) => builtAppexFrameworkPath + p); .then(() => {
const libPromises = []; const libs = fs
libs.forEach((i) => { .readdirSync(builtAppexFrameworkPath)
const proc = child.spawn('codesign', args.concat([i])); .filter((p) => p.endsWith(".dylib"))
stdOutProc(proc); .map((p) => builtAppexFrameworkPath + p);
libPromises.push(new Promise((resolve) => proc.on('close', resolve))); const libPromises = [];
}); libs.forEach((i) => {
return Promise.all(libPromises); const proc = child.spawn("codesign", args.concat([i]));
}).then(() => { stdOutProc(proc);
const proc = child.spawn('codesign', args.concat([builtAppexPath])); libPromises.push(new Promise((resolve) => proc.on("close", resolve)));
stdOutProc(proc); });
return new Promise((resolve) => proc.on('close', resolve)); return Promise.all(libPromises);
}).then(() => { })
return cb; .then(() => {
}, () => { const proc = child.spawn("codesign", args.concat([builtAppexPath]));
return cb; stdOutProc(proc);
}); return new Promise((resolve) => proc.on("close", resolve));
})
.then(
() => {
return cb;
},
() => {
return cb;
}
);
} }
function safariCopyAssets(source, dest) { function safariCopyAssets(source, dest) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
gulp.src(source) gulp
.on('error', reject) .src(source)
.pipe(gulpif('safari/Info.plist', replace('0.0.1', manifest.version))) .on("error", reject)
.pipe(gulpif('safari/Info.plist', replace('0.0.2', process.env.BUILD_NUMBER || manifest.version))) .pipe(gulpif("safari/Info.plist", replace("0.0.1", manifest.version)))
.pipe(gulpif('desktop.xcodeproj/project.pbxproj', replace('../../../build', '../safari/app'))) .pipe(
.pipe(gulp.dest(dest)) gulpif("safari/Info.plist", replace("0.0.2", process.env.BUILD_NUMBER || manifest.version))
.on('end', resolve); )
}); .pipe(gulpif("desktop.xcodeproj/project.pbxproj", replace("../../../build", "../safari/app")))
.pipe(gulp.dest(dest))
.on("end", resolve);
});
} }
function safariCopyBuild(source, dest) { function safariCopyBuild(source, dest) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
gulp.src(source) gulp
.on('error', reject) .src(source)
.pipe(filter(['**'].concat(filters.fonts))) .on("error", reject)
.pipe(gulpif('popup/index.html', replace('__BROWSER__', 'browser_safari'))) .pipe(filter(["**"].concat(filters.fonts)))
.pipe(gulpif('manifest.json', jeditor((manifest) => { .pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_safari")))
delete manifest.optional_permissions; .pipe(
manifest.permissions.push("nativeMessaging"); gulpif(
return manifest; "manifest.json",
}))) jeditor((manifest) => {
.pipe(gulp.dest(dest)) delete manifest.optional_permissions;
.on('end', resolve); manifest.permissions.push("nativeMessaging");
}); return manifest;
})
)
)
.pipe(gulp.dest(dest))
.on("end", resolve);
});
} }
function stdOutProc(proc) { function stdOutProc(proc) {
proc.stdout.on('data', (data) => console.log(data.toString())); proc.stdout.on("data", (data) => console.log(data.toString()));
proc.stderr.on('data', (data) => console.error(data.toString())); proc.stderr.on("data", (data) => console.error(data.toString()));
} }
function ciCoverage(cb) { function ciCoverage(cb) {
return gulp.src(paths.coverage + '**/*') return gulp
.pipe(filter(['**', '!coverage/coverage*.zip'])) .src(paths.coverage + "**/*")
.pipe(zip(`coverage${buildString()}.zip`)) .pipe(filter(["**", "!coverage/coverage*.zip"]))
.pipe(gulp.dest(paths.coverage)); .pipe(zip(`coverage${buildString()}.zip`))
.pipe(gulp.dest(paths.coverage));
} }
exports['dist:firefox'] = distFirefox; exports["dist:firefox"] = distFirefox;
exports['dist:chrome'] = distChrome; exports["dist:chrome"] = distChrome;
exports['dist:opera'] = distOpera; exports["dist:opera"] = distOpera;
exports['dist:edge'] = distEdge; exports["dist:edge"] = distEdge;
exports['dist:safari'] = gulp.parallel(distSafariMas, distSafariMasDev, distSafariDmg); exports["dist:safari"] = gulp.parallel(distSafariMas, distSafariMasDev, distSafariDmg);
exports['dist:safari:mas'] = distSafariMas; exports["dist:safari:mas"] = distSafariMas;
exports['dist:safari:masdev'] = distSafariMasDev; exports["dist:safari:masdev"] = distSafariMasDev;
exports['dist:safari:dmg'] = distSafariDmg; exports["dist:safari:dmg"] = distSafariDmg;
exports.dist = gulp.parallel(distFirefox, distChrome, distOpera, distEdge); exports.dist = gulp.parallel(distFirefox, distChrome, distOpera, distEdge);
exports['ci:coverage'] = ciCoverage; exports["ci:coverage"] = ciCoverage;
exports.ci = ciCoverage; exports.ci = ciCoverage;

View File

@ -1,76 +1,71 @@
const path = require('path'); const path = require("path");
module.exports = function(config) { module.exports = function (config) {
config.set({ config.set({
// base path that will be used to resolve all patterns (eg. files, exclude) // base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '', basePath: "",
// frameworks to use // frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine', 'webpack'], frameworks: ["jasmine", "webpack"],
// list of files / patterns to load in the browser // list of files / patterns to load in the browser
files: [ files: [{ pattern: "src/**/*.spec.ts", watch: false }],
{ pattern: 'src/**/*.spec.ts', watch: false },
],
exclude: [ exclude: [],
],
// preprocess matching files before serving them to the browser // preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: { preprocessors: {
'src/**/*.ts': 'webpack' "src/**/*.ts": "webpack",
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "kjhtml"],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Chrome"],
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
webpack: {
mode: "production",
resolve: {
extensions: [".js", ".ts", ".tsx"],
alias: {
"jslib-common": path.join(__dirname, "jslib/common/src"),
"jslib-angular": path.join(__dirname, "jslib/angular/src"),
}, },
},
// test results reporter to use module: {
// possible values: 'dots', 'progress' rules: [{ test: /\.tsx?$/, loader: "ts-loader" }],
// available reporters: https://npmjs.org/browse/keyword/karma-reporter },
reporters: ['progress', 'kjhtml'], stats: {
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true, colors: true,
modules: true,
// level of logging reasons: true,
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG errorDetails: true,
logLevel: config.LOG_INFO, },
devtool: "inline-source-map",
// start these browsers },
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher });
browsers: ['Chrome'], };
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
webpack: {
mode: 'production',
resolve: {
extensions: ['.js', '.ts', '.tsx'],
alias: {
"jslib-common": path.join(__dirname, 'jslib/common/src'),
"jslib-angular": path.join(__dirname, 'jslib/angular/src'),
},
},
module: {
rules: [
{test: /\.tsx?$/, loader: 'ts-loader'}
]
},
stats: {
colors: true,
modules: true,
reasons: true,
errorDetails: true
},
devtool: 'inline-source-map',
},
})
}

View File

@ -1,7 +1,6 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
</head> </head>
<body> <body></body>
</body>
</html> </html>

View File

@ -1,6 +1,6 @@
import MainBackground from './background/main.background'; import MainBackground from "./background/main.background";
const bitwardenMain = (window as any).bitwardenMain = new MainBackground(); const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().then(() => { bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping // Finished bootstrapping
}); });

View File

@ -1,98 +1,112 @@
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import MainBackground from './main.background'; import MainBackground from "./main.background";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class CommandsBackground { export default class CommandsBackground {
private isSafari: boolean; private isSafari: boolean;
private isVivaldi: boolean; private isVivaldi: boolean;
constructor(private main: MainBackground, private passwordGenerationService: PasswordGenerationService, constructor(
private platformUtilsService: PlatformUtilsService, private vaultTimeoutService: VaultTimeoutService) { private main: MainBackground,
this.isSafari = this.platformUtilsService.isSafari(); private passwordGenerationService: PasswordGenerationService,
this.isVivaldi = this.platformUtilsService.isVivaldi(); private platformUtilsService: PlatformUtilsService,
} private vaultTimeoutService: VaultTimeoutService
) {
this.isSafari = this.platformUtilsService.isSafari();
this.isVivaldi = this.platformUtilsService.isVivaldi();
}
async init() { async init() {
BrowserApi.messageListener('commands.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { BrowserApi.messageListener(
if (msg.command === 'unlockCompleted' && msg.data.target === 'commands.background') { "commands.background",
await this.processCommand(msg.data.commandToRetry.msg.command, msg.data.commandToRetry.sender); async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
} if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") {
await this.processCommand(
if (this.isVivaldi && msg.command === 'keyboardShortcutTriggered' && msg.shortcut) { msg.data.commandToRetry.msg.command,
await this.processCommand(msg.shortcut, sender); msg.data.commandToRetry.sender
} );
});
if (!this.isVivaldi && chrome && chrome.commands) {
chrome.commands.onCommand.addListener(async (command: string) => {
await this.processCommand(command);
});
}
}
private async processCommand(command: string, sender?: chrome.runtime.MessageSender) {
switch (command) {
case 'generate_password':
await this.generatePasswordToClipboard();
break;
case 'autofill_login':
await this.autoFillLogin(sender ? sender.tab : null);
break;
case 'open_popup':
await this.openPopup();
break;
case 'lock_vault':
await this.vaultTimeoutService.lock(true);
break;
default:
break;
}
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
}
private async autoFillLogin(tab?: chrome.tabs.Tab) {
if (!tab) {
tab = await BrowserApi.getTabFromCurrentWindowId();
} }
if (tab == null) { if (this.isVivaldi && msg.command === "keyboardShortcutTriggered" && msg.shortcut) {
return; await this.processCommand(msg.shortcut, sender);
} }
}
);
if (await this.vaultTimeoutService.isLocked()) { if (!this.isVivaldi && chrome && chrome.commands) {
const retryMessage: LockedVaultPendingNotificationsItem = { chrome.commands.onCommand.addListener(async (command: string) => {
commandToRetry: { await this.processCommand(command);
msg: { command: 'autofill_login' }, });
sender: { tab: tab }, }
}, }
target: 'commands.background',
};
await BrowserApi.tabSendMessageData(tab, 'addToLockedVaultPendingNotifications', retryMessage);
BrowserApi.tabSendMessageData(tab, 'promptForLogin'); private async processCommand(command: string, sender?: chrome.runtime.MessageSender) {
return; switch (command) {
} case "generate_password":
await this.generatePasswordToClipboard();
break;
case "autofill_login":
await this.autoFillLogin(sender ? sender.tab : null);
break;
case "open_popup":
await this.openPopup();
break;
case "lock_vault":
await this.vaultTimeoutService.lock(true);
break;
default:
break;
}
}
await this.main.collectPageDetailsForContentScript(tab, 'autofill_cmd'); private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
}
private async autoFillLogin(tab?: chrome.tabs.Tab) {
if (!tab) {
tab = await BrowserApi.getTabFromCurrentWindowId();
} }
private async openPopup() { if (tab == null) {
// Chrome APIs cannot open popup return;
if (!this.isSafari) {
return;
}
this.main.openPopup();
} }
if (await this.vaultTimeoutService.isLocked()) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: "autofill_login" },
sender: { tab: tab },
},
target: "commands.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd");
}
private async openPopup() {
// Chrome APIs cannot open popup
if (!this.isSafari) {
return;
}
this.main.openPopup();
}
} }

View File

@ -1,123 +1,142 @@
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import MainBackground from './main.background'; import MainBackground from "./main.background";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { EventType } from 'jslib-common/enums/eventType'; import { EventType } from "jslib-common/enums/eventType";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class ContextMenusBackground { export default class ContextMenusBackground {
private readonly noopCommandSuffix = 'noop'; private readonly noopCommandSuffix = "noop";
private contextMenus: any; private contextMenus: any;
constructor(private main: MainBackground, private cipherService: CipherService, constructor(
private passwordGenerationService: PasswordGenerationService, private main: MainBackground,
private platformUtilsService: PlatformUtilsService, private vaultTimeoutService: VaultTimeoutService, private cipherService: CipherService,
private eventService: EventService, private totpService: TotpService) { private passwordGenerationService: PasswordGenerationService,
this.contextMenus = chrome.contextMenus; private platformUtilsService: PlatformUtilsService,
private vaultTimeoutService: VaultTimeoutService,
private eventService: EventService,
private totpService: TotpService
) {
this.contextMenus = chrome.contextMenus;
}
async init() {
if (!this.contextMenus) {
return;
} }
async init() { this.contextMenus.onClicked.addListener(
if (!this.contextMenus) { async (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => {
return; if (info.menuItemId === "generate-password") {
await this.generatePasswordToClipboard();
} else if (info.menuItemId === "copy-identifier") {
await this.getClickedElement(tab, info.frameId);
} else if (
info.parentMenuItemId === "autofill" ||
info.parentMenuItemId === "copy-username" ||
info.parentMenuItemId === "copy-password" ||
info.parentMenuItemId === "copy-totp"
) {
await this.cipherAction(tab, info);
} }
}
);
this.contextMenus.onClicked.addListener(async (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => { BrowserApi.messageListener(
if (info.menuItemId === 'generate-password') { "contextmenus.background",
await this.generatePasswordToClipboard(); async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
} else if (info.menuItemId === 'copy-identifier') { if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
await this.getClickedElement(tab, info.frameId); await this.cipherAction(
} else if (info.parentMenuItemId === 'autofill' || msg.data.commandToRetry.sender.tab,
info.parentMenuItemId === 'copy-username' || msg.data.commandToRetry.msg.data
info.parentMenuItemId === 'copy-password' || );
info.parentMenuItemId === 'copy-totp') { }
await this.cipherAction(tab, info); }
} );
}); }
BrowserApi.messageListener('contextmenus.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { private async generatePasswordToClipboard() {
if (msg.command === 'unlockCompleted' && msg.data.target === 'contextmenus.background') { const options = (await this.passwordGenerationService.getOptions())[0];
await this.cipherAction(msg.data.commandToRetry.sender.tab, msg.data.commandToRetry.msg.data); const password = await this.passwordGenerationService.generatePassword(options);
} this.platformUtilsService.copyToClipboard(password, { window: window });
}); this.passwordGenerationService.addHistory(password);
}
private async getClickedElement(tab: chrome.tabs.Tab, frameId: number) {
if (tab == null) {
return;
} }
private async generatePasswordToClipboard() { BrowserApi.tabSendMessage(tab, { command: "getClickedElement" }, { frameId: frameId });
const options = (await this.passwordGenerationService.getOptions())[0]; }
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window }); private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
this.passwordGenerationService.addHistory(password); const id = info.menuItemId.split("_")[1];
if (await this.vaultTimeoutService.isLocked()) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: this.noopCommandSuffix, data: info },
sender: { tab: tab },
},
target: "contextmenus.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
} }
private async getClickedElement(tab: chrome.tabs.Tab, frameId: number) { let cipher: CipherView;
if (tab == null) { if (id === this.noopCommandSuffix) {
return; const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
} cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
} else {
BrowserApi.tabSendMessage(tab, { command: 'getClickedElement' }, { frameId: frameId }); const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find((c) => c.id === id);
} }
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) { if (cipher == null) {
const id = info.menuItemId.split('_')[1]; return;
if (await this.vaultTimeoutService.isLocked()) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: this.noopCommandSuffix, data: info },
sender: { tab: tab },
},
target: 'contextmenus.background',
};
await BrowserApi.tabSendMessageData(tab, 'addToLockedVaultPendingNotifications', retryMessage);
BrowserApi.tabSendMessageData(tab, 'promptForLogin');
return;
}
let cipher: CipherView;
if (id === this.noopCommandSuffix) {
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
cipher = ciphers.find(c => c.reprompt === CipherRepromptType.None);
} else {
const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find(c => c.id === id);
}
if (cipher == null) {
return;
}
if (info.parentMenuItemId === 'autofill') {
await this.startAutofillPage(tab, cipher);
} else if (info.parentMenuItemId === 'copy-username') {
this.platformUtilsService.copyToClipboard(cipher.login.username, { window: window });
} else if (info.parentMenuItemId === 'copy-password') {
this.platformUtilsService.copyToClipboard(cipher.login.password, { window: window });
this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (info.parentMenuItemId === 'copy-totp') {
const totpValue = await this.totpService.getCode(cipher.login.totp);
this.platformUtilsService.copyToClipboard(totpValue, { window: window });
}
} }
private async startAutofillPage(tab: chrome.tabs.Tab, cipher: CipherView) { if (info.parentMenuItemId === "autofill") {
this.main.loginToAutoFill = cipher; await this.startAutofillPage(tab, cipher);
if (tab == null) { } else if (info.parentMenuItemId === "copy-username") {
return; this.platformUtilsService.copyToClipboard(cipher.login.username, { window: window });
} } else if (info.parentMenuItemId === "copy-password") {
this.platformUtilsService.copyToClipboard(cipher.login.password, { window: window });
BrowserApi.tabSendMessage(tab, { this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
command: 'collectPageDetails', } else if (info.parentMenuItemId === "copy-totp") {
tab: tab, const totpValue = await this.totpService.getCode(cipher.login.totp);
sender: 'contextMenu', this.platformUtilsService.copyToClipboard(totpValue, { window: window });
});
} }
}
private async startAutofillPage(tab: chrome.tabs.Tab, cipher: CipherView) {
this.main.loginToAutoFill = cipher;
if (tab == null) {
return;
}
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
}
} }

View File

@ -1,68 +1,75 @@
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
const IdleInterval = 60 * 5; // 5 minutes const IdleInterval = 60 * 5; // 5 minutes
export default class IdleBackground { export default class IdleBackground {
private idle: any; private idle: any;
private idleTimer: number = null; private idleTimer: number = null;
private idleState = 'active'; private idleState = "active";
constructor(private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService, constructor(
private notificationsService: NotificationsService) { private vaultTimeoutService: VaultTimeoutService,
this.idle = chrome.idle || (browser != null ? browser.idle : null); private storageService: StorageService,
private notificationsService: NotificationsService
) {
this.idle = chrome.idle || (browser != null ? browser.idle : null);
}
async init() {
if (!this.idle) {
return;
} }
async init() { const idleHandler = (newState: string) => {
if (!this.idle) { if (newState === "active") {
return; this.notificationsService.reconnectFromActivity();
} } else {
this.notificationsService.disconnectFromInactivity();
}
};
if (this.idle.onStateChanged && this.idle.setDetectionInterval) {
this.idle.setDetectionInterval(IdleInterval);
this.idle.onStateChanged.addListener(idleHandler);
} else {
this.pollIdle(idleHandler);
}
const idleHandler = (newState: string) => { if (this.idle.onStateChanged) {
if (newState === 'active') { this.idle.onStateChanged.addListener(async (newState: string) => {
this.notificationsService.reconnectFromActivity(); if (newState === "locked") {
// If the screen is locked or the screensaver activates
const timeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
if (timeout === -2) {
// On System Lock vault timeout option
const action = await this.storageService.get<string>(
ConstantsService.vaultTimeoutActionKey
);
if (action === "logOut") {
await this.vaultTimeoutService.logOut();
} else { } else {
this.notificationsService.disconnectFromInactivity(); await this.vaultTimeoutService.lock(true);
} }
}; }
if (this.idle.onStateChanged && this.idle.setDetectionInterval) {
this.idle.setDetectionInterval(IdleInterval);
this.idle.onStateChanged.addListener(idleHandler);
} else {
this.pollIdle(idleHandler);
}
if (this.idle.onStateChanged) {
this.idle.onStateChanged.addListener(async (newState: string) => {
if (newState === 'locked') { // If the screen is locked or the screensaver activates
const timeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
if (timeout === -2) { // On System Lock vault timeout option
const action = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey);
if (action === 'logOut') {
await this.vaultTimeoutService.logOut();
} else {
await this.vaultTimeoutService.lock(true);
}
}
}
});
} }
});
} }
}
private pollIdle(handler: (newState: string) => void) { private pollIdle(handler: (newState: string) => void) {
if (this.idleTimer != null) { if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer); window.clearTimeout(this.idleTimer);
this.idleTimer = null; this.idleTimer = null;
}
this.idle.queryState(IdleInterval, (state: string) => {
if (state !== this.idleState) {
this.idleState = state;
handler(state);
}
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
});
} }
this.idle.queryState(IdleInterval, (state: string) => {
if (state !== this.idleState) {
this.idleState = state;
handler(state);
}
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import NotificationQueueMessage from './notificationQueueMessage'; import NotificationQueueMessage from "./notificationQueueMessage";
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage { export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
cipherId: string; cipherId: string;
newPassword: string; newPassword: string;
} }

View File

@ -1,7 +1,7 @@
import NotificationQueueMessage from './notificationQueueMessage'; import NotificationQueueMessage from "./notificationQueueMessage";
export default class AddLoginQueueMessage extends NotificationQueueMessage { export default class AddLoginQueueMessage extends NotificationQueueMessage {
username: string; username: string;
password: string; password: string;
uri: string; uri: string;
} }

View File

@ -1,5 +1,5 @@
export default class AddLoginRuntimeMessage { export default class AddLoginRuntimeMessage {
username: string; username: string;
password: string; password: string;
url: string; url: string;
} }

View File

@ -1,5 +1,5 @@
export default class ChangePasswordRuntimeMessage { export default class ChangePasswordRuntimeMessage {
currentPassword: string; currentPassword: string;
newPassword: string; newPassword: string;
url: string; url: string;
} }

View File

@ -1,7 +1,7 @@
export default class LockedVaultPendingNotificationsItem { export default class LockedVaultPendingNotificationsItem {
commandToRetry: { commandToRetry: {
msg: any; msg: any;
sender: chrome.runtime.MessageSender; sender: chrome.runtime.MessageSender;
}; };
target: string; target: string;
} }

View File

@ -1,9 +1,9 @@
import { NotificationQueueMessageType } from './notificationQueueMessageType'; import { NotificationQueueMessageType } from "./notificationQueueMessageType";
export default class NotificationQueueMessage { export default class NotificationQueueMessage {
type: NotificationQueueMessageType; type: NotificationQueueMessageType;
domain: string; domain: string;
tabId: number; tabId: number;
expires: Date; expires: Date;
wasVaultLocked: boolean; wasVaultLocked: boolean;
} }

View File

@ -1,4 +1,4 @@
export enum NotificationQueueMessageType { export enum NotificationQueueMessageType {
addLogin = 'addLogin', addLogin = "addLogin",
changePassword = 'changePassword', changePassword = "changePassword",
} }

View File

@ -1,333 +1,350 @@
import { AppIdService } from 'jslib-common/abstractions/appId.service'; import { AppIdService } from "jslib-common/abstractions/appId.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import RuntimeBackground from './runtime.background'; import RuntimeBackground from "./runtime.background";
const MessageValidTimeout = 10 * 1000; const MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = 'sha1'; const EncryptionAlgorithm = "sha1";
export class NativeMessagingBackground { export class NativeMessagingBackground {
private connected = false; private connected = false;
private connecting: boolean; private connecting: boolean;
private port: browser.runtime.Port | chrome.runtime.Port; private port: browser.runtime.Port | chrome.runtime.Port;
private resolver: any = null; private resolver: any = null;
private privateKey: ArrayBuffer = null; private privateKey: ArrayBuffer = null;
private publicKey: ArrayBuffer = null; private publicKey: ArrayBuffer = null;
private secureSetupResolve: any = null; private secureSetupResolve: any = null;
private sharedSecret: SymmetricCryptoKey; private sharedSecret: SymmetricCryptoKey;
private appId: string; private appId: string;
private validatingFingerprint: boolean; private validatingFingerprint: boolean;
constructor(private storageService: StorageService, private cryptoService: CryptoService, constructor(
private cryptoFunctionService: CryptoFunctionService, private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private userService: UserService, private cryptoService: CryptoService,
private messagingService: MessagingService, private appIdService: AppIdService, private cryptoFunctionService: CryptoFunctionService,
private platformUtilsService: PlatformUtilsService) { private vaultTimeoutService: VaultTimeoutService,
this.storageService.save(ConstantsService.biometricFingerprintValidated, false); private runtimeBackground: RuntimeBackground,
private i18nService: I18nService,
private userService: UserService,
private messagingService: MessagingService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService
) {
this.storageService.save(ConstantsService.biometricFingerprintValidated, false);
if (chrome?.permissions?.onAdded) { if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging // Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener(permissions => { chrome.permissions.onAdded.addListener((permissions) => {
BrowserApi.reloadExtension(null); BrowserApi.reloadExtension(null);
}); });
}
}
async connect() {
this.appId = await this.appIdService.getAppId();
this.storageService.save(ConstantsService.biometricFingerprintValidated, false);
return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative("com.8bit.bitwarden");
this.connecting = true;
const connectedCallback = () => {
this.connected = true;
this.connecting = false;
resolve();
};
// Safari has a bundled native component which is always available, no need to
// check if the desktop app is running.
if (this.platformUtilsService.isSafari()) {
connectedCallback();
}
this.port.onMessage.addListener(async (message: any) => {
switch (message.command) {
case "connected":
connectedCallback();
break;
case "disconnected":
if (this.connecting) {
this.messagingService.send("showDialog", {
text: this.i18nService.t("startDesktopDesc"),
title: this.i18nService.t("startDesktopTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
reject();
} }
} this.connected = false;
this.port.disconnect();
async connect() { break;
this.appId = await this.appIdService.getAppId(); case "setupEncryption":
this.storageService.save(ConstantsService.biometricFingerprintValidated, false); // Ignore since it belongs to another device
if (message.appId !== this.appId) {
return new Promise<void>((resolve, reject) => { return;
this.port = BrowserApi.connectNative('com.8bit.bitwarden');
this.connecting = true;
const connectedCallback = () => {
this.connected = true;
this.connecting = false;
resolve();
};
// Safari has a bundled native component which is always available, no need to
// check if the desktop app is running.
if (this.platformUtilsService.isSafari()) {
connectedCallback();
} }
this.port.onMessage.addListener(async (message: any) => { const encrypted = Utils.fromB64ToArray(message.sharedSecret);
switch (message.command) { const decrypted = await this.cryptoFunctionService.rsaDecrypt(
case 'connected': encrypted.buffer,
connectedCallback(); this.privateKey,
break; EncryptionAlgorithm
case 'disconnected': );
if (this.connecting) {
this.messagingService.send('showDialog', {
text: this.i18nService.t('startDesktopDesc'),
title: this.i18nService.t('startDesktopTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
reject();
}
this.connected = false;
this.port.disconnect();
break;
case 'setupEncryption':
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
return;
}
const encrypted = Utils.fromB64ToArray(message.sharedSecret); if (this.validatingFingerprint) {
const decrypted = await this.cryptoFunctionService.rsaDecrypt(encrypted.buffer, this.privateKey, EncryptionAlgorithm); this.validatingFingerprint = false;
this.storageService.save(ConstantsService.biometricFingerprintValidated, true);
if (this.validatingFingerprint) { }
this.validatingFingerprint = false; this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.storageService.save(ConstantsService.biometricFingerprintValidated, true); this.secureSetupResolve();
} break;
this.sharedSecret = new SymmetricCryptoKey(decrypted); case "invalidateEncryption":
this.secureSetupResolve(); // Ignore since it belongs to another device
break; if (message.appId !== this.appId) {
case 'invalidateEncryption': return;
// Ignore since it belongs to another device }
if (message.appId !== this.appId) {
return;
}
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.messagingService.send('showDialog', {
text: this.i18nService.t('nativeMessagingInvalidEncryptionDesc'),
title: this.i18nService.t('nativeMessagingInvalidEncryptionTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
break;
case 'verifyFingerprint': {
if (this.sharedSecret == null) {
this.validatingFingerprint = true;
this.showFingerprintDialog();
}
break;
}
case 'wrongUserId':
this.showWrongUserDialog();
default:
// Ignore since it belongs to another device
if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) {
return;
}
this.onMessage(message.message);
}
});
this.port.onDisconnect.addListener((p: any) => {
let error;
if (BrowserApi.isWebExtensionsApi) {
error = p.error.message;
} else {
error = chrome.runtime.lastError.message;
}
if (error != null) {
this.messagingService.send('showDialog', {
text: this.i18nService.t('desktopIntegrationDisabledDesc'),
title: this.i18nService.t('desktopIntegrationDisabledTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
}
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
reject();
});
});
}
showWrongUserDialog() {
this.messagingService.send('showDialog', {
text: this.i18nService.t('nativeMessagingWrongUserDesc'),
title: this.i18nService.t('nativeMessagingWrongUserTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
}
async send(message: any) {
if (!this.connected) {
await this.connect();
}
if (this.platformUtilsService.isSafari()) {
this.postMessage(message);
} else {
this.postMessage({appId: this.appId, message: await this.encryptMessage(message)});
}
}
async encryptMessage(message: any) {
if (this.sharedSecret == null) {
await this.secureCommunication();
}
message.timestamp = Date.now();
return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret);
}
getResponse(): Promise<any> {
return new Promise((resolve, reject) => {
this.resolver = resolve;
});
}
private postMessage(message: any) {
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
try {
this.port.postMessage(message);
} catch (e) {
// tslint:disable-next-line
console.error("NativeMessaging port disconnected, disconnecting.");
this.sharedSecret = null; this.sharedSecret = null;
this.privateKey = null; this.privateKey = null;
this.connected = false; this.connected = false;
this.messagingService.send('showDialog', { this.messagingService.send("showDialog", {
text: this.i18nService.t('nativeMessagingInvalidEncryptionDesc'), text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
title: this.i18nService.t('nativeMessagingInvalidEncryptionTitle'), title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
confirmText: this.i18nService.t('ok'), confirmText: this.i18nService.t("ok"),
type: 'error', type: "error",
}); });
break;
case "verifyFingerprint": {
if (this.sharedSecret == null) {
this.validatingFingerprint = true;
this.showFingerprintDialog();
}
break;
}
case "wrongUserId":
this.showWrongUserDialog();
default:
// Ignore since it belongs to another device
if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) {
return;
}
this.onMessage(message.message);
} }
});
this.port.onDisconnect.addListener((p: any) => {
let error;
if (BrowserApi.isWebExtensionsApi) {
error = p.error.message;
} else {
error = chrome.runtime.lastError.message;
}
if (error != null) {
this.messagingService.send("showDialog", {
text: this.i18nService.t("desktopIntegrationDisabledDesc"),
title: this.i18nService.t("desktopIntegrationDisabledTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
}
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
reject();
});
});
}
showWrongUserDialog() {
this.messagingService.send("showDialog", {
text: this.i18nService.t("nativeMessagingWrongUserDesc"),
title: this.i18nService.t("nativeMessagingWrongUserTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
}
async send(message: any) {
if (!this.connected) {
await this.connect();
} }
private async onMessage(rawMessage: any) { if (this.platformUtilsService.isSafari()) {
let message = rawMessage; this.postMessage(message);
if (!this.platformUtilsService.isSafari()) { } else {
message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) });
}
}
async encryptMessage(message: any) {
if (this.sharedSecret == null) {
await this.secureCommunication();
}
message.timestamp = Date.now();
return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret);
}
getResponse(): Promise<any> {
return new Promise((resolve, reject) => {
this.resolver = resolve;
});
}
private postMessage(message: any) {
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
try {
this.port.postMessage(message);
} catch (e) {
// tslint:disable-next-line
console.error("NativeMessaging port disconnected, disconnecting.");
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.messagingService.send("showDialog", {
text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
}
}
private async onMessage(rawMessage: any) {
let message = rawMessage;
if (!this.platformUtilsService.isSafari()) {
message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret));
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
// tslint:disable-next-line
console.error("NativeMessage is to old, ignoring.");
return;
}
switch (message.command) {
case "biometricUnlock":
await this.storageService.remove(ConstantsService.biometricAwaitingAcceptance);
if (message.response === "not enabled") {
this.messagingService.send("showDialog", {
text: this.i18nService.t("biometricsNotEnabledDesc"),
title: this.i18nService.t("biometricsNotEnabledTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
break;
} else if (message.response === "not supported") {
this.messagingService.send("showDialog", {
text: this.i18nService.t("biometricsNotSupportedDesc"),
title: this.i18nService.t("biometricsNotSupportedTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
break;
} }
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { const enabled = await this.storageService.get(ConstantsService.biometricUnlockKey);
if (enabled === null || enabled === false) {
if (message.response === "unlocked") {
await this.storageService.save(ConstantsService.biometricUnlockKey, true);
}
break;
}
// Ignore unlock if already unlockeded
if (!this.vaultTimeoutService.biometricLocked) {
break;
}
if (message.response === "unlocked") {
await this.cryptoService.setKey(
new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)
);
// Verify key is correct by attempting to decrypt a secret
try {
await this.cryptoService.getFingerprint(await this.userService.getUserId());
} catch (e) {
// tslint:disable-next-line // tslint:disable-next-line
console.error('NativeMessage is to old, ignoring.'); console.error("Unable to verify key:", e);
return; await this.cryptoService.clearKey();
} this.showWrongUserDialog();
switch (message.command) { message = false;
case 'biometricUnlock': break;
await this.storageService.remove(ConstantsService.biometricAwaitingAcceptance); }
if (message.response === 'not enabled') { this.vaultTimeoutService.biometricLocked = false;
this.messagingService.send('showDialog', { this.runtimeBackground.processMessage({ command: "unlocked" }, null, null);
text: this.i18nService.t('biometricsNotEnabledDesc'),
title: this.i18nService.t('biometricsNotEnabledTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
break;
} else if (message.response === 'not supported') {
this.messagingService.send('showDialog', {
text: this.i18nService.t('biometricsNotSupportedDesc'),
title: this.i18nService.t('biometricsNotSupportedTitle'),
confirmText: this.i18nService.t('ok'),
type: 'error',
});
break;
}
const enabled = await this.storageService.get(ConstantsService.biometricUnlockKey);
if (enabled === null || enabled === false) {
if (message.response === 'unlocked') {
await this.storageService.save(ConstantsService.biometricUnlockKey, true);
}
break;
}
// Ignore unlock if already unlockeded
if (!this.vaultTimeoutService.biometricLocked) {
break;
}
if (message.response === 'unlocked') {
await this.cryptoService.setKey(new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer));
// Verify key is correct by attempting to decrypt a secret
try {
await this.cryptoService.getFingerprint(await this.userService.getUserId());
} catch (e) {
// tslint:disable-next-line
console.error('Unable to verify key:', e);
await this.cryptoService.clearKey();
this.showWrongUserDialog();
message = false;
break;
}
this.vaultTimeoutService.biometricLocked = false;
this.runtimeBackground.processMessage({command: 'unlocked'}, null, null);
}
break;
default:
// tslint:disable-next-line
console.error('NativeMessage, got unknown command: ', message.command);
}
if (this.resolver) {
this.resolver(message);
} }
break;
default:
// tslint:disable-next-line
console.error("NativeMessage, got unknown command: ", message.command);
} }
private async secureCommunication() { if (this.resolver) {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); this.resolver(message);
this.publicKey = publicKey; }
this.privateKey = privateKey; }
this.sendUnencrypted({ private async secureCommunication() {
command: 'setupEncryption', const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
publicKey: Utils.fromBufferToB64(publicKey), this.publicKey = publicKey;
userId: await this.userService.getUserId(), this.privateKey = privateKey;
});
return new Promise((resolve, reject) => this.secureSetupResolve = resolve); this.sendUnencrypted({
command: "setupEncryption",
publicKey: Utils.fromBufferToB64(publicKey),
userId: await this.userService.getUserId(),
});
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
}
private async sendUnencrypted(message: any) {
if (!this.connected) {
await this.connect();
} }
private async sendUnencrypted(message: any) { message.timestamp = Date.now();
if (!this.connected) {
await this.connect();
}
message.timestamp = Date.now(); this.postMessage({ appId: this.appId, message: message });
}
this.postMessage({appId: this.appId, message: message}); private async showFingerprintDialog() {
} const fingerprint = (
await this.cryptoService.getFingerprint(await this.userService.getUserId(), this.publicKey)
).join(" ");
private async showFingerprintDialog() { this.messagingService.send("showDialog", {
const fingerprint = (await this.cryptoService.getFingerprint(await this.userService.getUserId(), this.publicKey)).join(' '); html: `${this.i18nService.t(
"desktopIntegrationVerificationText"
this.messagingService.send('showDialog', { )}<br><br><strong>${fingerprint}</strong>`,
html: `${this.i18nService.t('desktopIntegrationVerificationText')}<br><br><strong>${fingerprint}</strong>`, title: this.i18nService.t("desktopSyncVerificationTitle"),
title: this.i18nService.t('desktopSyncVerificationTitle'), confirmText: this.i18nService.t("ok"),
confirmText: this.i18nService.t('ok'), type: "warning",
type: 'warning', });
}); }
}
} }

View File

@ -1,417 +1,460 @@
import { CipherType } from 'jslib-common/enums/cipherType'; import { CipherType } from "jslib-common/enums/cipherType";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { LoginUriView } from 'jslib-common/models/view/loginUriView'; import { LoginUriView } from "jslib-common/models/view/loginUriView";
import { LoginView } from 'jslib-common/models/view/loginView'; import { LoginView } from "jslib-common/models/view/loginView";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { AutofillService } from '../services/abstractions/autofill.service'; import { AutofillService } from "../services/abstractions/autofill.service";
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import MainBackground from './main.background'; import MainBackground from "./main.background";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { PolicyType } from 'jslib-common/enums/policyType'; import { PolicyType } from "jslib-common/enums/policyType";
import AddChangePasswordQueueMessage from './models/addChangePasswordQueueMessage'; import AddChangePasswordQueueMessage from "./models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from './models/addLoginQueueMessage'; import AddLoginQueueMessage from "./models/addLoginQueueMessage";
import AddLoginRuntimeMessage from './models/addLoginRuntimeMessage'; import AddLoginRuntimeMessage from "./models/addLoginRuntimeMessage";
import ChangePasswordRuntimeMessage from './models/changePasswordRuntimeMessage'; import ChangePasswordRuntimeMessage from "./models/changePasswordRuntimeMessage";
import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
import { NotificationQueueMessageType } from './models/notificationQueueMessageType'; import { NotificationQueueMessageType } from "./models/notificationQueueMessageType";
export default class NotificationBackground { export default class NotificationBackground {
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = []; constructor(
private main: MainBackground,
private autofillService: AutofillService,
private cipherService: CipherService,
private storageService: StorageService,
private vaultTimeoutService: VaultTimeoutService,
private policyService: PolicyService,
private folderService: FolderService,
private userService: UserService
) {}
constructor(private main: MainBackground, private autofillService: AutofillService, async init() {
private cipherService: CipherService, private storageService: StorageService, if (chrome.runtime == null) {
private vaultTimeoutService: VaultTimeoutService, private policyService: PolicyService, return;
private folderService: FolderService, private userService: UserService) {
} }
async init() { BrowserApi.messageListener(
if (chrome.runtime == null) { "notification.background",
return; async (msg: any, sender: chrome.runtime.MessageSender) => {
await this.processMessage(msg, sender);
}
);
this.cleanupNotificationQueue();
}
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
switch (msg.command) {
case "unlockCompleted":
if (msg.data.target !== "notification.background") {
return;
} }
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
BrowserApi.messageListener('notification.background', async (msg: any, sender: chrome.runtime.MessageSender) => { break;
await this.processMessage(msg, sender); case "bgGetDataForTab":
}); await this.getDataForTab(sender.tab, msg.responseCommand);
break;
this.cleanupNotificationQueue(); case "bgCloseNotificationBar":
} await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
break;
async processMessage(msg: any, sender: chrome.runtime.MessageSender) { case "bgAdjustNotificationBar":
switch (msg.command) { await BrowserApi.tabSendMessageData(sender.tab, "adjustNotificationBar", msg.data);
case 'unlockCompleted': break;
if (msg.data.target !== 'notification.background') { case "bgAddLogin":
return; await this.addLogin(msg.login, sender.tab);
} break;
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender); case "bgChangedPassword":
break; await this.changedPassword(msg.data, sender.tab);
case 'bgGetDataForTab': break;
await this.getDataForTab(sender.tab, msg.responseCommand); case "bgAddClose":
break; case "bgChangeClose":
case 'bgCloseNotificationBar': this.removeTabFromNotificationQueue(sender.tab);
await BrowserApi.tabSendMessageData(sender.tab, 'closeNotificationBar'); break;
break; case "bgAddSave":
case 'bgAdjustNotificationBar': case "bgChangeSave":
await BrowserApi.tabSendMessageData(sender.tab, 'adjustNotificationBar', msg.data); if (await this.vaultTimeoutService.isLocked()) {
break; const retryMessage: LockedVaultPendingNotificationsItem = {
case 'bgAddLogin': commandToRetry: {
await this.addLogin(msg.login, sender.tab); msg: msg,
break; sender: sender,
case 'bgChangedPassword': },
await this.changedPassword(msg.data, sender.tab); target: "notification.background",
break; };
case 'bgAddClose': await BrowserApi.tabSendMessageData(
case 'bgChangeClose': sender.tab,
this.removeTabFromNotificationQueue(sender.tab); "addToLockedVaultPendingNotifications",
break; retryMessage
case 'bgAddSave': );
case 'bgChangeSave': await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
if (await this.vaultTimeoutService.isLocked()) { return;
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: msg,
sender: sender,
},
target: 'notification.background',
};
await BrowserApi.tabSendMessageData(sender.tab, 'addToLockedVaultPendingNotifications', retryMessage);
await BrowserApi.tabSendMessageData(sender.tab, 'promptForLogin');
return;
}
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
break;
case 'bgNeverSave':
await this.saveNever(sender.tab);
break;
case 'collectPageDetailsResponse':
switch (msg.sender) {
case 'notificationBar':
const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
await BrowserApi.tabSendMessageData(msg.tab, 'notificationBarPageDetails', {
details: msg.details,
forms: forms,
});
break;
default:
break;
}
break;
default:
break;
} }
} await this.saveOrUpdateCredentials(sender.tab, msg.folder);
break;
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> { case "bgNeverSave":
if (this.notificationQueue.length === 0) { await this.saveNever(sender.tab);
return; break;
} case "collectPageDetailsResponse":
switch (msg.sender) {
if (tab != null) { case "notificationBar":
this.doNotificationQueueCheck(tab); const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
return; await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", {
} details: msg.details,
forms: forms,
const currentTab = await BrowserApi.getTabFromCurrentWindow(); });
if (currentTab != null) { break;
this.doNotificationQueueCheck(currentTab); default:
}
}
private cleanupNotificationQueue() {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (this.notificationQueue[i].expires < new Date()) {
this.notificationQueue.splice(i, 1);
}
}
setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes
}
private doNotificationQueueCheck(tab: chrome.tabs.Tab): void {
if (tab == null) {
return;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain == null) {
return;
}
for (let i = 0; i < this.notificationQueue.length; i++) {
if (this.notificationQueue[i].tabId !== tab.id || this.notificationQueue[i].domain !== tabDomain) {
continue;
}
if (this.notificationQueue[i].type === NotificationQueueMessageType.addLogin) {
BrowserApi.tabSendMessageData(tab, 'openNotificationBar', {
type: 'add',
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.changePassword) {
BrowserApi.tabSendMessageData(tab, 'openNotificationBar', {
type: 'change',
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
}
break; break;
} }
break;
default:
break;
}
}
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> {
if (this.notificationQueue.length === 0) {
return;
} }
private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) { if (tab != null) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { this.doNotificationQueueCheck(tab);
if (this.notificationQueue[i].tabId === tab.id) { return;
this.notificationQueue.splice(i, 1);
}
}
} }
private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) { const currentTab = await BrowserApi.getTabFromCurrentWindow();
if (!await this.userService.isAuthenticated()) { if (currentTab != null) {
return; this.doNotificationQueueCheck(currentTab);
} }
}
const loginDomain = Utils.getDomain(loginInfo.url); private cleanupNotificationQueue() {
if (loginDomain == null) { for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
return; if (this.notificationQueue[i].expires < new Date()) {
} this.notificationQueue.splice(i, 1);
}
}
setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes
}
let normalizedUsername = loginInfo.username; private doNotificationQueueCheck(tab: chrome.tabs.Tab): void {
if (normalizedUsername != null) { if (tab == null) {
normalizedUsername = normalizedUsername.toLowerCase(); return;
}
const disabledAddLogin = await this.storageService.get<boolean>(ConstantsService.disableAddLoginNotificationKey);
if (await this.vaultTimeoutService.isLocked()) {
if (disabledAddLogin) {
return;
}
if (!await this.allowPersonalOwnership()) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
const usernameMatches = ciphers.filter(c =>
c.login.username != null && c.login.username.toLowerCase() === normalizedUsername);
if (usernameMatches.length === 0) {
if (disabledAddLogin) {
return;
}
if (!await this.allowPersonalOwnership()) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
} else if (usernameMatches.length === 1 && usernameMatches[0].login.password !== loginInfo.password) {
const disabledChangePassword = await this.storageService.get<boolean>(
ConstantsService.disableChangedPasswordNotificationKey);
if (disabledChangePassword) {
return;
}
this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab);
}
} }
private async pushAddLoginToQueue(loginDomain: string, loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab, isVaultLocked: boolean = false) { const tabDomain = Utils.getDomain(tab.url);
// remove any old messages for this tab if (tabDomain == null) {
this.removeTabFromNotificationQueue(tab); return;
const message: AddLoginQueueMessage = {
type: NotificationQueueMessageType.addLogin,
username: loginInfo.username,
password: loginInfo.password,
domain: loginDomain,
uri: loginInfo.url,
tabId: tab.id,
expires: new Date((new Date()).getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
} }
private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) { for (let i = 0; i < this.notificationQueue.length; i++) {
const loginDomain = Utils.getDomain(changeData.url); if (
if (loginDomain == null) { this.notificationQueue[i].tabId !== tab.id ||
return; this.notificationQueue[i].domain !== tabDomain
} ) {
continue;
}
if (await this.vaultTimeoutService.isLocked()) { if (this.notificationQueue[i].type === NotificationQueueMessageType.addLogin) {
this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true); BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
return; type: "add",
} typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.changePassword) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
type: "change",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
}
break;
}
}
let id: string = null; private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) {
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url); for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (changeData.currentPassword != null) { if (this.notificationQueue[i].tabId === tab.id) {
const passwordMatches = ciphers.filter(c => c.login.password === changeData.currentPassword); this.notificationQueue.splice(i, 1);
if (passwordMatches.length === 1) { }
id = passwordMatches[0].id; }
} }
} else if (ciphers.length === 1) {
id = ciphers[0].id; private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) {
} if (!(await this.userService.isAuthenticated())) {
if (id != null) { return;
this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
}
} }
private async pushChangePasswordToQueue(cipherId: string, loginDomain: string, newPassword: string, tab: chrome.tabs.Tab, isVaultLocked: boolean = false) { const loginDomain = Utils.getDomain(loginInfo.url);
// remove any old messages for this tab if (loginDomain == null) {
this.removeTabFromNotificationQueue(tab); return;
const message: AddChangePasswordQueueMessage = {
type: NotificationQueueMessageType.changePassword,
cipherId: cipherId,
newPassword: newPassword,
domain: loginDomain,
tabId: tab.id,
expires: new Date((new Date()).getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
} }
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) { let normalizedUsername = loginInfo.username;
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { if (normalizedUsername != null) {
const queueMessage = this.notificationQueue[i]; normalizedUsername = normalizedUsername.toLowerCase();
if (queueMessage.tabId !== tab.id || }
(queueMessage.type !== NotificationQueueMessageType.addLogin && queueMessage.type !== NotificationQueueMessageType.changePassword)) {
continue;
}
const tabDomain = Utils.getDomain(tab.url); const disabledAddLogin = await this.storageService.get<boolean>(
if (tabDomain != null && tabDomain !== queueMessage.domain) { ConstantsService.disableAddLoginNotificationKey
continue; );
} if (await this.vaultTimeoutService.isLocked()) {
if (disabledAddLogin) {
return;
}
this.notificationQueue.splice(i, 1); if (!(await this.allowPersonalOwnership())) {
BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); return;
}
if (queueMessage.type === NotificationQueueMessageType.changePassword) { this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
const message = (queueMessage as AddChangePasswordQueueMessage); return;
const cipher = await this.getDecryptedCipherById(message.cipherId); }
if (cipher == null) {
return;
}
await this.updateCipher(cipher, message.newPassword);
return;
}
if (!queueMessage.wasVaultLocked) { const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId); const usernameMatches = ciphers.filter(
} (c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername
);
if (usernameMatches.length === 0) {
if (disabledAddLogin) {
return;
}
// If the vault was locked, check if a cipher needs updating instead of creating a new one if (!(await this.allowPersonalOwnership())) {
if (queueMessage.type === NotificationQueueMessageType.addLogin && queueMessage.wasVaultLocked === true) { return;
const message = (queueMessage as AddLoginQueueMessage); }
const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri);
const usernameMatches = ciphers.filter(c => c.login.username != null &&
c.login.username.toLowerCase() === message.username);
if (usernameMatches.length >= 1) { this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
await this.updateCipher(usernameMatches[0], message.password); } else if (
return; usernameMatches.length === 1 &&
} usernameMatches[0].login.password !== loginInfo.password
) {
const disabledChangePassword = await this.storageService.get<boolean>(
ConstantsService.disableChangedPasswordNotificationKey
);
if (disabledChangePassword) {
return;
}
this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab);
}
}
await this.createNewCipher(message, folderId); private async pushAddLoginToQueue(
} loginDomain: string,
loginInfo: AddLoginRuntimeMessage,
tab: chrome.tabs.Tab,
isVaultLocked: boolean = false
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddLoginQueueMessage = {
type: NotificationQueueMessageType.addLogin,
username: loginInfo.username,
password: loginInfo.password,
domain: loginDomain,
uri: loginInfo.url,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
}
private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) {
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
return;
}
if (await this.vaultTimeoutService.isLocked()) {
this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
return;
}
let id: string = null;
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url);
if (changeData.currentPassword != null) {
const passwordMatches = ciphers.filter(
(c) => c.login.password === changeData.currentPassword
);
if (passwordMatches.length === 1) {
id = passwordMatches[0].id;
}
} else if (ciphers.length === 1) {
id = ciphers[0].id;
}
if (id != null) {
this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
}
}
private async pushChangePasswordToQueue(
cipherId: string,
loginDomain: string,
newPassword: string,
tab: chrome.tabs.Tab,
isVaultLocked: boolean = false
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddChangePasswordQueueMessage = {
type: NotificationQueueMessageType.changePassword,
cipherId: cipherId,
newPassword: newPassword,
domain: loginDomain,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
}
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
queueMessage.tabId !== tab.id ||
(queueMessage.type !== NotificationQueueMessageType.addLogin &&
queueMessage.type !== NotificationQueueMessageType.changePassword)
) {
continue;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
if (queueMessage.type === NotificationQueueMessageType.changePassword) {
const message = queueMessage as AddChangePasswordQueueMessage;
const cipher = await this.getDecryptedCipherById(message.cipherId);
if (cipher == null) {
return;
} }
} await this.updateCipher(cipher, message.newPassword);
return;
}
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) { if (!queueMessage.wasVaultLocked) {
const loginModel = new LoginView(); await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
const loginUri = new LoginUriView(); }
loginUri.uri = queueMessage.uri;
loginModel.uris = [loginUri];
loginModel.username = queueMessage.username;
loginModel.password = queueMessage.password;
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 the vault was locked, check if a cipher needs updating instead of creating a new one
const folders = await this.folderService.getAllDecrypted(); if (
if (folders.some(x => x.id === folderId)) { queueMessage.type === NotificationQueueMessageType.addLogin &&
model.folderId = folderId; queueMessage.wasVaultLocked === true
} ) {
const message = queueMessage as AddLoginQueueMessage;
const ciphers = await this.cipherService.getAllDecryptedForUrl(message.uri);
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === message.username
);
if (usernameMatches.length >= 1) {
await this.updateCipher(usernameMatches[0], message.password);
return;
} }
const cipher = await this.cipherService.encrypt(model); await this.createNewCipher(message, folderId);
await this.cipherService.saveWithServer(cipher); }
}
}
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
const loginModel = new LoginView();
const loginUri = new LoginUriView();
loginUri.uri = queueMessage.uri;
loginModel.uris = [loginUri];
loginModel.username = queueMessage.username;
loginModel.password = queueMessage.password;
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)) {
const folders = await this.folderService.getAllDecrypted();
if (folders.some((x) => x.id === folderId)) {
model.folderId = folderId;
}
} }
private async getDecryptedCipherById(cipherId: string) { const cipher = await this.cipherService.encrypt(model);
const cipher = await this.cipherService.get(cipherId); await this.cipherService.saveWithServer(cipher);
if (cipher != null && cipher.type === CipherType.Login) { }
return await cipher.decrypt();
} private async getDecryptedCipherById(cipherId: string) {
return null; const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt();
}
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.saveWithServer(newCipher);
}
}
private async saveNever(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
queueMessage.tabId !== tab.id ||
queueMessage.type !== NotificationQueueMessageType.addLogin
) {
continue;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
const hostname = Utils.getHostname(tab.url);
await this.cipherService.saveNeverDomain(hostname);
}
}
private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) {
const responseData: any = {};
if (responseCommand === "notificationBarGetFoldersList") {
responseData.folders = await this.folderService.getAllDecrypted();
} }
private async updateCipher(cipher: CipherView, newPassword: string) { await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
if (cipher != null && cipher.type === CipherType.Login) { }
cipher.login.password = newPassword;
const newCipher = await this.cipherService.encrypt(cipher);
await this.cipherService.saveWithServer(newCipher);
}
}
private async saveNever(tab: chrome.tabs.Tab) { private async allowPersonalOwnership(): Promise<boolean> {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
const queueMessage = this.notificationQueue[i]; }
if (queueMessage.tabId !== tab.id || queueMessage.type !== NotificationQueueMessageType.addLogin) {
continue;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, 'closeNotificationBar');
const hostname = Utils.getHostname(tab.url);
await this.cipherService.saveNeverDomain(hostname);
}
}
private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) {
const responseData: any = {};
if (responseCommand === 'notificationBarGetFoldersList') {
responseData.folders = await this.folderService.getAllDecrypted();
}
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
}
private async allowPersonalOwnership(): Promise<boolean> {
return !await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
}
} }

View File

@ -1,219 +1,248 @@
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SystemService } from 'jslib-common/abstractions/system.service'; import { SystemService } from "jslib-common/abstractions/system.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { AutofillService } from '../services/abstractions/autofill.service'; import { AutofillService } from "../services/abstractions/autofill.service";
import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import MainBackground from './main.background'; import MainBackground from "./main.background";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import LockedVaultPendingNotificationsItem from './models/lockedVaultPendingNotificationsItem'; import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class RuntimeBackground { export default class RuntimeBackground {
private autofillTimeout: any; private autofillTimeout: any;
private pageDetailsToAutoFill: any[] = []; private pageDetailsToAutoFill: any[] = [];
private onInstalledReason: string = null; private onInstalledReason: string = null;
private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = []; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = [];
constructor(private main: MainBackground, private autofillService: AutofillService, constructor(
private platformUtilsService: BrowserPlatformUtilsService, private main: MainBackground,
private storageService: StorageService, private i18nService: I18nService, private autofillService: AutofillService,
private notificationsService: NotificationsService, private systemService: SystemService, private platformUtilsService: BrowserPlatformUtilsService,
private environmentService: EnvironmentService, private messagingService: MessagingService, private storageService: StorageService,
private logService: LogService) { private i18nService: I18nService,
private notificationsService: NotificationsService,
private systemService: SystemService,
private environmentService: EnvironmentService,
private messagingService: MessagingService,
private logService: LogService
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
this.onInstalledReason = details.reason;
});
}
// onInstalled listener must be wired up before anything else, so we do it in the ctor async init() {
chrome.runtime.onInstalled.addListener((details: any) => { if (!chrome.runtime) {
this.onInstalledReason = details.reason; return;
});
} }
async init() { await this.checkOnInstalled();
if (!chrome.runtime) { BrowserApi.messageListener(
return; "runtime.background",
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
await this.processMessage(msg, sender, sendResponse);
}
);
}
async processMessage(msg: any, sender: any, sendResponse: any) {
switch (msg.command) {
case "loggedIn":
case "unlocked":
let item: LockedVaultPendingNotificationsItem;
if (this.lockedVaultPendingNotifications.length > 0) {
await BrowserApi.closeLoginTab();
item = this.lockedVaultPendingNotifications.pop();
if (item.commandToRetry.sender?.tab?.id) {
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
}
} }
await this.checkOnInstalled(); await this.main.setIcon();
BrowserApi.messageListener('runtime.background', async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { await this.main.refreshBadgeAndMenu(false);
await this.processMessage(msg, sender, sendResponse); this.notificationsService.updateConnection(msg.command === "unlocked");
}); this.systemService.cancelProcessReload();
}
async processMessage(msg: any, sender: any, sendResponse: any) { if (item) {
switch (msg.command) { await BrowserApi.tabSendMessageData(
case 'loggedIn': item.commandToRetry.sender.tab,
case 'unlocked': "unlockCompleted",
let item: LockedVaultPendingNotificationsItem; item
);
if (this.lockedVaultPendingNotifications.length > 0) {
await BrowserApi.closeLoginTab();
item = this.lockedVaultPendingNotifications.pop();
if (item.commandToRetry.sender?.tab?.id) {
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
}
}
await this.main.setIcon();
await this.main.refreshBadgeAndMenu(false);
this.notificationsService.updateConnection(msg.command === 'unlocked');
this.systemService.cancelProcessReload();
if (item) {
await BrowserApi.tabSendMessageData(item.commandToRetry.sender.tab, 'unlockCompleted', item);
}
break;
case 'addToLockedVaultPendingNotifications':
this.lockedVaultPendingNotifications.push(msg.data);
break;
case 'logout':
await this.main.logout(msg.expired);
break;
case 'syncCompleted':
if (msg.successfully) {
setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000);
}
break;
case 'openPopup':
await this.main.openPopup();
break;
case 'promptForLogin':
await BrowserApi.createNewTab('popup/index.html?uilocation=popout', true, true);
break;
case 'showDialogResolve':
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
break;
case 'bgCollectPageDetails':
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
break;
case 'bgUpdateContextMenu':
case 'editedCipher':
case 'addedCipher':
case 'deletedCipher':
await this.main.refreshBadgeAndMenu();
break;
case 'bgReseedStorage':
await this.main.reseedStorage();
break;
case 'collectPageDetailsResponse':
switch (msg.sender) {
case 'autofiller':
case 'autofill_cmd':
const totpCode = await this.autofillService.doAutoFillActiveTab([{
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
}], msg.sender === 'autofill_cmd');
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
}
break;
case 'contextMenu':
clearTimeout(this.autofillTimeout);
this.pageDetailsToAutoFill.push({
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
});
this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300);
break;
default:
break;
}
break;
case 'authResult':
const vaultUrl = this.environmentService.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
return;
}
try {
BrowserApi.createNewTab('popup/index.html?uilocation=popout#/sso?code=' +
encodeURIComponent(msg.code) + '&state=' + encodeURIComponent(msg.state));
}
catch {
this.logService.error('Unable to open sso popout tab');
}
break;
case 'webAuthnResult':
const vaultUrl2 = this.environmentService.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl2) !== msg.referrer) {
return;
}
const params = `webAuthnResponse=${encodeURIComponent(msg.data)};` +
`remember=${encodeURIComponent(msg.remember)}`;
BrowserApi.createNewTab(`popup/index.html?uilocation=popout#/2fa;${params}`, undefined, false);
break;
case 'reloadPopup':
this.messagingService.send('reloadPopup');
break;
case 'emailVerificationRequired':
this.messagingService.send('showDialog', {
dialogId: 'emailVerificationRequired',
title: this.i18nService.t('emailVerificationRequired'),
text: this.i18nService.t('emailVerificationRequiredDesc'),
confirmText: this.i18nService.t('ok'),
type: 'info',
});
break;
case 'getClickedElementResponse':
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
default:
break;
} }
} break;
case "addToLockedVaultPendingNotifications":
private async autofillPage() { this.lockedVaultPendingNotifications.push(msg.data);
const totpCode = await this.autofillService.doAutoFill({ break;
cipher: this.main.loginToAutoFill, case "logout":
pageDetails: this.pageDetailsToAutoFill, await this.main.logout(msg.expired);
fillNewPassword: true, break;
}); case "syncCompleted":
if (msg.successfully) {
if (totpCode != null) { setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000);
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
} }
break;
// reset case "openPopup":
this.main.loginToAutoFill = null; await this.main.openPopup();
this.pageDetailsToAutoFill = []; break;
} case "promptForLogin":
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
private async checkOnInstalled() { break;
setTimeout(async () => { case "showDialogResolve":
if (this.onInstalledReason != null) { this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
if (this.onInstalledReason === 'install') { break;
BrowserApi.createNewTab('https://bitwarden.com/browser-start/'); case "bgCollectPageDetails":
await this.setDefaultSettings(); await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
} break;
case "bgUpdateContextMenu":
this.onInstalledReason = null; case "editedCipher":
case "addedCipher":
case "deletedCipher":
await this.main.refreshBadgeAndMenu();
break;
case "bgReseedStorage":
await this.main.reseedStorage();
break;
case "collectPageDetailsResponse":
switch (msg.sender) {
case "autofiller":
case "autofill_cmd":
const totpCode = await this.autofillService.doAutoFillActiveTab(
[
{
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
},
],
msg.sender === "autofill_cmd"
);
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
} }
}, 100); break;
} case "contextMenu":
clearTimeout(this.autofillTimeout);
this.pageDetailsToAutoFill.push({
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
});
this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300);
break;
default:
break;
}
break;
case "authResult":
const vaultUrl = this.environmentService.getWebVaultUrl();
private async setDefaultSettings() { if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
// Default timeout option to "on restart". return;
const currentVaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
if (currentVaultTimeout == null) {
await this.storageService.save(ConstantsService.vaultTimeoutKey, -1);
} }
// Default action to "lock". try {
const currentVaultTimeoutAction = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey); BrowserApi.createNewTab(
if (currentVaultTimeoutAction == null) { "popup/index.html?uilocation=popout#/sso?code=" +
await this.storageService.save(ConstantsService.vaultTimeoutActionKey, 'lock'); encodeURIComponent(msg.code) +
"&state=" +
encodeURIComponent(msg.state)
);
} catch {
this.logService.error("Unable to open sso popout tab");
} }
break;
case "webAuthnResult":
const vaultUrl2 = this.environmentService.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl2) !== msg.referrer) {
return;
}
const params =
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
`remember=${encodeURIComponent(msg.remember)}`;
BrowserApi.createNewTab(
`popup/index.html?uilocation=popout#/2fa;${params}`,
undefined,
false
);
break;
case "reloadPopup":
this.messagingService.send("reloadPopup");
break;
case "emailVerificationRequired":
this.messagingService.send("showDialog", {
dialogId: "emailVerificationRequired",
title: this.i18nService.t("emailVerificationRequired"),
text: this.i18nService.t("emailVerificationRequiredDesc"),
confirmText: this.i18nService.t("ok"),
type: "info",
});
break;
case "getClickedElementResponse":
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
default:
break;
} }
}
private async autofillPage() {
const totpCode = await this.autofillService.doAutoFill({
cipher: this.main.loginToAutoFill,
pageDetails: this.pageDetailsToAutoFill,
fillNewPassword: true,
});
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
}
// reset
this.main.loginToAutoFill = null;
this.pageDetailsToAutoFill = [];
}
private async checkOnInstalled() {
setTimeout(async () => {
if (this.onInstalledReason != null) {
if (this.onInstalledReason === "install") {
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
await this.setDefaultSettings();
}
this.onInstalledReason = null;
}
}, 100);
}
private async setDefaultSettings() {
// Default timeout option to "on restart".
const currentVaultTimeout = await this.storageService.get<number>(
ConstantsService.vaultTimeoutKey
);
if (currentVaultTimeout == null) {
await this.storageService.save(ConstantsService.vaultTimeoutKey, -1);
}
// Default action to "lock".
const currentVaultTimeoutAction = await this.storageService.get<string>(
ConstantsService.vaultTimeoutActionKey
);
if (currentVaultTimeoutAction == null) {
await this.storageService.save(ConstantsService.vaultTimeoutActionKey, "lock");
}
}
} }

View File

@ -1,41 +1,45 @@
import MainBackground from './main.background'; import MainBackground from "./main.background";
import NotificationBackground from './notification.background'; import NotificationBackground from "./notification.background";
export default class TabsBackground { export default class TabsBackground {
constructor(private main: MainBackground, private notificationBackground: NotificationBackground) { constructor(
private main: MainBackground,
private notificationBackground: NotificationBackground
) {}
async init() {
if (!chrome.tabs) {
return;
} }
async init() { chrome.tabs.onActivated.addListener(async (activeInfo: chrome.tabs.TabActiveInfo) => {
if (!chrome.tabs) { await this.main.refreshBadgeAndMenu();
return; this.main.messagingService.send("tabActivated");
this.main.messagingService.send("tabChanged");
});
chrome.tabs.onReplaced.addListener(async (addedTabId: number, removedTabId: number) => {
if (this.main.onReplacedRan) {
return;
}
this.main.onReplacedRan = true;
await this.notificationBackground.checkNotificationQueue();
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send("tabReplaced");
this.main.messagingService.send("tabChanged");
});
chrome.tabs.onUpdated.addListener(
async (tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
if (this.main.onUpdatedRan) {
return;
} }
this.main.onUpdatedRan = true;
chrome.tabs.onActivated.addListener(async (activeInfo: chrome.tabs.TabActiveInfo) => { await this.notificationBackground.checkNotificationQueue(tab);
await this.main.refreshBadgeAndMenu(); await this.main.refreshBadgeAndMenu();
this.main.messagingService.send('tabActivated'); this.main.messagingService.send("tabUpdated");
this.main.messagingService.send('tabChanged'); this.main.messagingService.send("tabChanged");
}); }
);
chrome.tabs.onReplaced.addListener(async (addedTabId: number, removedTabId: number) => { }
if (this.main.onReplacedRan) {
return;
}
this.main.onReplacedRan = true;
await this.notificationBackground.checkNotificationQueue();
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send('tabReplaced');
this.main.messagingService.send('tabChanged');
});
chrome.tabs.onUpdated.addListener(async (tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
if (this.main.onUpdatedRan) {
return;
}
this.main.onUpdatedRan = true;
await this.notificationBackground.checkNotificationQueue(tab);
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send('tabUpdated');
this.main.messagingService.send('tabChanged');
});
}
} }

View File

@ -1,78 +1,94 @@
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { UriMatchType } from 'jslib-common/enums/uriMatchType'; import { UriMatchType } from "jslib-common/enums/uriMatchType";
export default class WebRequestBackground { export default class WebRequestBackground {
private pendingAuthRequests: any[] = []; private pendingAuthRequests: any[] = [];
private webRequest: any; private webRequest: any;
private isFirefox: boolean; private isFirefox: boolean;
constructor(platformUtilsService: PlatformUtilsService, private cipherService: CipherService, constructor(
private vaultTimeoutService: VaultTimeoutService) { platformUtilsService: PlatformUtilsService,
this.webRequest = (window as any).chrome.webRequest; private cipherService: CipherService,
this.isFirefox = platformUtilsService.isFirefox(); private vaultTimeoutService: VaultTimeoutService
) {
this.webRequest = (window as any).chrome.webRequest;
this.isFirefox = platformUtilsService.isFirefox();
}
async init() {
if (!this.webRequest || !this.webRequest.onAuthRequired) {
return;
} }
async init() { this.webRequest.onAuthRequired.addListener(
if (!this.webRequest || !this.webRequest.onAuthRequired) { async (details: any, callback: any) => {
return; if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
if (callback) {
callback();
}
return;
} }
this.webRequest.onAuthRequired.addListener(async (details: any, callback: any) => { this.pendingAuthRequests.push(details.requestId);
if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
if (callback) {
callback();
}
return;
}
this.pendingAuthRequests.push(details.requestId); if (this.isFirefox) {
return new Promise(async (resolve, reject) => {
await this.resolveAuthCredentials(details.url, resolve, reject);
});
} else {
await this.resolveAuthCredentials(details.url, callback, callback);
}
},
{ urls: ["http://*/*", "https://*/*"] },
[this.isFirefox ? "blocking" : "asyncBlocking"]
);
if (this.isFirefox) { this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
return new Promise(async (resolve, reject) => { urls: ["http://*/*"],
await this.resolveAuthCredentials(details.url, resolve, reject); });
}); this.webRequest.onErrorOccurred.addListener(
} else { (details: any) => this.completeAuthRequest(details),
await this.resolveAuthCredentials(details.url, callback, callback); {
} urls: ["http://*/*"],
}, { urls: ['http://*/*', 'https://*/*'] }, [this.isFirefox ? 'blocking' : 'asyncBlocking']); }
);
}
this.webRequest.onCompleted.addListener( private async resolveAuthCredentials(domain: string, success: Function, error: Function) {
(details: any) => this.completeAuthRequest(details), { urls: ['http://*/*'] }); if (await this.vaultTimeoutService.isLocked()) {
this.webRequest.onErrorOccurred.addListener( error();
(details: any) => this.completeAuthRequest(details), { urls: ['http://*/*'] }); return;
} }
private async resolveAuthCredentials(domain: string, success: Function, error: Function) { try {
if (await this.vaultTimeoutService.isLocked()) { const ciphers = await this.cipherService.getAllDecryptedForUrl(
error(); domain,
return; null,
} UriMatchType.Host
);
if (ciphers == null || ciphers.length !== 1) {
error();
return;
}
try { success({
const ciphers = await this.cipherService.getAllDecryptedForUrl(domain, null, UriMatchType.Host); authCredentials: {
if (ciphers == null || ciphers.length !== 1) { username: ciphers[0].login.username,
error(); password: ciphers[0].login.password,
return; },
} });
} catch {
success({ error();
authCredentials: {
username: ciphers[0].login.username,
password: ciphers[0].login.password,
},
});
} catch {
error();
}
} }
}
private completeAuthRequest(details: any) { private completeAuthRequest(details: any) {
const i = this.pendingAuthRequests.indexOf(details.requestId); const i = this.pendingAuthRequests.indexOf(details.requestId);
if (i > -1) { if (i > -1) {
this.pendingAuthRequests.splice(i, 1); this.pendingAuthRequests.splice(i, 1);
}
} }
}
} }

View File

@ -1,25 +1,25 @@
import MainBackground from './main.background'; import MainBackground from "./main.background";
export default class WindowsBackground { export default class WindowsBackground {
private windows: any; private windows: any;
constructor(private main: MainBackground) { constructor(private main: MainBackground) {
this.windows = chrome.windows; this.windows = chrome.windows;
}
async init() {
if (!this.windows) {
return;
} }
async init() { this.windows.onFocusChanged.addListener(async (windowId: any) => {
if (!this.windows) { if (windowId === null || windowId < 0) {
return; return;
} }
this.windows.onFocusChanged.addListener(async (windowId: any) => { await this.main.refreshBadgeAndMenu();
if (windowId === null || windowId < 0) { this.main.messagingService.send("windowFocused");
return; this.main.messagingService.send("windowChanged");
} });
}
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send('windowFocused');
this.main.messagingService.send('windowChanged');
});
}
} }

View File

@ -1,208 +1,228 @@
import { SafariApp } from './safariApp'; import { SafariApp } from "./safariApp";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class BrowserApi { export class BrowserApi {
static isWebExtensionsApi: boolean = (typeof browser !== 'undefined'); static isWebExtensionsApi: boolean = typeof browser !== "undefined";
static isSafariApi: boolean = navigator.userAgent.indexOf(' Safari/') !== -1 && static isSafariApi: boolean =
navigator.userAgent.indexOf(' Chrome/') === -1 && navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf(' Chromium/') === -1; navigator.userAgent.indexOf(" Chrome/") === -1 &&
static isChromeApi: boolean = !BrowserApi.isSafariApi && (typeof chrome !== 'undefined'); navigator.userAgent.indexOf(" Chromium/") === -1;
static isFirefoxOnAndroid: boolean = navigator.userAgent.indexOf('Firefox/') !== -1 && static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
navigator.userAgent.indexOf('Android') !== -1; static isFirefoxOnAndroid: boolean =
navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1;
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null { static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({ return await BrowserApi.tabsQueryFirst({
active: true, active: true,
windowId: chrome.windows.WINDOW_ID_CURRENT, windowId: chrome.windows.WINDOW_ID_CURRENT,
}); });
}
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({
active: true,
currentWindow: true,
});
}
static async getActiveTabs(): Promise<chrome.tabs.Tab[]> {
return await BrowserApi.tabsQuery({
active: true,
});
}
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
return new Promise((resolve) => {
chrome.tabs.query(options, (tabs: any[]) => {
resolve(tabs);
});
});
}
static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab> | null {
const tabs = await BrowserApi.tabsQuery(options);
if (tabs.length > 0) {
return tabs[0];
} }
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null { return null;
return await BrowserApi.tabsQueryFirst({ }
active: true,
currentWindow: true, static tabSendMessageData(
}); tab: chrome.tabs.Tab,
command: string,
data: any = null
): Promise<any[]> {
const obj: any = {
command: command,
};
if (data != null) {
obj.data = data;
} }
static async getActiveTabs(): Promise<chrome.tabs.Tab[]> { return BrowserApi.tabSendMessage(tab, obj);
return await BrowserApi.tabsQuery({ }
active: true,
}); static async tabSendMessage(
tab: chrome.tabs.Tab,
obj: any,
options: chrome.tabs.MessageSendOptions = null
): Promise<any> {
if (!tab || !tab.id) {
return;
} }
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> { return new Promise<void>((resolve) => {
return new Promise(resolve => { chrome.tabs.sendMessage(tab.id, obj, options, () => {
chrome.tabs.query(options, (tabs: any[]) => { if (chrome.runtime.lastError) {
resolve(tabs); // Some error happened
});
});
}
static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab> | null {
const tabs = await BrowserApi.tabsQuery(options);
if (tabs.length > 0) {
return tabs[0];
} }
resolve();
});
});
}
return null; static getBackgroundPage(): any {
return chrome.extension.getBackgroundPage();
}
static getApplicationVersion(): string {
return chrome.runtime.getManifest().version;
}
static async isPopupOpen(): Promise<boolean> {
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
}
static createNewTab(url: string, extensionPage: boolean = false, active: boolean = true) {
chrome.tabs.create({ url: url, active: active });
}
static messageListener(
name: string,
callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void
) {
chrome.runtime.onMessage.addListener(
(msg: any, sender: chrome.runtime.MessageSender, response: any) => {
callback(msg, sender, response);
}
);
}
static async closeLoginTab() {
const tabs = await BrowserApi.tabsQuery({
active: true,
title: "Bitwarden",
windowType: "normal",
currentWindow: true,
});
if (tabs.length === 0) {
return;
} }
static tabSendMessageData(tab: chrome.tabs.Tab, command: string, data: any = null): Promise<any[]> { const tabToClose = tabs[tabs.length - 1].id;
const obj: any = { chrome.tabs.remove(tabToClose);
command: command, }
};
if (data != null) { static async focusSpecifiedTab(tabId: number) {
obj.data = data; chrome.tabs.update(tabId, { active: true, highlighted: true });
} }
return BrowserApi.tabSendMessage(tab, obj); static closePopup(win: Window) {
if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) {
// Reactivating the active tab dismisses the popup tab. The promise final
// condition is only called if the popup wasn't already dismissed (future proofing).
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1433604
browser.tabs.update({ active: true }).finally(win.close);
} else {
win.close();
} }
}
static async tabSendMessage(tab: chrome.tabs.Tab, obj: any, options: chrome.tabs.MessageSendOptions = null): Promise<any> { static downloadFile(win: Window, blobData: any, blobOptions: any, fileName: string) {
if (!tab || !tab.id) { if (BrowserApi.isSafariApi) {
return; const type = blobOptions != null ? blobOptions.type : null;
} let data: string = null;
if (type === "text/plain" && typeof blobData === "string") {
return new Promise<void>(resolve => { data = blobData;
chrome.tabs.sendMessage(tab.id, obj, options, () => { } else {
if (chrome.runtime.lastError) { data = Utils.fromBufferToB64(blobData);
// Some error happened }
} SafariApp.sendMessageToApp(
resolve(); "downloadFile",
}); JSON.stringify({
}); blobData: data,
blobOptions: blobOptions,
fileName: fileName,
}),
true
);
} else {
const blob = new Blob([blobData], blobOptions);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
const a = win.document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
win.document.body.appendChild(a);
a.click();
win.document.body.removeChild(a);
}
} }
}
static getBackgroundPage(): any { static gaFilter() {
return chrome.extension.getBackgroundPage(); return process.env.ENV !== "production";
}
static getUILanguage(win: Window) {
return chrome.i18n.getUILanguage();
}
static reloadExtension(win: Window) {
if (win != null) {
return win.location.reload(true);
} else {
return chrome.runtime.reload();
} }
}
static getApplicationVersion(): string { static reloadOpenWindows() {
return chrome.runtime.getManifest().version; const views = chrome.extension.getViews() as Window[];
views
.filter((w) => w.location.href != null)
.forEach((w) => {
w.location.reload();
});
}
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.connectNative(application);
} else if (BrowserApi.isChromeApi) {
return chrome.runtime.connectNative(application);
} }
}
static async isPopupOpen(): Promise<boolean> { static requestPermission(permission: any) {
return Promise.resolve(chrome.extension.getViews({ type: 'popup' }).length > 0); if (BrowserApi.isWebExtensionsApi) {
return browser.permissions.request(permission);
} }
return new Promise((resolve, reject) => {
chrome.permissions.request(permission, resolve);
});
}
static createNewTab(url: string, extensionPage: boolean = false, active: boolean = true) { static getPlatformInfo(): Promise<browser.runtime.PlatformInfo | chrome.runtime.PlatformInfo> {
chrome.tabs.create({ url: url, active: active }); if (BrowserApi.isWebExtensionsApi) {
} return browser.runtime.getPlatformInfo();
static messageListener(name: string, callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void) {
chrome.runtime.onMessage.addListener((msg: any, sender: chrome.runtime.MessageSender, response: any) => {
callback(msg, sender, response);
});
}
static async closeLoginTab() {
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 });
}
static closePopup(win: Window) {
if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) {
// Reactivating the active tab dismisses the popup tab. The promise final
// condition is only called if the popup wasn't already dismissed (future proofing).
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1433604
browser.tabs.update({ active: true }).finally(win.close);
} else {
win.close();
}
}
static downloadFile(win: Window, blobData: any, blobOptions: any, fileName: string) {
if (BrowserApi.isSafariApi) {
const type = blobOptions != null ? blobOptions.type : null;
let data: string = null;
if (type === 'text/plain' && typeof (blobData) === 'string') {
data = blobData;
} else {
data = Utils.fromBufferToB64(blobData);
}
SafariApp.sendMessageToApp('downloadFile', JSON.stringify({
blobData: data,
blobOptions: blobOptions,
fileName: fileName,
}), true);
} else {
const blob = new Blob([blobData], blobOptions);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
const a = win.document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
win.document.body.appendChild(a);
a.click();
win.document.body.removeChild(a);
}
}
}
static gaFilter() {
return process.env.ENV !== 'production';
}
static getUILanguage(win: Window) {
return chrome.i18n.getUILanguage();
}
static reloadExtension(win: Window) {
if (win != null) {
return win.location.reload(true);
} else {
return chrome.runtime.reload();
}
}
static reloadOpenWindows() {
const views = chrome.extension.getViews() as Window[];
views.filter(w => w.location.href != null).forEach(w => {
w.location.reload();
});
}
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.connectNative(application);
} else if (BrowserApi.isChromeApi) {
return chrome.runtime.connectNative(application);
}
}
static requestPermission(permission: any) {
if (BrowserApi.isWebExtensionsApi) {
return browser.permissions.request(permission);
}
return new Promise((resolve, reject) => {
chrome.permissions.request(permission, resolve);
});
}
static getPlatformInfo(): Promise<browser.runtime.PlatformInfo | chrome.runtime.PlatformInfo> {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.getPlatformInfo();
}
return new Promise(resolve => {
chrome.runtime.getPlatformInfo(resolve);
});
} }
return new Promise((resolve) => {
chrome.runtime.getPlatformInfo(resolve);
});
}
} }

View File

@ -1,21 +1,26 @@
import { BrowserApi } from './browserApi'; import { BrowserApi } from "./browserApi";
export class SafariApp { export class SafariApp {
static sendMessageToApp(command: string, data: any = null, resolveNow = false): Promise<any> { static sendMessageToApp(command: string, data: any = null, resolveNow = false): Promise<any> {
if (!BrowserApi.isSafariApi) { if (!BrowserApi.isSafariApi) {
return Promise.resolve(null); return Promise.resolve(null);
}
return new Promise(resolve => {
const now = new Date();
const messageId = now.getTime().toString() + '_' + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
(browser as any).runtime.sendNativeMessage('com.bitwarden.desktop', {
id: messageId,
command: command,
data: data,
responseData: null,
}, (response: any) => {
resolve(response);
});
});
} }
return new Promise((resolve) => {
const now = new Date();
const messageId =
now.getTime().toString() + "_" + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
(browser as any).runtime.sendNativeMessage(
"com.bitwarden.desktop",
{
id: messageId,
command: command,
data: data,
responseData: null,
},
(response: any) => {
resolve(response);
}
);
});
}
} }

View File

@ -1,36 +1,36 @@
@-webkit-keyframes bitwardenfill { @-webkit-keyframes bitwardenfill {
0% { 0% {
-webkit-transform: scale(1.0, 1.0); -webkit-transform: scale(1, 1);
} }
50% { 50% {
-webkit-transform: scale(1.2, 1.2); -webkit-transform: scale(1.2, 1.2);
} }
100% { 100% {
-webkit-transform: scale(1.0, 1.0); -webkit-transform: scale(1, 1);
} }
} }
@-moz-keyframes bitwardenfill { @-moz-keyframes bitwardenfill {
0% { 0% {
transform: scale(1.0, 1.0); transform: scale(1, 1);
} }
50% { 50% {
transform: scale(1.2, 1.2); transform: scale(1.2, 1.2);
} }
100% { 100% {
transform: scale(1.0, 1.0); transform: scale(1, 1);
} }
} }
span[data-bwautofill].com-bitwarden-browser-animated-fill { span[data-bwautofill].com-bitwarden-browser-animated-fill {
display: inline-block; display: inline-block;
} }
.com-bitwarden-browser-animated-fill { .com-bitwarden-browser-animated-fill {
animation: bitwardenfill 200ms ease-in-out 0ms 1; animation: bitwardenfill 200ms ease-in-out 0ms 1;
-webkit-animation: bitwardenfill 200ms ease-in-out 0ms 1; -webkit-animation: bitwardenfill 200ms ease-in-out 0ms 1;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,43 @@
document.addEventListener('DOMContentLoaded', event => { document.addEventListener("DOMContentLoaded", (event) => {
let pageHref: string = null; let pageHref: string = null;
let filledThisHref = false; let filledThisHref = false;
let delayFillTimeout: number; let delayFillTimeout: number;
const enabledKey = 'enableAutoFillOnPageLoad'; const enabledKey = "enableAutoFillOnPageLoad";
chrome.storage.local.get(enabledKey, (obj: any) => { chrome.storage.local.get(enabledKey, (obj: any) => {
if (obj != null && obj[enabledKey] === true) { if (obj != null && obj[enabledKey] === true) {
setInterval(() => doFillIfNeeded(), 500); setInterval(() => doFillIfNeeded(), 500);
}
});
chrome.runtime.onMessage.addListener((msg: any, sender: any, sendResponse: Function) => {
if (msg.command === 'fillForm' && pageHref === msg.url) {
filledThisHref = true;
}
});
function doFillIfNeeded(force: boolean = false) {
if (force || pageHref !== window.location.href) {
if (!force) {
// Some websites are slow and rendering all page content. Try to fill again later
// if we haven't already.
filledThisHref = false;
if (delayFillTimeout != null) {
window.clearTimeout(delayFillTimeout);
}
delayFillTimeout = window.setTimeout(() => {
if (!filledThisHref) {
doFillIfNeeded(true);
}
}, 1500);
}
pageHref = window.location.href;
const msg: any = {
command: 'bgCollectPageDetails',
sender: 'autofiller',
};
chrome.runtime.sendMessage(msg);
}
} }
});
chrome.runtime.onMessage.addListener((msg: any, sender: any, sendResponse: Function) => {
if (msg.command === "fillForm" && pageHref === msg.url) {
filledThisHref = true;
}
});
function doFillIfNeeded(force: boolean = false) {
if (force || pageHref !== window.location.href) {
if (!force) {
// Some websites are slow and rendering all page content. Try to fill again later
// if we haven't already.
filledThisHref = false;
if (delayFillTimeout != null) {
window.clearTimeout(delayFillTimeout);
}
delayFillTimeout = window.setTimeout(() => {
if (!filledThisHref) {
doFillIfNeeded(true);
}
}, 1500);
}
pageHref = window.location.href;
const msg: any = {
command: "bgCollectPageDetails",
sender: "autofiller",
};
chrome.runtime.sendMessage(msg);
}
}
}); });

View File

@ -1,66 +1,66 @@
const inputTags = ['input', 'textarea', 'select']; const inputTags = ["input", "textarea", "select"];
const labelTags = ['label', 'span']; const labelTags = ["label", "span"];
const attributes = ['id', 'name', 'label-aria', 'placeholder']; const attributes = ["id", "name", "label-aria", "placeholder"];
const invalidElement = chrome.i18n.getMessage('copyCustomFieldNameInvalidElement'); const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
const noUniqueIdentifier = chrome.i18n.getMessage('copyCustomFieldNameNotUnique'); const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
let clickedEl: HTMLElement = null; let clickedEl: HTMLElement = null;
// Find the best attribute to be used as the Name for an element in a custom field. // Find the best attribute to be used as the Name for an element in a custom field.
function getClickedElementIdentifier() { function getClickedElementIdentifier() {
if (clickedEl == null) { if (clickedEl == null) {
return invalidElement; return invalidElement;
} }
const clickedTag = clickedEl.nodeName.toLowerCase(); const clickedTag = clickedEl.nodeName.toLowerCase();
let inputEl = null; let inputEl = null;
// Try to identify the input element (which may not be the clicked element) // Try to identify the input element (which may not be the clicked element)
if (labelTags.includes(clickedTag)) { if (labelTags.includes(clickedTag)) {
let inputId = null; let inputId = null;
if (clickedTag === 'label') { if (clickedTag === "label") {
inputId = clickedEl.getAttribute('for'); inputId = clickedEl.getAttribute("for");
} else {
inputId = clickedEl.closest('label')?.getAttribute('for');
}
inputEl = document.getElementById(inputId);
} else { } else {
inputEl = clickedEl; inputId = clickedEl.closest("label")?.getAttribute("for");
} }
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) { inputEl = document.getElementById(inputId);
return invalidElement; } else {
} inputEl = clickedEl;
}
for (const attr of attributes) { if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) {
const attributeValue = inputEl.getAttribute(attr); return invalidElement;
const selector = '[' + attr + '="' + attributeValue + '"]'; }
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
return attributeValue; for (const attr of attributes) {
} const attributeValue = inputEl.getAttribute(attr);
const selector = "[" + attr + '="' + attributeValue + '"]';
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
return attributeValue;
} }
return noUniqueIdentifier; }
return noUniqueIdentifier;
} }
function isNullOrEmpty(s: string) { function isNullOrEmpty(s: string) {
return s == null || s === ''; return s == null || s === "";
} }
// We only have access to the element that's been clicked when the context menu is first opened. // We only have access to the element that's been clicked when the context menu is first opened.
// Remember it for use later. // Remember it for use later.
document.addEventListener('contextmenu', event => { document.addEventListener("contextmenu", (event) => {
clickedEl = event.target as HTMLElement; clickedEl = event.target as HTMLElement;
}); });
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked. // Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
chrome.runtime.onMessage.addListener(event => { chrome.runtime.onMessage.addListener((event) => {
if (event.command === 'getClickedElement') { if (event.command === "getClickedElement") {
const identifier = getClickedElementIdentifier(); const identifier = getClickedElementIdentifier();
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
command: 'getClickedElementResponse', command: "getClickedElementResponse",
sender: 'contextMenuHandler', sender: "contextMenuHandler",
identifier: identifier, identifier: identifier,
}); });
} }
}); });

View File

@ -1,30 +1,37 @@
window.addEventListener('message', event => { window.addEventListener(
if (event.source !== window) "message",
return; (event) => {
if (event.source !== window) return;
if (event.data.command && (event.data.command === 'authResult')) { if (event.data.command && event.data.command === "authResult") {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
command: event.data.command, command: event.data.command,
code: event.data.code, code: event.data.code,
state: event.data.state, state: event.data.state,
referrer: event.source.location.hostname, referrer: event.source.location.hostname,
}); });
} }
if (event.data.command && (event.data.command === 'webAuthnResult')) { if (event.data.command && event.data.command === "webAuthnResult") {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
command: event.data.command, command: event.data.command,
data: event.data.data, data: event.data.data,
remember: event.data.remember, remember: event.data.remember,
referrer: event.source.location.hostname, referrer: event.source.location.hostname,
}); });
} }
}, false); },
false
);
const forwardCommands = ['promptForLogin', 'addToLockedVaultPendingNotifications', 'unlockCompleted']; const forwardCommands = [
"promptForLogin",
"addToLockedVaultPendingNotifications",
"unlockCompleted",
];
chrome.runtime.onMessage.addListener(event => { chrome.runtime.onMessage.addListener((event) => {
if (forwardCommands.includes(event.command)) { if (forwardCommands.includes(event.command)) {
chrome.runtime.sendMessage(event); chrome.runtime.sendMessage(event);
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,52 @@
import * as Mousetrap from 'mousetrap'; import * as Mousetrap from "mousetrap";
document.addEventListener('DOMContentLoaded', event => { document.addEventListener("DOMContentLoaded", (event) => {
const isSafari = (typeof safari !== 'undefined') && navigator.userAgent.indexOf(' Safari/') !== -1 && const isSafari =
navigator.userAgent.indexOf('Chrome') === -1; typeof safari !== "undefined" &&
const isVivaldi = !isSafari && navigator.userAgent.indexOf(' Vivaldi/') !== -1; navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf("Chrome") === -1;
const isVivaldi = !isSafari && navigator.userAgent.indexOf(" Vivaldi/") !== -1;
if (!isSafari && !isVivaldi) { if (!isSafari && !isVivaldi) {
return; return;
} }
if (isSafari && (window as any).__bitwardenFrameId == null) { if (isSafari && (window as any).__bitwardenFrameId == null) {
(window as any).__bitwardenFrameId = Math.floor(Math.random() * Math.floor(99999999)); (window as any).__bitwardenFrameId = Math.floor(Math.random() * Math.floor(99999999));
} }
Mousetrap.prototype.stopCallback = () => { Mousetrap.prototype.stopCallback = () => {
return false; return false;
}; };
let autofillCommand = ['mod+shift+l']; let autofillCommand = ["mod+shift+l"];
if (isSafari) { if (isSafari) {
autofillCommand = ['mod+\\', 'mod+8', 'mod+shift+p']; autofillCommand = ["mod+\\", "mod+8", "mod+shift+p"];
} }
Mousetrap.bind(autofillCommand, () => { Mousetrap.bind(autofillCommand, () => {
sendMessage('autofill_login'); sendMessage("autofill_login");
});
if (isSafari) {
Mousetrap.bind("mod+shift+y", () => {
sendMessage("open_popup");
}); });
if (isSafari) { Mousetrap.bind("mod+shift+s", () => {
Mousetrap.bind('mod+shift+y', () => { sendMessage("lock_vault");
sendMessage('open_popup'); });
}); } else {
Mousetrap.bind("mod+shift+9", () => {
sendMessage("generate_password");
});
}
Mousetrap.bind('mod+shift+s', () => { function sendMessage(shortcut: string) {
sendMessage('lock_vault'); const msg: any = {
}); command: "keyboardShortcutTriggered",
} else { shortcut: shortcut,
Mousetrap.bind('mod+shift+9', () => { };
sendMessage('generate_password');
});
}
function sendMessage(shortcut: string) { chrome.runtime.sendMessage(msg);
const msg: any = { }
command: 'keyboardShortcutTriggered',
shortcut: shortcut,
};
chrome.runtime.sendMessage(msg);
}
}); });

View File

@ -23,47 +23,25 @@
"content/notificationBar.js", "content/notificationBar.js",
"content/contextMenuHandler.js" "content/contextMenuHandler.js"
], ],
"matches": [ "matches": ["http://*/*", "https://*/*", "file:///*"],
"http://*/*",
"https://*/*",
"file:///*"
],
"run_at": "document_start" "run_at": "document_start"
}, },
{ {
"all_frames": false, "all_frames": false,
"js": [ "js": ["content/shortcuts.js"],
"content/shortcuts.js" "matches": ["http://*/*", "https://*/*", "file:///*"],
],
"matches": [
"http://*/*",
"https://*/*",
"file:///*"
],
"run_at": "document_start" "run_at": "document_start"
}, },
{ {
"all_frames": false, "all_frames": false,
"js": [ "js": ["content/message_handler.js"],
"content/message_handler.js" "matches": ["http://*/*", "https://*/*", "file:///*"],
],
"matches": [
"http://*/*",
"https://*/*",
"file:///*"
],
"run_at": "document_start" "run_at": "document_start"
}, },
{ {
"all_frames": true, "all_frames": true,
"css": [ "css": ["content/autofill.css"],
"content/autofill.css" "matches": ["http://*/*", "https://*/*", "file:///*"],
],
"matches": [
"http://*/*",
"https://*/*",
"file:///*"
],
"run_at": "document_end" "run_at": "document_end"
} }
], ],
@ -92,9 +70,7 @@
"webRequest", "webRequest",
"webRequestBlocking" "webRequestBlocking"
], ],
"optional_permissions": [ "optional_permissions": ["nativeMessaging"],
"nativeMessaging"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"commands": { "commands": {
"_execute_browser_action": { "_execute_browser_action": {
@ -144,4 +120,4 @@
"default_panel": "popup/index.html?uilocation=sidebar", "default_panel": "popup/index.html?uilocation=sidebar",
"default_icon": "images/icon19.png" "default_icon": "images/icon19.png"
} }
} }

View File

@ -1,25 +1,25 @@
export default class AutofillField { export default class AutofillField {
opid: string; opid: string;
elementNumber: number; elementNumber: number;
visible: boolean; visible: boolean;
viewable: boolean; viewable: boolean;
htmlID: string; htmlID: string;
htmlName: string; htmlName: string;
htmlClass: string; htmlClass: string;
'label-left': string; "label-left": string;
'label-right': string; "label-right": string;
'label-top': string; "label-top": string;
'label-tag': string; "label-tag": string;
'label-aria': string; "label-aria": string;
placeholder: string; placeholder: string;
type: string; type: string;
value: string; value: string;
disabled: boolean; disabled: boolean;
readonly: boolean; readonly: boolean;
onePasswordFieldType: string; onePasswordFieldType: string;
form: string; form: string;
autoCompleteType: string; autoCompleteType: string;
selectInfo: any; selectInfo: any;
maxLength: number; maxLength: number;
tagName: string; tagName: string;
} }

View File

@ -1,7 +1,7 @@
export default class AutofillForm { export default class AutofillForm {
opid: string; opid: string;
htmlName: string; htmlName: string;
htmlID: string; htmlID: string;
htmlAction: string; htmlAction: string;
htmlMethod: string; htmlMethod: string;
} }

View File

@ -1,13 +1,13 @@
import AutofillField from './autofillField'; import AutofillField from "./autofillField";
import AutofillForm from './autofillForm'; import AutofillForm from "./autofillForm";
export default class AutofillPageDetails { export default class AutofillPageDetails {
documentUUID: string; documentUUID: string;
title: string; title: string;
url: string; url: string;
documentUrl: string; documentUrl: string;
tabUrl: string; tabUrl: string;
forms: { [id: string]: AutofillForm; }; forms: { [id: string]: AutofillForm };
fields: AutofillField[]; fields: AutofillField[];
collectedTimestamp: number; collectedTimestamp: number;
} }

View File

@ -1,12 +1,12 @@
export default class AutofillScript { export default class AutofillScript {
script: string[][] = []; script: string[][] = [];
documentUUID: any = {}; documentUUID: any = {};
properties: any = {}; properties: any = {};
options: any = {}; options: any = {};
metadata: any = {}; metadata: any = {};
autosubmit: any = null; autosubmit: any = null;
constructor(documentUUID: string) { constructor(documentUUID: string) {
this.documentUUID = documentUUID; this.documentUUID = documentUUID;
} }
} }

View File

@ -1,41 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<head>
<title>Bitwarden</title> <title>Bitwarden</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
</head> </head>
<body> <body>
<div class="outer-wrapper"> <div class="outer-wrapper">
<div class="logo"> <div class="logo">
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link"> <a href="https://vault.bitwarden.com" target="_blank" id="logo-link">
<img id="logo" alt="Bitwarden" /> <img id="logo" alt="Bitwarden" />
</a> </a>
</div> </div>
<div id="content"></div> <div id="content"></div>
<div> <div>
<button type="button" class="neutral" id="close-button"> <button type="button" class="neutral" id="close-button">
<img id="close" alt="Close" /> <img id="close" alt="Close" />
</button> </button>
</div> </div>
</div> </div>
<div id="templates" style="display: none;"> <div id="templates" style="display: none">
<div class="inner-wrapper" id="template-add"> <div class="inner-wrapper" id="template-add">
<div class="add-text"></div> <div class="add-text"></div>
<div class="add-buttons"> <div class="add-buttons">
<button type="button" class="never-save link"></button> <button type="button" class="never-save link"></button>
<select class="select-folder" isVaultLocked="false"></select> <select class="select-folder" isVaultLocked="false"></select>
<button type="button" class="add-save"></button> <button type="button" class="add-save"></button>
</div>
</div> </div>
<div class="inner-wrapper" id="template-change"> </div>
<div class="change-text"></div> <div class="inner-wrapper" id="template-change">
<div class="change-buttons"> <div class="change-text"></div>
<button type="button" class="change-save"></button> <div class="change-buttons">
</div> <button type="button" class="change-save"></button>
</div> </div>
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,159 +1,160 @@
require('./bar.scss'); require("./bar.scss");
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
var i18n = {}; var i18n = {};
var lang = window.navigator.language; var lang = window.navigator.language;
i18n.appName = chrome.i18n.getMessage('appName'); i18n.appName = chrome.i18n.getMessage("appName");
i18n.close = chrome.i18n.getMessage('close'); i18n.close = chrome.i18n.getMessage("close");
i18n.never = chrome.i18n.getMessage('never'); i18n.never = chrome.i18n.getMessage("never");
i18n.folder = chrome.i18n.getMessage('folder'); i18n.folder = chrome.i18n.getMessage("folder");
i18n.notificationAddSave = chrome.i18n.getMessage('notificationAddSave'); i18n.notificationAddSave = chrome.i18n.getMessage("notificationAddSave");
i18n.notificationAddDesc = chrome.i18n.getMessage('notificationAddDesc'); i18n.notificationAddDesc = chrome.i18n.getMessage("notificationAddDesc");
i18n.notificationChangeSave = chrome.i18n.getMessage('notificationChangeSave'); i18n.notificationChangeSave = chrome.i18n.getMessage("notificationChangeSave");
i18n.notificationChangeDesc = chrome.i18n.getMessage('notificationChangeDesc'); i18n.notificationChangeDesc = chrome.i18n.getMessage("notificationChangeDesc");
lang = chrome.i18n.getUILanguage(); lang = chrome.i18n.getUILanguage();
// delay 50ms so that we get proper body dimensions // delay 50ms so that we get proper body dimensions
setTimeout(load, 50); setTimeout(load, 50);
function load() { function load() {
const isVaultLocked = getQueryVariable('isVaultLocked') == 'true'; const isVaultLocked = getQueryVariable("isVaultLocked") == "true";
document.getElementById('logo').src = isVaultLocked document.getElementById("logo").src = isVaultLocked
? chrome.runtime.getURL('images/icon38_locked.png') ? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL('images/icon38.png'); : chrome.runtime.getURL("images/icon38.png");
document.getElementById('logo-link').title = i18n.appName; document.getElementById("logo-link").title = i18n.appName;
var neverButton = document.querySelector('#template-add .never-save'); var neverButton = document.querySelector("#template-add .never-save");
neverButton.textContent = i18n.never; neverButton.textContent = i18n.never;
var selectFolder = document.querySelector('#template-add .select-folder'); var selectFolder = document.querySelector("#template-add .select-folder");
selectFolder.setAttribute('aria-label', i18n.folder); selectFolder.setAttribute("aria-label", i18n.folder);
selectFolder.setAttribute('isVaultLocked', isVaultLocked.toString()); selectFolder.setAttribute("isVaultLocked", isVaultLocked.toString());
var addButton = document.querySelector('#template-add .add-save'); var addButton = document.querySelector("#template-add .add-save");
addButton.textContent = i18n.notificationAddSave; addButton.textContent = i18n.notificationAddSave;
var changeButton = document.querySelector('#template-change .change-save'); var changeButton = document.querySelector("#template-change .change-save");
changeButton.textContent = i18n.notificationChangeSave; changeButton.textContent = i18n.notificationChangeSave;
var closeIcon = document.getElementById('close'); var closeIcon = document.getElementById("close");
closeIcon.src = chrome.runtime.getURL('images/close.png'); closeIcon.src = chrome.runtime.getURL("images/close.png");
closeIcon.alt = i18n.close; closeIcon.alt = i18n.close;
var closeButton = document.getElementById('close-button') var closeButton = document.getElementById("close-button");
closeButton.title = i18n.close; closeButton.title = i18n.close;
closeButton.setAttribute('aria-label', i18n.close); closeButton.setAttribute("aria-label", i18n.close);
document.querySelector('#template-add .add-text').textContent = i18n.notificationAddDesc; document.querySelector("#template-add .add-text").textContent = i18n.notificationAddDesc;
document.querySelector('#template-change .change-text').textContent = i18n.notificationChangeDesc; document.querySelector("#template-change .change-text").textContent =
i18n.notificationChangeDesc;
if (getQueryVariable('add')) { if (getQueryVariable("add")) {
setContent(document.getElementById('template-add')); setContent(document.getElementById("template-add"));
var addButton = document.querySelector('#template-add-clone .add-save'), var addButton = document.querySelector("#template-add-clone .add-save"),
neverButton = document.querySelector('#template-add-clone .never-save'); neverButton = document.querySelector("#template-add-clone .never-save");
addButton.addEventListener('click', (e) => { addButton.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
const folderId = document.querySelector('#template-add-clone .select-folder').value; const folderId = document.querySelector("#template-add-clone .select-folder").value;
const bgAddSaveMessage = { const bgAddSaveMessage = {
command: 'bgAddSave', command: "bgAddSave",
folder: folderId, folder: folderId,
}; };
sendPlatformMessage(bgAddSaveMessage); sendPlatformMessage(bgAddSaveMessage);
}); });
neverButton.addEventListener('click', (e) => { neverButton.addEventListener("click", (e) => {
e.preventDefault(); 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
});
}
} else if (getQueryVariable('change')) {
setContent(document.getElementById('template-change'));
var changeButton = document.querySelector('#template-change-clone .change-save');
changeButton.addEventListener('click', (e) => {
e.preventDefault();
const bgChangeSaveMessage = {
command: 'bgChangeSave'
};
sendPlatformMessage(bgChangeSaveMessage);
});
}
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 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({ sendPlatformMessage({
command: 'bgAdjustNotificationBar', command: "bgNeverSave",
data: {
height: document.querySelector('body').scrollHeight
}
}); });
});
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,
});
}
} else if (getQueryVariable("change")) {
setContent(document.getElementById("template-change"));
var changeButton = document.querySelector("#template-change-clone .change-save");
changeButton.addEventListener("click", (e) => {
e.preventDefault();
const bgChangeSaveMessage = {
command: "bgChangeSave",
};
sendPlatformMessage(bgChangeSaveMessage);
});
} }
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 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,
},
});
}
}); });

View File

@ -1,94 +1,94 @@
body { body {
background-color: #ffffff; background-color: #ffffff;
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 100%; height: 100%;
font-size: 14px; font-size: 14px;
line-height: 16px; line-height: 16px;
color: #333333; color: #333333;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
} }
.outer-wrapper { .outer-wrapper {
padding: 0 10px; padding: 0 10px;
border-bottom: 2px solid #175ddc; border-bottom: 2px solid #175ddc;
display: grid; display: grid;
grid-template-columns: 24px auto 55px; grid-template-columns: 24px auto 55px;
grid-column-gap: 10px; grid-column-gap: 10px;
box-sizing: border-box; box-sizing: border-box;
min-height: 42px; min-height: 42px;
} }
.inner-wrapper { .inner-wrapper {
display: grid; display: grid;
grid-template-columns: auto max-content; grid-template-columns: auto max-content;
} }
.outer-wrapper > *, .inner-wrapper > * { .outer-wrapper > *,
align-self: center; .inner-wrapper > * {
align-self: center;
} }
img { img {
border: 0; border: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#logo { #logo {
width: 24px; width: 24px;
height: 24px; height: 24px;
display: block; display: block;
} }
#close { #close {
width: 15px; width: 15px;
height: 15px; height: 15px;
display: block; display: block;
padding: 5px 0; padding: 5px 0;
margin-right: 10px; margin-right: 10px;
} }
button:not(.link), button:not(.link),
button:not(.neutral) { button:not(.neutral) {
background-color: #175DDC; background-color: #175ddc;
padding: 5px 15px; padding: 5px 15px;
border-radius: 3px; border-radius: 3px;
color: #ffffff; color: #ffffff;
border: 0; border: 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background-color: #1751bd; background-color: #1751bd;
} }
} }
button.link, button.link,
button.neutral { button.neutral {
background: none; background: none;
padding: 5px 15px; padding: 5px 15px;
color: #175DDC; color: #175ddc;
border: 0; border: 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background: none; background: none;
text-decoration: underline; text-decoration: underline;
} }
} }
.select-folder[isVaultLocked="true"] { .select-folder[isVaultLocked="true"] {
display: none display: none;
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.select-folder { .select-folder {
display: none display: none;
} }
} }
@media (print) { @media (print) {
body { body {
display: none; display: none;
} }
} }

View File

@ -1,67 +1,104 @@
<form #form (ngSubmit)="submit()"> <form #form (ngSubmit)="submit()">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/home">{{'close' | i18n}}</a> <a routerLink="/home">{{ "close" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ "appName" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="box">
<h2 class="box-header">
{{ "selfHostedEnvironment" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
name="BaseUrl"
[(ngModel)]="baseUrl"
placeholder="ex. https://bitwarden.company.com"
appInputVerbatim
/>
</div> </div>
<h1 class="center"> </div>
<span class="title">{{'appName' | i18n}}</span> <div class="box-footer">
</h1> {{ "selfHostedEnvironmentFooter" | i18n }}
<div class="right"> </div>
<button type="submit" appBlurClick [disabled]="form.loading"> </div>
<span [hidden]="form.loading">{{'save' | i18n}}</span> <div class="box">
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> <h2 class="box-header">
</button> {{ "customEnvironment" | i18n }}
</h2>
<div class="box-content" [hidden]="!showCustom">
<div class="box-content-row" appBoxRow>
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
name="WebVaultUrl"
[(ngModel)]="webVaultUrl"
inputmode="url"
appInputVerbatim
/>
</div> </div>
</header> <div class="box-content-row" appBoxRow>
<content> <label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<div class="box"> <input
<h2 class="box-header"> id="apiUrl"
{{'selfHostedEnvironment' | i18n}} type="text"
</h2> name="ApiUrl"
<div class="box-content"> [(ngModel)]="apiUrl"
<div class="box-content-row" appBoxRow> inputmode="url"
<label for="baseUrl">{{'baseUrl' | i18n}}</label> appInputVerbatim
<input id="baseUrl" type="text" name="BaseUrl" [(ngModel)]="baseUrl" />
placeholder="ex. https://bitwarden.company.com" appInputVerbatim>
</div>
</div>
<div class="box-footer">
{{'selfHostedEnvironmentFooter' | i18n}}
</div>
</div> </div>
<div class="box"> <div class="box-content-row" appBoxRow>
<h2 class="box-header"> <label for="identityUrl">{{ "identityUrl" | i18n }}</label>
{{'customEnvironment' | i18n}} <input
</h2> id="identityUrl"
<div class="box-content" [hidden]="!showCustom"> type="text"
<div class="box-content-row" appBoxRow> name="IdentityUrl"
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label> [(ngModel)]="identityUrl"
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl" inputmode="url" inputmode="url"
appInputVerbatim> appInputVerbatim
</div> />
<div class="box-content-row" appBoxRow>
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" inputmode="url" appInputVerbatim>
</div>
<div class="box-content-row" appBoxRow>
<label for="identityUrl">{{'identityUrl' | i18n}}</label>
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl" inputmode="url"
appInputVerbatim>
</div>
<div class="box-content-row" appBoxRow>
<label for="notificationsUrl">{{'notificationsUrl' | i18n}}</label>
<input id="notificationsUrl" type="text" name="NotificationsUrl" inputmode="url"
[(ngModel)]="notificationsUrl" appInputVerbatim>
</div>
<div class="box-content-row" appBoxRow>
<label for="iconsUrl">{{'iconsUrl' | i18n}}</label>
<input id="iconsUrl" type="text" name="IconsUrl" [(ngModel)]="iconsUrl" inputmode="url"
appInputVerbatim>
</div>
</div>
<div class="box-footer" [hidden]="!showCustom">
{{'customEnvironmentFooter' | i18n}}
</div>
</div> </div>
</content> <div class="box-content-row" appBoxRow>
<label for="notificationsUrl">{{ "notificationsUrl" | i18n }}</label>
<input
id="notificationsUrl"
type="text"
name="NotificationsUrl"
inputmode="url"
[(ngModel)]="notificationsUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="iconsUrl">{{ "iconsUrl" | i18n }}</label>
<input
id="iconsUrl"
type="text"
name="IconsUrl"
[(ngModel)]="iconsUrl"
inputmode="url"
appInputVerbatim
/>
</div>
</div>
<div class="box-footer" [hidden]="!showCustom">
{{ "customEnvironmentFooter" | i18n }}
</div>
</div>
</content>
</form> </form>

View File

@ -1,25 +1,29 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component'; import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component";
@Component({ @Component({
selector: 'app-environment', selector: "app-environment",
templateUrl: 'environment.component.html', templateUrl: "environment.component.html",
}) })
export class EnvironmentComponent extends BaseEnvironmentComponent { export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, constructor(
i18nService: I18nService, private router: Router) { platformUtilsService: PlatformUtilsService,
super(platformUtilsService, environmentService, i18nService); environmentService: EnvironmentService,
this.showCustom = true; i18nService: I18nService,
} private router: Router
) {
super(platformUtilsService, environmentService, i18nService);
this.showCustom = true;
}
saved() { saved() {
super.saved(); super.saved();
this.router.navigate(['']); this.router.navigate([""]);
} }
} }

View File

@ -1,30 +1,38 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/login">{{'cancel' | i18n}}</a> <a routerLink="/login">{{ "cancel" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ "passwordHint" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
appAutofocus
inputmode="email"
appInputVerbatim="false"
/>
</div> </div>
<h1 class="center"> </div>
<span class="title">{{'passwordHint' | i18n}}</span> <div class="box-footer">
</h1> {{ "enterEmailToGetHint" | i18n }}
<div class="right"> </div>
<button type="submit" appBlurClick [disabled]="form.loading"> </div>
<span [hidden]="form.loading">{{'submit' | i18n}}</span> </content>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" type="text" name="Email" [(ngModel)]="email" required appAutofocus
inputmode="email" appInputVerbatim="false">
</div>
</div>
<div class="box-footer">
{{'enterEmailToGetHint' | i18n}}
</div>
</div>
</content>
</form> </form>

View File

@ -1,20 +1,25 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { HintComponent as BaseHintComponent } from 'jslib-angular/components/hint.component'; import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component";
@Component({ @Component({
selector: 'app-hint', selector: "app-hint",
templateUrl: 'hint.component.html', templateUrl: "hint.component.html",
}) })
export class HintComponent extends BaseHintComponent { export class HintComponent extends BaseHintComponent {
constructor(router: Router, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, apiService: ApiService, logService: LogService) { router: Router,
super(router, i18nService, apiService, platformUtilsService, logService); platformUtilsService: PlatformUtilsService,
} i18nService: I18nService,
apiService: ApiService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
} }

View File

@ -1,14 +1,16 @@
<div class="center-content"> <div class="center-content">
<div class="content"> <div class="content">
<div class="logo-image"></div> <div class="logo-image"></div>
<p class="lead text-center">{{'loginOrCreateNewAccount' | i18n}}</p> <p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p>
<a class="btn primary block" routerLink="/login"><b>{{'login' | i18n}}</b></a> <a class="btn primary block" routerLink="/login"
<button type="button" (click)="launchSsoBrowser()" class="btn block"> ><b>{{ "login" | i18n }}</b></a
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}} >
</button> <button type="button" (click)="launchSsoBrowser()" class="btn block">
<a class="btn block" routerLink="/register">{{'createAccount' | i18n}}</a> <i class="fa fa-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</div> </button>
<a class="btn block" routerLink="/register">{{ "createAccount" | i18n }}</a>
</div>
</div> </div>
<a routerLink="/environment" class="settings-icon"> <a routerLink="/environment" class="settings-icon">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i><span>&nbsp;{{'settings' | i18n}}</span> <i class="fa fa-cog fa-lg" aria-hidden="true"></i><span>&nbsp;{{ "settings" | i18n }}</span>
</a> </a>

View File

@ -1,53 +1,66 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
@Component({ @Component({
selector: 'app-home', selector: "app-home",
templateUrl: 'home.component.html', templateUrl: "home.component.html",
}) })
export class HomeComponent { export class HomeComponent {
constructor(protected platformUtilsService: PlatformUtilsService, constructor(
private passwordGenerationService: PasswordGenerationService, private storageService: StorageService, protected platformUtilsService: PlatformUtilsService,
private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService) { } private passwordGenerationService: PasswordGenerationService,
private storageService: StorageService,
private cryptoFunctionService: CryptoFunctionService,
private environmentService: EnvironmentService
) {}
async launchSsoBrowser() { async launchSsoBrowser() {
// Generate necessary sso params // Generate necessary sso params
const passwordOptions: any = { const passwordOptions: any = {
type: 'password', type: "password",
length: 64, length: 64,
uppercase: true, uppercase: true,
lowercase: true, lowercase: true,
numbers: true, numbers: true,
special: false, special: false,
}; };
const state = (await this.passwordGenerationService.generatePassword(passwordOptions)) + ':clientId=browser'; const state =
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); (await this.passwordGenerationService.generatePassword(passwordOptions)) +
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); ":clientId=browser";
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier);
await this.storageService.save(ConstantsService.ssoStateKey, state); await this.storageService.save(ConstantsService.ssoStateKey, state);
let url = this.environmentService.getWebVaultUrl(); let url = this.environmentService.getWebVaultUrl();
if (url == null) { if (url == null) {
url = 'https://vault.bitwarden.com'; url = "https://vault.bitwarden.com";
}
const redirectUri = url + '/sso-connector.html';
// Launch browser
this.platformUtilsService.launchUri(url + '/#/sso?clientId=browser' +
'&redirectUri=' + encodeURIComponent(redirectUri) +
'&state=' + state + '&codeChallenge=' + codeChallenge);
} }
const redirectUri = url + "/sso-connector.html";
// Launch browser
this.platformUtilsService.launchUri(
url +
"/#/sso?clientId=browser" +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge
);
}
} }

View File

@ -1,49 +1,74 @@
<form (ngSubmit)="submit()"> <form (ngSubmit)="submit()">
<header> <header>
<div class="left"></div> <div class="left"></div>
<h1 class="center"> <h1 class="center">
<span class="title">{{'verifyIdentity' | i18n}}</span> <span class="title">{{ "verifyIdentity" | i18n }}</span>
</h1> </h1>
<div class="right"> <div class="right">
<button type="submit" appBlurClick *ngIf="!hideInput">{{'unlock' | i18n}}</button> <button type="submit" appBlurClick *ngIf="!hideInput">{{ "unlock" | i18n }}</button>
</div>
</header>
<content>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput">
<div class="row-main" *ngIf="pinLock">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="!pinLock">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button>
</div>
</div> </div>
</header> </div>
<content> <div class="box-footer">
<div class="box"> <p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box-content"> {{ "loggedInAsOn" | i18n: email:webVaultHostname }}
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput"> </div>
<div class="row-main" *ngIf="pinLock"> </div>
<label for="pin">{{'pin' | i18n}}</label> <div class="box" *ngIf="biometricLock">
<input id="pin" type="{{showPassword ? 'text' : 'password'}}" name="PIN" class="monospaced" <div class="box-footer">
[(ngModel)]="pin" required appInputVerbatim> <button type="button" class="btn primary block" (click)="unlockBiometric()" appStopClick>
</div> {{ "unlockWithBiometrics" | i18n }}
<div class="row-main" *ngIf="!pinLock"> </button>
<label for="masterPassword">{{'masterPass' | i18n}}</label> </div>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" </div>
class="monospaced" [(ngModel)]="masterPassword" required appInputVerbatim> <p class="text-center">
</div> <button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
<div class="action-buttons"> </p>
<button type="button" class="row-btn" appStopClick appBlurClick </content>
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"
aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="box-footer">
<p>{{'yourVaultIsLocked' | i18n}}</p>
{{'loggedInAsOn' | i18n : email : webVaultHostname}}
</div>
</div>
<div class="box" *ngIf="biometricLock">
<div class="box-footer">
<button type="button" class="btn primary block" (click)="unlockBiometric()"
appStopClick>{{'unlockWithBiometrics' | i18n}}</button>
</div>
</div>
<p class="text-center">
<button type="button" appStopClick (click)="logOut()">{{'logOut' | i18n}}</button>
</p>
</content>
</form> </form>

View File

@ -1,87 +1,107 @@
import { import { Component, NgZone } from "@angular/core";
Component, import { Router } from "@angular/router";
NgZone, import Swal from "sweetalert2";
} from '@angular/core';
import { Router } from '@angular/router';
import Swal from 'sweetalert2';
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { LockComponent as BaseLockComponent } from 'jslib-angular/components/lock.component'; import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component";
@Component({ @Component({
selector: 'app-lock', selector: "app-lock",
templateUrl: 'lock.component.html', templateUrl: "lock.component.html",
}) })
export class LockComponent extends BaseLockComponent { export class LockComponent extends BaseLockComponent {
private isInitialLockScreen: boolean; private isInitialLockScreen: boolean;
constructor(router: Router, i18nService: I18nService, constructor(
platformUtilsService: PlatformUtilsService, messagingService: MessagingService, router: Router,
userService: UserService, cryptoService: CryptoService, i18nService: I18nService,
storageService: StorageService, vaultTimeoutService: VaultTimeoutService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, stateService: StateService, messagingService: MessagingService,
apiService: ApiService, logService: LogService, keyConnectorService: KeyConnectorService, userService: UserService,
ngZone: NgZone) { cryptoService: CryptoService,
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService, storageService: StorageService,
storageService, vaultTimeoutService, environmentService, stateService, apiService, logService, vaultTimeoutService: VaultTimeoutService,
keyConnectorService, ngZone); environmentService: EnvironmentService,
this.successRoute = '/tabs/current'; stateService: StateService,
this.isInitialLockScreen = (window as any).previousPopupUrl == null; apiService: ApiService,
} logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone
) {
super(
router,
i18nService,
platformUtilsService,
messagingService,
userService,
cryptoService,
storageService,
vaultTimeoutService,
environmentService,
stateService,
apiService,
logService,
keyConnectorService,
ngZone
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
}
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
const disableAutoBiometricsPrompt = await this.storageService.get<boolean>( const disableAutoBiometricsPrompt =
ConstantsService.disableAutoBiometricsPromptKey) ?? true; (await this.storageService.get<boolean>(ConstantsService.disableAutoBiometricsPromptKey)) ??
true;
window.setTimeout(async () => { window.setTimeout(async () => {
document.getElementById(this.pinLock ? 'pin' : 'masterPassword').focus(); document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) { if (this.biometricLock && !disableAutoBiometricsPrompt && this.isInitialLockScreen) {
if (await this.vaultTimeoutService.isLocked()) { if (await this.vaultTimeoutService.isLocked()) {
await this.unlockBiometric(); await this.unlockBiometric();
}
}
}, 100);
}
async unlockBiometric(): Promise<boolean> {
if (!this.biometricLock) {
return;
} }
}
}, 100);
}
const div = document.createElement('div'); async unlockBiometric(): Promise<boolean> {
div.innerHTML = `<div class="swal2-text">${this.i18nService.t('awaitDesktop')}</div>`; if (!this.biometricLock) {
return;
Swal.fire({
heightAuto: false,
buttonsStyling: false,
html: div,
showCancelButton: true,
cancelButtonText: this.i18nService.t('cancel'),
showConfirmButton: false,
});
const success = await super.unlockBiometric();
// Avoid closing the error dialogs
if (success) {
Swal.close();
}
return success;
} }
const div = document.createElement("div");
div.innerHTML = `<div class="swal2-text">${this.i18nService.t("awaitDesktop")}</div>`;
Swal.fire({
heightAuto: false,
buttonsStyling: false,
html: div,
showCancelButton: true,
cancelButtonText: this.i18nService.t("cancel"),
showConfirmButton: false,
});
const success = await super.unlockBiometric();
// Avoid closing the error dialogs
if (success) {
Swal.close();
}
return success;
}
} }

View File

@ -1,47 +1,71 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/home">{{'cancel' | i18n}}</a> <a routerLink="/home">{{ "cancel" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ "appName" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{ "login" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
inputmode="email"
appInputVerbatim="false"
/>
</div> </div>
<h1 class="center"> <div class="box-content-row box-content-row-flex" appBoxRow>
<span class="title">{{'appName' | i18n}}</span> <div class="row-main">
</h1> <label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="right"> <input
<button type="submit" appBlurClick [disabled]="form.loading"> id="masterPassword"
<span [hidden]="form.loading">{{'login' | i18n}}</span> type="{{ showPassword ? 'text' : 'password' }}"
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
aria-hidden="true"
></i>
</button> </button>
</div>
</div> </div>
</header> <div class="box-content-row" [hidden]="!showCaptcha()">
<content> <iframe id="hcaptcha_iframe" height="80"></iframe>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" type="text" name="Email" [(ngModel)]="email" required inputmode="email"
appInputVerbatim="false">
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword"
class="monospaced" [(ngModel)]="masterPassword" required appInputVerbatim>
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"
aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row" [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
</div>
</div> </div>
<p class="text-center"> </div>
<a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a> </div>
</p> <p class="text-center">
</content> <a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
</p>
</content>
</form> </form>

View File

@ -1,42 +1,58 @@
import { import { Component, NgZone } from "@angular/core";
Component, import { Router } from "@angular/router";
NgZone,
} from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { LoginComponent as BaseLoginComponent } from 'jslib-angular/components/login.component'; import { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component";
@Component({ @Component({
selector: 'app-login', selector: "app-login",
templateUrl: 'login.component.html', templateUrl: "login.component.html",
}) })
export class LoginComponent extends BaseLoginComponent { export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router, constructor(
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, authService: AuthService,
protected stateService: StateService, protected environmentService: EnvironmentService, router: Router,
protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService,
protected cryptoFunctionService: CryptoFunctionService, storageService: StorageService, protected i18nService: I18nService,
syncService: SyncService, logService: LogService, ngZone: NgZone) { protected stateService: StateService,
super(authService, router, platformUtilsService, i18nService, stateService, environmentService, protected environmentService: EnvironmentService,
passwordGenerationService, cryptoFunctionService, storageService, logService, ngZone); protected passwordGenerationService: PasswordGenerationService,
super.onSuccessfulLogin = async () => { protected cryptoFunctionService: CryptoFunctionService,
await syncService.fullSync(true); storageService: StorageService,
}; syncService: SyncService,
super.successRoute = '/tabs/vault'; logService: LogService,
} ngZone: NgZone
) {
super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
storageService,
logService,
ngZone
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
super.successRoute = "/tabs/vault";
}
settings() { settings() {
this.router.navigate(['environment']); this.router.navigate(["environment"]);
} }
} }

View File

@ -1,100 +1,160 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/home">{{'cancel' | i18n}}</a> <a routerLink="/home">{{ "cancel" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ "createAccount" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
</div> </div>
<h1 class="center"> <div class="box-content-row" appBoxRow>
<span class="title">{{'createAccount' | i18n}}</span> <div class="box-content-row-flex">
</h1> <div class="row-main">
<div class="right"> <label for="masterPassword">
<button type="submit" appBlurClick [disabled]="form.loading"> {{ "masterPass" | i18n }}
<span [hidden]="form.loading">{{'submit' | i18n}}</span> <strong
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> class="sub-label text-{{ masterPasswordScoreColor }}"
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="email !== ''"
appInputVerbatim
(input)="updatePasswordStrength()"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="progress">
<div
class="progress-bar bg-{{ masterPasswordScoreColor }}"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div>
</div>
<div class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button> </button>
</div>
</div> </div>
</header> <div class="box-content-row" appBoxRow>
<content> <label for="hint">{{ "masterPassHint" | i18n }}</label>
<div class="box"> <input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" type="text" name="Email" [(ngModel)]="email" required
[appAutofocus]="email === ''" inputmode="email" appInputVerbatim="false">
</div>
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{'masterPass' | i18n}}
<strong class="sub-label text-{{masterPasswordScoreColor}}"
*ngIf="masterPasswordScoreText">
{{masterPasswordScoreText}}
</strong>
</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="monospaced" [(ngModel)]="masterPassword" required
[appAutofocus]="email !== ''" appInputVerbatim (input)="updatePasswordStrength()">
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="progress">
<div class="progress-bar bg-{{masterPasswordScoreColor}}" role="progressbar" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100" [ngStyle]="{width: (masterPasswordScoreWidth + '%')}"
attr.aria-valuenow="{{masterPasswordScoreWidth}}"></div>
</div>
</div>
</div>
<div class="box-footer">
{{'masterPassDesc' | i18n}}
</div>
</div> </div>
<div class="box"> </div>
<div class="box-content"> <div class="box-footer">
<div class="box-content-row box-content-row-flex" appBoxRow> {{ "masterPassHintDesc" | i18n }}
<div class="row-main"> </div>
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label> </div>
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}" <div [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div>
name="MasterPasswordRetype" class="monospaced" [(ngModel)]="confirmMasterPassword" required <div class="box last" *ngIf="showTerms">
appInputVerbatim> <div class="box-content">
</div> <div
<div class="action-buttons"> class="box-content-row box-content-row-checkbox box-content-row-checkbox-left box-content-row-word-break"
<button type="button" class="row-btn" appStopClick appBlurClick appBoxRow
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)" [attr.aria-pressed]="showPassword"> >
<i class="fa fa-lg" aria-hidden="true" <input
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i> type="checkbox"
</button> id="acceptPolicies"
</div> [(ngModel)]="acceptPolicies"
</div> name="AcceptPolicies"
<div class="box-content-row" appBoxRow> />
<label for="hint">{{'masterPassHint' | i18n}}</label> <label for="acceptPolicies">
<input id="hint" type="text" name="Hint" [(ngModel)]="hint"> {{ "acceptPolicies" | i18n }}<br />
</div> <a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
</div> "termsOfService" | i18n
<div class="box-footer"> }}</a
{{'masterPassHintDesc' | i18n}} >,
</div> <a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div> </div>
<div [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div> </div>
<div class="box last" *ngIf="showTerms"> </div>
<div class="box-content"> </content>
<div class="box-content-row box-content-row-checkbox box-content-row-checkbox-left box-content-row-word-break"
appBoxRow>
<input type="checkbox" id="acceptPolicies" [(ngModel)]="acceptPolicies" name="AcceptPolicies">
<label for="acceptPolicies">
{{'acceptPolicies' | i18n}}<br>
<a href="https://bitwarden.com/terms/" target="_blank"
rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank"
rel="noopener">{{'privacyPolicy' | i18n}}</a>
</label>
</div>
</div>
</div>
</content>
</form> </form>

View File

@ -1,29 +1,46 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { RegisterComponent as BaseRegisterComponent } from 'jslib-angular/components/register.component'; import { RegisterComponent as BaseRegisterComponent } from "jslib-angular/components/register.component";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
@Component({ @Component({
selector: 'app-register', selector: "app-register",
templateUrl: 'register.component.html', templateUrl: "register.component.html",
}) })
export class RegisterComponent extends BaseRegisterComponent { export class RegisterComponent extends BaseRegisterComponent {
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, cryptoService: CryptoService, authService: AuthService,
apiService: ApiService, stateService: StateService, platformUtilsService: PlatformUtilsService, router: Router,
passwordGenerationService: PasswordGenerationService, environmentService: EnvironmentService, i18nService: I18nService,
logService: LogService) { cryptoService: CryptoService,
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService, apiService: ApiService,
passwordGenerationService, environmentService, logService); stateService: StateService,
} platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
} }

View File

@ -1,29 +1,49 @@
<header> <header>
<div class="left"></div> <div class="left"></div>
<div class="center"> <div class="center">
<span class="title">{{'removeMasterPassword' | i18n}}</span> <span class="title">{{ "removeMasterPassword" | i18n }}</span>
</div> </div>
<div class="right"></div> <div class="right"></div>
</header> </header>
<content> <content>
<div class="box"> <div class="box">
<div class="box-content"> <div class="box-content">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<p>{{'convertOrganizationEncryptionDesc' | i18n : organization.name}}</p> <p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
</div> </div>
<div class="box-content-row"> <div class="box-content-row">
<button type="button" class="btn block primary" (click)="convert()" [disabled]="actionPromise"> <button
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="continuing"></i> type="button"
{{'removeMasterPassword' | i18n}} class="btn block primary"
</button> (click)="convert()"
</div> [disabled]="actionPromise"
<div class="box-content-row"> >
<button type="button" class="btn btn-outline-secondary block" (click)="leave()" [disabled]="actionPromise"> <i
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="leaving"></i> class="fa fa-spinner fa-spin"
{{'leaveOrganization' | i18n}} title="{{ 'loading' | i18n }}"
</button> aria-hidden="true"
</div> *ngIf="continuing"
</div> ></i>
{{ "removeMasterPassword" | i18n }}
</button>
</div>
<div class="box-content-row">
<button
type="button"
class="btn btn-outline-secondary block"
(click)="leave()"
[disabled]="actionPromise"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
</div>
</div> </div>
</div>
</content> </content>

View File

@ -1,10 +1,9 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from 'jslib-angular/components/remove-password.component'; import { RemovePasswordComponent as BaseRemovePasswordComponent } from "jslib-angular/components/remove-password.component";
@Component({ @Component({
selector: 'app-remove-password', selector: "app-remove-password",
templateUrl: 'remove-password.component.html', templateUrl: "remove-password.component.html",
}) })
export class RemovePasswordComponent extends BaseRemovePasswordComponent { export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
}

View File

@ -1,100 +1,150 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/home">{{'cancel' | i18n}}</a> <a routerLink="/home">{{ "cancel" | i18n }}</a>
</div> </div>
<h1 class="center"> <h1 class="center">
<span class="title">{{'setMasterPassword' | i18n}}</span> <span class="title">{{ "setMasterPassword" | i18n }}</span>
</h1> </h1>
<div class="right"> <div class="right">
<button type="submit" appBlurClick [disabled]="form.loading"> <button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{'submit' | i18n}}</span> <span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> <i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button> </button>
</div> </div>
</header> </header>
<content> <content>
<div class="full-loading-spinner" *ngIf="syncLoading"> <div class="full-loading-spinner" *ngIf="syncLoading">
<i class="fa fa-spinner fa-spin fa-3x" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin fa-3x" aria-hidden="true"></i>
</div> </div>
<div *ngIf="!syncLoading"> <div *ngIf="!syncLoading">
<div class="box"> <div class="box">
<app-callout type="tip">{{'ssoCompleteRegistration' | i18n}}</app-callout> <app-callout type="tip">{{ "ssoCompleteRegistration" | i18n }}</app-callout>
<app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}" <app-callout
*ngIf="resetPasswordAutoEnroll"> type="warning"
{{'resetPasswordAutoEnrollInviteWarning' | i18n}} title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
</app-callout> *ngIf="resetPasswordAutoEnroll"
<app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions"> >
</app-callout> {{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword"
>{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ masterPasswordScoreColor }}"
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
(input)="updatePasswordStrength()"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
role="button"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div> </div>
<div class="box"> <div class="progress">
<div class="box-content"> <div
<div class="box-content-row" appBoxRow> class="progress-bar bg-{{ masterPasswordScoreColor }}"
<div class="box-content-row-flex"> role="progressbar"
<div class="row-main"> aria-valuenow="0"
<label for="masterPassword">{{'masterPass' | i18n}} aria-valuemin="0"
<strong class="sub-label text-{{masterPasswordScoreColor}}" aria-valuemax="100"
*ngIf="masterPasswordScoreText"> [ngStyle]="{ width: masterPasswordScoreWidth + '%' }"
{{masterPasswordScoreText}} attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
</strong> ></div>
</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="monospaced" [(ngModel)]="masterPassword" required
(input)="updatePasswordStrength()" appInputVerbatim>
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="progress">
<div class="progress-bar bg-{{masterPasswordScoreColor}}" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
[ngStyle]="{width: (masterPasswordScoreWidth + '%')}"
attr.aria-valuenow="{{masterPasswordScoreWidth}}">
</div>
</div>
</div>
</div>
<div class="box-footer">
{{'masterPassDesc' | i18n}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="monospaced" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint">
</div>
</div>
<div class="box-footer">
{{'masterPassHintDesc' | i18n}}
</div>
</div> </div>
</div>
</div> </div>
</content> <div class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
role="button"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
</div>
</div>
<div class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
</div>
</content>
</form> </form>

View File

@ -1,65 +1,79 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component";
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib-angular/components/set-password.component';
@Component({ @Component({
selector: 'app-set-password', selector: "app-set-password",
templateUrl: 'set-password.component.html', templateUrl: "set-password.component.html",
}) })
export class SetPasswordComponent extends BaseSetPasswordComponent { export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor(apiService: ApiService, i18nService: I18nService, constructor(
cryptoService: CryptoService, messagingService: MessagingService, apiService: ApiService,
userService: UserService, passwordGenerationService: PasswordGenerationService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router, cryptoService: CryptoService,
syncService: SyncService, route: ActivatedRoute) { messagingService: MessagingService,
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, userService: UserService,
platformUtilsService, policyService, router, apiService, syncService, route); passwordGenerationService: PasswordGenerationService,
} platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
router: Router,
syncService: SyncService,
route: ActivatedRoute
) {
super(
i18nService,
cryptoService,
messagingService,
userService,
passwordGenerationService,
platformUtilsService,
policyService,
router,
apiService,
syncService,
route
);
}
get masterPasswordScoreWidth() { get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
} }
get masterPasswordScoreColor() { get masterPasswordScoreColor() {
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return 'success'; return "success";
case 3: case 3:
return 'primary'; return "primary";
case 2: case 2:
return 'warning'; return "warning";
default: default:
return 'danger'; return "danger";
}
} }
}
get masterPasswordScoreText() { get masterPasswordScoreText() {
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return this.i18nService.t('strong'); return this.i18nService.t("strong");
case 3: case 3:
return this.i18nService.t('good'); return this.i18nService.t("good");
case 2: case 2:
return this.i18nService.t('weak'); return this.i18nService.t("weak");
default: default:
return this.masterPasswordScore != null ? this.i18nService.t('weak') : null; return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
} }
}
} }

View File

@ -1,55 +1,73 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.component'; import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component";
import { BrowserApi } from '../../browser/browserApi'; import { BrowserApi } from "../../browser/browserApi";
@Component({ @Component({
selector: 'app-sso', selector: "app-sso",
templateUrl: 'sso.component.html', templateUrl: "sso.component.html",
}) })
export class SsoComponent extends BaseSsoComponent { export class SsoComponent extends BaseSsoComponent {
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, route: ActivatedRoute, authService: AuthService,
storageService: StorageService, stateService: StateService, router: Router,
platformUtilsService: PlatformUtilsService, apiService: ApiService, i18nService: I18nService,
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService, route: ActivatedRoute,
syncService: SyncService, environmentService: EnvironmentService, logService: LogService, storageService: StorageService,
private vaultTimeoutService: VaultTimeoutService) { stateService: StateService,
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService, platformUtilsService: PlatformUtilsService,
apiService, cryptoFunctionService, environmentService, passwordGenerationService, logService); apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService,
syncService: SyncService,
environmentService: EnvironmentService,
logService: LogService,
private vaultTimeoutService: VaultTimeoutService
) {
super(
authService,
router,
i18nService,
route,
storageService,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
);
const url = this.environmentService.getWebVaultUrl(); const url = this.environmentService.getWebVaultUrl();
this.redirectUri = url + '/sso-connector.html'; this.redirectUri = url + "/sso-connector.html";
this.clientId = 'browser'; this.clientId = "browser";
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
await syncService.fullSync(true); await syncService.fullSync(true);
if (await this.vaultTimeoutService.isLocked()) { if (await this.vaultTimeoutService.isLocked()) {
// If the vault is unlocked then this will clear keys from memory, which we don't want to do // If the vault is unlocked then this will clear keys from memory, which we don't want to do
BrowserApi.reloadOpenWindows(); BrowserApi.reloadOpenWindows();
} }
const thisWindow = window.open('', '_self'); const thisWindow = window.open("", "_self");
thisWindow.close(); thisWindow.close();
}; };
} }
} }

View File

@ -1,23 +1,29 @@
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/2fa">{{'close' | i18n}}</a> <a routerLink="/2fa">{{ "close" | i18n }}</a>
</div> </div>
<h1 class="center"> <h1 class="center">
<span class="title">{{'twoStepOptions' | i18n}}</span> <span class="title">{{ "twoStepOptions" | i18n }}</span>
</h1> </h1>
<div class="right"></div> <div class="right"></div>
</header> </header>
<content> <content>
<div class="box"> <div class="box">
<div class="box-content"> <div class="box-content">
<button type="button" appStopClick *ngFor="let p of providers" class="box-content-row" (click)="choose(p)"> <button
<span class="text">{{p.name}}</span> type="button"
<span class="detail">{{p.description}}</span> appStopClick
</button> *ngFor="let p of providers"
<button type="button" appStopClick class="box-content-row" (click)="recover()"> class="box-content-row"
<span class="text">{{'recoveryCodeTitle' | i18n}}</span> (click)="choose(p)"
<span class="detail">{{'recoveryCodeDesc' | i18n}}</span> >
</button> <span class="text">{{ p.name }}</span>
</div> <span class="detail">{{ p.description }}</span>
</button>
<button type="button" appStopClick class="box-content-row" (click)="recover()">
<span class="text">{{ "recoveryCodeTitle" | i18n }}</span>
<span class="detail">{{ "recoveryCodeDesc" | i18n }}</span>
</button>
</div> </div>
</div>
</content> </content>

View File

@ -1,27 +1,29 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "jslib-angular/components/two-factor-options.component";
TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent,
} from 'jslib-angular/components/two-factor-options.component';
@Component({ @Component({
selector: 'app-two-factor-options', selector: "app-two-factor-options",
templateUrl: 'two-factor-options.component.html', templateUrl: "two-factor-options.component.html",
}) })
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, platformUtilsService: PlatformUtilsService) { authService: AuthService,
super(authService, router, i18nService, platformUtilsService, window); router: Router,
} i18nService: I18nService,
platformUtilsService: PlatformUtilsService
) {
super(authService, router, i18nService, platformUtilsService, window);
}
choose(p: any) { choose(p: any) {
super.choose(p); super.choose(p);
this.authService.selectedTwoFactorProviderType = p.type; this.authService.selectedTwoFactorProviderType = p.type;
this.router.navigate(['2fa']); this.router.navigate(["2fa"]);
} }
} }

View File

@ -1,106 +1,141 @@
<form id="two-factor-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form id="two-factor-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a routerLink="/login">{{'back' | i18n}}</a> <a routerLink="/login">{{ "back" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button
type="submit"
appBlurClick
[disabled]="form.loading"
*ngIf="
selectedProviderType != null &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo &&
(selectedProviderType !== providerType.WebAuthn || form.loading)
"
>
<span [hidden]="form.loading">{{ "continue" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<ng-container
*ngIf="
selectedProviderType === providerType.Authenticator ||
selectedProviderType === providerType.Email
"
>
<div class="content text-center">
<span *ngIf="selectedProviderType === providerType.Authenticator">
{{ "enterVerificationCodeApp" | i18n }}
</span>
<span *ngIf="selectedProviderType === providerType.Email">
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
</span>
</div>
<div class="box first">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="text"
name="Code"
[(ngModel)]="token"
required
appAutofocus
inputmode="tel"
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div> </div>
<h1 class="center"> </div>
<span class="title">{{title}}</span> </ng-container>
</h1> <ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<div class="right"> <div class="content text-center">
<button type="submit" appBlurClick [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo && <p class="text-center">{{ "insertYubiKey" | i18n }}</p>
selectedProviderType !== providerType.OrganizationDuo && <img src="../images/yubikey.jpg" class="img-rounded img-responsive" alt="" />
(selectedProviderType !== providerType.WebAuthn || form.loading)"> </div>
<span [hidden]="form.loading">{{'continue' | i18n}}</span> <div class="box first">
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> <div class="box-content">
</button> <div class="box-content-row" appBoxRow>
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="password"
name="Code"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div> </div>
</header> </div>
<content> </ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Authenticator || <ng-container *ngIf="selectedProviderType === providerType.WebAuthn && !webAuthnNewTab">
selectedProviderType === providerType.Email"> <div id="web-authn-frame"><iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe></div>
<div class="content text-center"> <div class="box">
<span *ngIf="selectedProviderType === providerType.Authenticator"> <div class="box-content">
{{'enterVerificationCodeApp' | i18n}} <div class="box-content-row box-content-row-checkbox" appBoxRow>
</span> <label for="remember">{{ "rememberMe" | i18n }}</label>
<span *ngIf="selectedProviderType === providerType.Email"> <input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}} </div>
</span>
</div>
<div class="box first">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" [(ngModel)]="token" required appAutofocus
inputmode="tel" appInputVerbatim>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{'rememberMe' | i18n}}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<div class="content text-center">
<p class="text-center">{{'insertYubiKey' | i18n}}</p>
<img src="../images/yubikey.jpg" class="img-rounded img-responsive" alt="">
</div>
<div class="box first">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" [(ngModel)]="token" required appAutofocus
appInputVerbatim>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{'rememberMe' | i18n}}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn && !webAuthnNewTab">
<div id="web-authn-frame"><iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe></div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{'rememberMe' | i18n}}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn && webAuthnNewTab">
<div class="content text-center" *ngIf="webAuthnNewTab">
<p class="text-center">{{'webAuthnNewTab' | i18n}}</p>
<button type="button" class="btn primary block" (click)="authWebAuthn()" appStopClick>{{'webAuthnNewTabOpen' | i18n}}</button>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo">
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{'rememberMe' | i18n}}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
</div>
</div>
</div>
</ng-container>
<div class="content" *ngIf="selectedProviderType == null">
<p class="text-center">{{'noTwoStepProviders' | i18n}}</p>
<p class="text-center">{{'noTwoStepProviders2' | i18n}}</p>
</div> </div>
<div class="content no-vpad" *ngIf="selectedProviderType != null"> </div>
<p class="text-center"> </ng-container>
<button type="button" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</button> <ng-container *ngIf="selectedProviderType === providerType.WebAuthn && webAuthnNewTab">
</p> <div class="content text-center" *ngIf="webAuthnNewTab">
<p *ngIf="selectedProviderType === providerType.Email" class="text-center"> <p class="text-center">{{ "webAuthnNewTab" | i18n }}</p>
<button type="button" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"> <button type="button" class="btn primary block" (click)="authWebAuthn()" appStopClick>
{{'sendVerificationCodeEmailAgain' | i18n}} {{ "webAuthnNewTabOpen" | i18n }}
</button> </button>
</p> </div>
</ng-container>
<ng-container
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div> </div>
</content> </div>
</ng-container>
<div class="content" *ngIf="selectedProviderType == null">
<p class="text-center">{{ "noTwoStepProviders" | i18n }}</p>
<p class="text-center">{{ "noTwoStepProviders2" | i18n }}</p>
</div>
<div class="content no-vpad" *ngIf="selectedProviderType != null">
<p class="text-center">
<button type="button" appStopClick (click)="anotherMethod()">
{{ "useAnotherTwoStepMethod" | i18n }}
</button>
</p>
<p *ngIf="selectedProviderType === providerType.Email" class="text-center">
<button type="button" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise">
{{ "sendVerificationCodeEmailAgain" | i18n }}
</button>
</p>
</div>
</content>
</form> </form>

View File

@ -1,116 +1,140 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute, import { first } from "rxjs/operators";
Router,
} from '@angular/router';
import { first } from 'rxjs/operators';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib-angular/components/two-factor.component'; import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
import { PopupUtilsService } from '../services/popup-utils.service'; import { PopupUtilsService } from "../services/popup-utils.service";
import { BrowserApi } from '../../browser/browserApi'; import { BrowserApi } from "../../browser/browserApi";
const BroadcasterSubscriptionId = 'TwoFactorComponent'; const BroadcasterSubscriptionId = "TwoFactorComponent";
@Component({ @Component({
selector: 'app-two-factor', selector: "app-two-factor",
templateUrl: 'two-factor.component.html', templateUrl: "two-factor.component.html",
}) })
export class TwoFactorComponent extends BaseTwoFactorComponent { export class TwoFactorComponent extends BaseTwoFactorComponent {
showNewWindowMessage = false; showNewWindowMessage = false;
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, apiService: ApiService, authService: AuthService,
platformUtilsService: PlatformUtilsService, private syncService: SyncService, router: Router,
environmentService: EnvironmentService, private broadcasterService: BroadcasterService, i18nService: I18nService,
private popupUtilsService: PopupUtilsService, stateService: StateService, apiService: ApiService,
storageService: StorageService, route: ActivatedRoute, private messagingService: MessagingService, platformUtilsService: PlatformUtilsService,
logService: LogService) { private syncService: SyncService,
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, environmentService: EnvironmentService,
stateService, storageService, route, logService); private broadcasterService: BroadcasterService,
private popupUtilsService: PopupUtilsService,
stateService: StateService,
storageService: StorageService,
route: ActivatedRoute,
private messagingService: MessagingService,
logService: LogService
) {
super(
authService,
router,
i18nService,
apiService,
platformUtilsService,
window,
environmentService,
stateService,
storageService,
route,
logService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
super.successRoute = "/tabs/vault";
this.webAuthnNewTab =
this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari();
}
async ngOnInit() {
if (this.route.snapshot.paramMap.has("webAuthnResponse")) {
// WebAuthn fallback response
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
super.onSuccessfulLogin = async () => {
this.syncService.fullSync(true);
this.messagingService.send("reloadPopup");
window.close();
};
this.remember = this.route.snapshot.paramMap.get("remember") === "true";
await this.doSubmit();
return;
}
await super.ngOnInit();
if (this.selectedProviderType == null) {
return;
}
// WebAuthn prompt appears inside the popup on linux, and requires a larger popup width
// than usual to avoid cutting off the dialog.
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) {
document.body.classList.add("linux-webauthn");
}
if (
this.selectedProviderType === TwoFactorProviderType.Email &&
this.popupUtilsService.inPopup(window)
) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("popup2faCloseMessage"),
null,
this.i18nService.t("yes"),
this.i18nService.t("no")
);
if (confirmed) {
this.popupUtilsService.popOut(window);
}
}
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.sso === "true") {
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
return syncService.fullSync(true); BrowserApi.reloadOpenWindows();
const thisWindow = window.open("", "_self");
thisWindow.close();
return this.syncService.fullSync(true);
}; };
super.successRoute = '/tabs/vault'; }
this.webAuthnNewTab = this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari(); });
}
async ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) {
document.body.classList.remove("linux-webauthn");
} }
super.ngOnDestroy();
}
async ngOnInit() { anotherMethod() {
if (this.route.snapshot.paramMap.has('webAuthnResponse')) { this.router.navigate(["2fa-options"]);
// WebAuthn fallback response }
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get('webAuthnResponse');
super.onSuccessfulLogin = async () => {
this.syncService.fullSync(true);
this.messagingService.send('reloadPopup');
window.close();
};
this.remember = this.route.snapshot.paramMap.get('remember') === 'true';
await this.doSubmit();
return;
}
await super.ngOnInit(); async isLinux() {
if (this.selectedProviderType == null) { return (await BrowserApi.getPlatformInfo()).os === "linux";
return; }
}
// WebAuthn prompt appears inside the popup on linux, and requires a larger popup width
// than usual to avoid cutting off the dialog.
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && await this.isLinux()) {
document.body.classList.add('linux-webauthn');
}
if (this.selectedProviderType === TwoFactorProviderType.Email &&
this.popupUtilsService.inPopup(window)) {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('popup2faCloseMessage'),
null, this.i18nService.t('yes'), this.i18nService.t('no'));
if (confirmed) {
this.popupUtilsService.popOut(window);
}
}
this.route.queryParams.pipe(first()).subscribe(async qParams => {
if (qParams.sso === 'true') {
super.onSuccessfulLogin = () => {
BrowserApi.reloadOpenWindows();
const thisWindow = window.open('', '_self');
thisWindow.close();
return this.syncService.fullSync(true);
};
}
});
}
async ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && await this.isLinux()) {
document.body.classList.remove('linux-webauthn');
}
super.ngOnDestroy();
}
anotherMethod() {
this.router.navigate(['2fa-options']);
}
async isLinux() {
return (await BrowserApi.getPlatformInfo()).os === 'linux';
}
} }

View File

@ -1,86 +1,130 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<a (click)="logOut()">{{'logOut' | i18n}}</a> <a (click)="logOut()">{{ "logOut" | i18n }}</a>
</div>
<h1 class="center">
<span class="title">{{ "updateMasterPassword" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" appBlurClick [disabled]="form.loading">
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ "updateMasterPasswordWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ masterPasswordScoreStyle.Color }}"
*ngIf="masterPasswordScoreStyle.Text"
>
{{ masterPasswordScoreStyle.Text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
(input)="updatePasswordStrength()"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="progress">
<div
class="progress-bar bg-{{ masterPasswordScoreStyle.Color }}"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
[ngStyle]="{ width: masterPasswordScoreStyle.Width + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreStyle.Width }}"
></div>
</div>
</div> </div>
<h1 class="center"> </div>
<span class="title">{{'updateMasterPassword' | i18n}}</span> </div>
</h1> <div class="box">
<div class="right"> <div class="box-content">
<button type="submit" appBlurClick [disabled]="form.loading"> <div class="box-content-row box-content-row-flex" appBoxRow>
<span [hidden]="form.loading">{{'submit' | i18n}}</span> <div class="row-main">
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading" aria-hidden="true"></i> <label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
[attr.aria-pressed]="showPassword"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button> </button>
</div>
</div> </div>
</header> </div>
<content> </div>
<app-callout type="warning" title="{{'updateMasterPassword' | i18n}}"> <div class="box">
{{'updateMasterPasswordWarning' | i18n}} <div class="box-content">
</app-callout> <div class="box-content-row" appBoxRow>
<app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions"> <label for="hint">{{ "masterPassHint" | i18n }}</label>
</app-callout> <input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{'masterPass' | i18n}}
<strong class="sub-label text-{{masterPasswordScoreStyle.Color}}"
*ngIf="masterPasswordScoreStyle.Text">
{{masterPasswordScoreStyle.Text}}
</strong>
</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="monospaced" [(ngModel)]="masterPassword" required
appInputVerbatim (input)="updatePasswordStrength()">
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="progress">
<div class="progress-bar bg-{{masterPasswordScoreStyle.Color}}" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
[ngStyle]="{width: (masterPasswordScoreStyle.Width + '%')}"
attr.aria-valuenow="{{masterPasswordScoreStyle.Width}}"></div>
</div>
</div>
</div>
</div> </div>
<div class="box"> </div>
<div class="box-content"> <div class="box-footer">
<div class="box-content-row box-content-row-flex" appBoxRow> {{ "masterPassHintDesc" | i18n }}
<div class="row-main"> </div>
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label> </div>
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}" </content>
name="MasterPasswordRetype" class="monospaced" [(ngModel)]="masterPasswordRetype" required
appInputVerbatim>
</div>
<div class="action-buttons">
<button type="button" class="row-btn" appStopClick appBlurClick
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)" [attr.aria-pressed]="showPassword">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint">
</div>
</div>
<div class="box-footer">
{{'masterPassHintDesc' | i18n}}
</div>
</div>
</content>
</form> </form>

View File

@ -1,65 +1,82 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from 'jslib-angular/components/update-temp-password.component'; import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
interface MasterPasswordScore { interface MasterPasswordScore {
Color: string; Color: string;
Text: string; Text: string;
Width: number; Width: number;
} }
@Component({ @Component({
selector: 'app-update-temp-password', selector: "app-update-temp-password",
templateUrl: 'update-temp-password.component.html', templateUrl: "update-temp-password.component.html",
}) })
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
get masterPasswordScoreStyle(): MasterPasswordScore { get masterPasswordScoreStyle(): MasterPasswordScore {
const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) { switch (this.masterPasswordScore) {
case 4: case 4:
return { return {
Color: 'bg-success', Color: "bg-success",
Text: 'strong', Text: "strong",
Width: scoreWidth, Width: scoreWidth,
}; };
case 3: case 3:
return { return {
Color: 'bg-primary', Color: "bg-primary",
Text: 'good', Text: "good",
Width: scoreWidth, Width: scoreWidth,
}; };
case 2: case 2:
return { return {
Color: 'bg-warning', Color: "bg-warning",
Text: 'weak', Text: "weak",
Width: scoreWidth, Width: scoreWidth,
}; };
default: default:
return { return {
Color: 'bg-danger', Color: "bg-danger",
Text: 'weak', Text: "weak",
Width: scoreWidth, Width: scoreWidth,
}; };
}
} }
}
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(
passwordGenerationService: PasswordGenerationService, policyService: PolicyService, i18nService: I18nService,
cryptoService: CryptoService, userService: UserService, platformUtilsService: PlatformUtilsService,
messagingService: MessagingService, apiService: ApiService, passwordGenerationService: PasswordGenerationService,
syncService: SyncService, logService: LogService) { policyService: PolicyService,
super(i18nService, platformUtilsService, passwordGenerationService, policyService, cryptoService, cryptoService: CryptoService,
userService, messagingService, apiService, syncService, logService); userService: UserService,
} messagingService: MessagingService,
apiService: ApiService,
syncService: SyncService,
logService: LogService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
userService,
messagingService,
apiService,
syncService,
logService
);
}
} }

View File

@ -1,199 +1,213 @@
import { import { animate, group, query, style, transition, trigger } from "@angular/animations";
animate,
group,
query,
style,
transition,
trigger,
} from '@angular/animations';
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
const queryShown = query(':enter, :leave', [ const queryShown = query(
style({ position: 'fixed', width: '100%', height: '100%' }), ":enter, :leave",
], { optional: true }); [style({ position: "fixed", width: "100%", height: "100%" })],
{
optional: true,
}
);
// ref: https://github.com/angular/angular/issues/15477 // ref: https://github.com/angular/angular/issues/15477
const queryChildRoute = query('router-outlet ~ *', [ const queryChildRoute = query("router-outlet ~ *", [style({}), animate(1, style({}))], {
style({}), optional: true,
animate(1, style({})), });
], { optional: true });
const speed = '0.4s'; const speed = "0.4s";
export function queryTranslate(direction: string, axis: string, from: number, to: number, zIndex: number = 1000) { export function queryTranslate(
return query(':' + direction, [ direction: string,
style({ transform: 'translate' + axis + '(' + from + '%)', zIndex: zIndex, boxShadow: '0 3px 2px -2px gray' }), axis: string,
animate(speed + ' ease-in-out', style({ transform: 'translate' + axis + '(' + to + '%)' })), from: number,
], { optional: true }); to: number,
zIndex: number = 1000
) {
return query(
":" + direction,
[
style({
transform: "translate" + axis + "(" + from + "%)",
zIndex: zIndex,
boxShadow: "0 3px 2px -2px gray",
}),
animate(speed + " ease-in-out", style({ transform: "translate" + axis + "(" + to + "%)" })),
],
{ optional: true }
);
} }
export function queryTranslateX(direction: string, from: number, to: number, zIndex: number = 1000) { export function queryTranslateX(
return queryTranslate(direction, 'X', from, to, zIndex); direction: string,
from: number,
to: number,
zIndex: number = 1000
) {
return queryTranslate(direction, "X", from, to, zIndex);
} }
export function queryTranslateY(direction: string, from: number, to: number, zIndex: number = 1000) { export function queryTranslateY(
return queryTranslate(direction, 'Y', from, to, zIndex); direction: string,
from: number,
to: number,
zIndex: number = 1000
) {
return queryTranslate(direction, "Y", from, to, zIndex);
} }
const inSlideLeft = [ const inSlideLeft = [
queryShown, queryShown,
group([ group([queryTranslateX("enter", 100, 0), queryTranslateX("leave", 0, -100), queryChildRoute]),
queryTranslateX('enter', 100, 0),
queryTranslateX('leave', 0, -100),
queryChildRoute,
]),
]; ];
const outSlideRight = [ const outSlideRight = [
queryShown, queryShown,
group([ group([queryTranslateX("enter", -100, 0), queryTranslateX("leave", 0, 100)]),
queryTranslateX('enter', -100, 0),
queryTranslateX('leave', 0, 100),
]),
]; ];
const inSlideUp = [ const inSlideUp = [
queryShown, queryShown,
group([ group([queryTranslateY("enter", 100, 0, 1010), queryTranslateY("leave", 0, 0), queryChildRoute]),
queryTranslateY('enter', 100, 0, 1010),
queryTranslateY('leave', 0, 0),
queryChildRoute,
]),
]; ];
const outSlideDown = [ const outSlideDown = [
queryShown, queryShown,
group([ group([queryTranslateY("enter", 0, 0), queryTranslateY("leave", 0, 100, 1010)]),
queryTranslateY('enter', 0, 0),
queryTranslateY('leave', 0, 100, 1010),
]),
]; ];
const inSlideDown = [ const inSlideDown = [
queryShown, queryShown,
group([ group([queryTranslateY("enter", -100, 0, 1010), queryTranslateY("leave", 0, 0), queryChildRoute]),
queryTranslateY('enter', -100, 0, 1010),
queryTranslateY('leave', 0, 0),
queryChildRoute,
]),
]; ];
const outSlideUp = [ const outSlideUp = [
queryShown, queryShown,
group([ group([queryTranslateY("enter", 0, 0), queryTranslateY("leave", 0, -100, 1010)]),
queryTranslateY('enter', 0, 0),
queryTranslateY('leave', 0, -100, 1010),
]),
]; ];
export function tabsToCiphers(fromState: string, toState: string) { export function tabsToCiphers(fromState: string, toState: string) {
if (fromState == null || toState === null || toState.indexOf('ciphers_') === -1) { if (fromState == null || toState === null || toState.indexOf("ciphers_") === -1) {
return false; return false;
} }
return (fromState.indexOf('ciphers_') === 0 && fromState.indexOf('ciphers_direction=b') === -1) || return (
fromState === 'tabs'; (fromState.indexOf("ciphers_") === 0 && fromState.indexOf("ciphers_direction=b") === -1) ||
fromState === "tabs"
);
} }
export function ciphersToTabs(fromState: string, toState: string) { export function ciphersToTabs(fromState: string, toState: string) {
if (fromState == null || toState === null || fromState.indexOf('ciphers_') === -1) { if (fromState == null || toState === null || fromState.indexOf("ciphers_") === -1) {
return false; return false;
} }
return toState.indexOf('ciphers_direction=b') === 0 || toState === 'tabs'; return toState.indexOf("ciphers_direction=b") === 0 || toState === "tabs";
} }
export function ciphersToView(fromState: string, toState: string) { export function ciphersToView(fromState: string, toState: string) {
if (fromState == null || toState === null) { if (fromState == null || toState === null) {
return false; return false;
} }
return fromState.indexOf('ciphers_') === 0 && return (
(toState === 'view-cipher' || toState === 'add-cipher' || toState === 'clone-cipher'); fromState.indexOf("ciphers_") === 0 &&
(toState === "view-cipher" || toState === "add-cipher" || toState === "clone-cipher")
);
} }
export function viewToCiphers(fromState: string, toState: string) { export function viewToCiphers(fromState: string, toState: string) {
if (fromState == null || toState === null) { if (fromState == null || toState === null) {
return false; return false;
} }
return (fromState === 'view-cipher' || fromState === 'add-cipher' || fromState === 'clone-cipher') && return (
toState.indexOf('ciphers_') === 0; (fromState === "view-cipher" || fromState === "add-cipher" || fromState === "clone-cipher") &&
toState.indexOf("ciphers_") === 0
);
} }
export const routerTransition = trigger('routerTransition', [ export const routerTransition = trigger("routerTransition", [
transition('void => home', inSlideLeft), transition("void => home", inSlideLeft),
transition('void => tabs', inSlideLeft), transition("void => tabs", inSlideLeft),
transition('home => environment, home => login, home => register', inSlideUp), transition("home => environment, home => login, home => register", inSlideUp),
transition('login => home', outSlideDown), transition("login => home", outSlideDown),
transition('login => hint', inSlideUp), transition("login => hint", inSlideUp),
transition('login => tabs, login => 2fa', inSlideLeft), transition("login => tabs, login => 2fa", inSlideLeft),
transition('hint => login, register => home, environment => home', outSlideDown), transition("hint => login, register => home, environment => home", outSlideDown),
transition('2fa => login', outSlideRight), transition("2fa => login", outSlideRight),
transition('2fa => 2fa-options', inSlideUp), transition("2fa => 2fa-options", inSlideUp),
transition('2fa-options => 2fa', outSlideDown), transition("2fa-options => 2fa", outSlideDown),
transition('2fa => tabs', inSlideLeft), transition("2fa => tabs", inSlideLeft),
transition(tabsToCiphers, inSlideLeft), transition(tabsToCiphers, inSlideLeft),
transition(ciphersToTabs, outSlideRight), transition(ciphersToTabs, outSlideRight),
transition(ciphersToView, inSlideUp), transition(ciphersToView, inSlideUp),
transition(viewToCiphers, outSlideDown), transition(viewToCiphers, outSlideDown),
transition('tabs => view-cipher', inSlideUp), transition("tabs => view-cipher", inSlideUp),
transition('view-cipher => tabs', outSlideDown), transition("view-cipher => tabs", outSlideDown),
transition('view-cipher => edit-cipher, view-cipher => cipher-password-history', inSlideUp), transition("view-cipher => edit-cipher, view-cipher => cipher-password-history", inSlideUp),
transition('edit-cipher => view-cipher, cipher-password-history => view-cipher, edit-cipher => tabs', outSlideDown), transition(
"edit-cipher => view-cipher, cipher-password-history => view-cipher, edit-cipher => tabs",
outSlideDown
),
transition('view-cipher => clone-cipher', inSlideUp), transition("view-cipher => clone-cipher", inSlideUp),
transition('clone-cipher => view-cipher, clone-cipher => tabs', outSlideDown), transition("clone-cipher => view-cipher, clone-cipher => tabs", outSlideDown),
transition('view-cipher => share-cipher', inSlideUp), transition("view-cipher => share-cipher", inSlideUp),
transition('share-cipher => view-cipher', outSlideDown), transition("share-cipher => view-cipher", outSlideDown),
transition('tabs => add-cipher', inSlideUp), transition("tabs => add-cipher", inSlideUp),
transition('add-cipher => tabs', outSlideDown), transition("add-cipher => tabs", outSlideDown),
transition('generator => generator-history, tabs => generator-history', inSlideLeft), transition("generator => generator-history, tabs => generator-history", inSlideLeft),
transition('generator-history => generator, generator-history => tabs', outSlideRight), transition("generator-history => generator, generator-history => tabs", outSlideRight),
transition('add-cipher => generator, edit-cipher => generator, clone-cipher => generator', inSlideUp), transition(
transition('generator => add-cipher, generator => edit-cipher, generator => clone-cipher', outSlideDown), "add-cipher => generator, edit-cipher => generator, clone-cipher => generator",
inSlideUp
),
transition(
"generator => add-cipher, generator => edit-cipher, generator => clone-cipher",
outSlideDown
),
transition('edit-cipher => attachments, edit-cipher => collections', inSlideLeft), transition("edit-cipher => attachments, edit-cipher => collections", inSlideLeft),
transition('attachments => edit-cipher, collections => edit-cipher', outSlideRight), transition("attachments => edit-cipher, collections => edit-cipher", outSlideRight),
transition('clone-cipher => attachments, clone-cipher => collections', inSlideLeft), transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft),
transition('attachments => clone-cipher, collections => clone-cipher', outSlideRight), transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight),
transition('tabs => export', inSlideLeft), transition("tabs => export", inSlideLeft),
transition('export => tabs', outSlideRight), transition("export => tabs", outSlideRight),
transition('tabs => folders', inSlideLeft), transition("tabs => folders", inSlideLeft),
transition('folders => tabs', outSlideRight), transition("folders => tabs", outSlideRight),
transition('folders => edit-folder, folders => add-folder', inSlideUp), transition("folders => edit-folder, folders => add-folder", inSlideUp),
transition('edit-folder => folders, add-folder => folders', outSlideDown), transition("edit-folder => folders, add-folder => folders", outSlideDown),
transition('tabs => sync', inSlideLeft), transition("tabs => sync", inSlideLeft),
transition('sync => tabs', outSlideRight), transition("sync => tabs", outSlideRight),
transition('tabs => options', inSlideLeft), transition("tabs => options", inSlideLeft),
transition('options => tabs', outSlideRight), transition("options => tabs", outSlideRight),
transition('tabs => premium', inSlideLeft), transition("tabs => premium", inSlideLeft),
transition('premium => tabs', outSlideRight), transition("premium => tabs", outSlideRight),
transition('tabs => lock', inSlideDown), transition("tabs => lock", inSlideDown),
transition('tabs => send-type', inSlideLeft), transition("tabs => send-type", inSlideLeft),
transition('send-type => tabs', outSlideRight), transition("send-type => tabs", outSlideRight),
transition('tabs => add-send, send-type => add-send', inSlideUp), transition("tabs => add-send, send-type => add-send", inSlideUp),
transition('add-send => tabs, add-send => send-type', outSlideDown), transition("add-send => tabs, add-send => send-type", outSlideDown),
transition('tabs => edit-send, send-type => edit-send', inSlideUp), transition("tabs => edit-send, send-type => edit-send", inSlideUp),
transition('edit-send => tabs, edit-send => send-type', outSlideDown), transition("edit-send => tabs, edit-send => send-type", outSlideDown),
]); ]);

View File

@ -1,355 +1,352 @@
import { Injectable, NgModule } from '@angular/core'; import { Injectable, NgModule } from "@angular/core";
import { import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
ActivatedRouteSnapshot,
RouteReuseStrategy,
RouterModule,
Routes,
} from '@angular/router';
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service'; import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
import { LockGuardService } from 'jslib-angular/services/lock-guard.service'; import { LockGuardService } from "jslib-angular/services/lock-guard.service";
import { DebounceNavigationService } from './services/debounceNavigationService'; import { DebounceNavigationService } from "./services/debounceNavigationService";
import { LaunchGuardService } from './services/launch-guard.service'; import { LaunchGuardService } from "./services/launch-guard.service";
import { EnvironmentComponent } from './accounts/environment.component'; import { EnvironmentComponent } from "./accounts/environment.component";
import { HintComponent } from './accounts/hint.component'; import { HintComponent } from "./accounts/hint.component";
import { HomeComponent } from './accounts/home.component'; import { HomeComponent } from "./accounts/home.component";
import { LockComponent } from './accounts/lock.component'; import { LockComponent } from "./accounts/lock.component";
import { LoginComponent } from './accounts/login.component'; import { LoginComponent } from "./accounts/login.component";
import { RegisterComponent } from './accounts/register.component'; import { RegisterComponent } from "./accounts/register.component";
import { RemovePasswordComponent } from './accounts/remove-password.component'; import { RemovePasswordComponent } from "./accounts/remove-password.component";
import { SetPasswordComponent } from './accounts/set-password.component'; import { SetPasswordComponent } from "./accounts/set-password.component";
import { SsoComponent } from './accounts/sso.component'; import { SsoComponent } from "./accounts/sso.component";
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorOptionsComponent } from "./accounts/two-factor-options.component";
import { TwoFactorComponent } from './accounts/two-factor.component'; import { TwoFactorComponent } from "./accounts/two-factor.component";
import { UpdateTempPasswordComponent } from './accounts/update-temp-password.component'; import { UpdateTempPasswordComponent } from "./accounts/update-temp-password.component";
import { PasswordGeneratorHistoryComponent } from './generator/password-generator-history.component'; import { PasswordGeneratorHistoryComponent } from "./generator/password-generator-history.component";
import { PasswordGeneratorComponent } from './generator/password-generator.component'; import { PasswordGeneratorComponent } from "./generator/password-generator.component";
import { PrivateModeComponent } from './private-mode.component'; import { PrivateModeComponent } from "./private-mode.component";
import { TabsComponent } from './tabs.component'; import { TabsComponent } from "./tabs.component";
import { ExcludedDomainsComponent } from './settings/excluded-domains.component'; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { ExportComponent } from './settings/export.component'; import { ExportComponent } from "./settings/export.component";
import { FolderAddEditComponent } from './settings/folder-add-edit.component'; import { FolderAddEditComponent } from "./settings/folder-add-edit.component";
import { FoldersComponent } from './settings/folders.component'; import { FoldersComponent } from "./settings/folders.component";
import { OptionsComponent } from './settings/options.component'; import { OptionsComponent } from "./settings/options.component";
import { PremiumComponent } from './settings/premium.component'; import { PremiumComponent } from "./settings/premium.component";
import { SettingsComponent } from './settings/settings.component'; import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from './settings/sync.component'; import { SyncComponent } from "./settings/sync.component";
import { AddEditComponent } from './vault/add-edit.component'; import { AddEditComponent } from "./vault/add-edit.component";
import { AttachmentsComponent } from './vault/attachments.component'; import { AttachmentsComponent } from "./vault/attachments.component";
import { CiphersComponent } from './vault/ciphers.component'; import { CiphersComponent } from "./vault/ciphers.component";
import { CollectionsComponent } from './vault/collections.component'; import { CollectionsComponent } from "./vault/collections.component";
import { CurrentTabComponent } from './vault/current-tab.component'; import { CurrentTabComponent } from "./vault/current-tab.component";
import { GroupingsComponent } from './vault/groupings.component'; import { GroupingsComponent } from "./vault/groupings.component";
import { PasswordHistoryComponent } from './vault/password-history.component'; import { PasswordHistoryComponent } from "./vault/password-history.component";
import { ShareComponent } from './vault/share.component'; import { ShareComponent } from "./vault/share.component";
import { ViewComponent } from './vault/view.component'; import { ViewComponent } from "./vault/view.component";
import { SendAddEditComponent } from './send/send-add-edit.component'; import { SendAddEditComponent } from "./send/send-add-edit.component";
import { SendGroupingsComponent } from './send/send-groupings.component'; import { SendGroupingsComponent } from "./send/send-groupings.component";
import { SendTypeComponent } from './send/send-type.component'; import { SendTypeComponent } from "./send/send-type.component";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: "",
redirectTo: 'home', redirectTo: "home",
pathMatch: 'full', pathMatch: "full",
}, },
{ {
path: 'vault', path: "vault",
redirectTo: '/tabs/vault', redirectTo: "/tabs/vault",
pathMatch: 'full', pathMatch: "full",
}, },
{ {
path: 'home', path: "home",
component: HomeComponent, component: HomeComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
data: { state: 'home' }, data: { state: "home" },
}, },
{ {
path: 'login', path: "login",
component: LoginComponent, component: LoginComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
data: { state: 'login' }, data: { state: "login" },
}, },
{ {
path: 'lock', path: "lock",
component: LockComponent, component: LockComponent,
canActivate: [LockGuardService], canActivate: [LockGuardService],
data: { state: 'lock' }, data: { state: "lock" },
}, },
{ {
path: '2fa', path: "2fa",
component: TwoFactorComponent, component: TwoFactorComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
data: { state: '2fa' }, data: { state: "2fa" },
}, },
{ {
path: '2fa-options', path: "2fa-options",
component: TwoFactorOptionsComponent, component: TwoFactorOptionsComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
data: { state: '2fa-options' }, data: { state: "2fa-options" },
}, },
{ {
path: 'sso', path: "sso",
component: SsoComponent, component: SsoComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
data: { state: 'sso' }, data: { state: "sso" },
}, },
{ {
path: 'set-password', path: "set-password",
component: SetPasswordComponent, component: SetPasswordComponent,
data: { state: 'set-password' }, data: { state: "set-password" },
}, },
{ {
path: 'remove-password', path: "remove-password",
component: RemovePasswordComponent, component: RemovePasswordComponent,
canActivate: [AuthGuardService],
data: { state: "remove-password" },
},
{
path: "register",
component: RegisterComponent,
canActivate: [LaunchGuardService],
data: { state: "register" },
},
{
path: "hint",
component: HintComponent,
canActivate: [LaunchGuardService],
data: { state: "hint" },
},
{
path: "environment",
component: EnvironmentComponent,
canActivate: [LaunchGuardService],
data: { state: "environment" },
},
{
path: "ciphers",
component: CiphersComponent,
canActivate: [AuthGuardService],
data: { state: "ciphers" },
},
{
path: "view-cipher",
component: ViewComponent,
canActivate: [AuthGuardService],
data: { state: "view-cipher" },
},
{
path: "cipher-password-history",
component: PasswordHistoryComponent,
canActivate: [AuthGuardService],
data: { state: "cipher-password-history" },
},
{
path: "add-cipher",
component: AddEditComponent,
canActivate: [AuthGuardService, DebounceNavigationService],
data: { state: "add-cipher" },
runGuardsAndResolvers: "always",
},
{
path: "edit-cipher",
component: AddEditComponent,
canActivate: [AuthGuardService, DebounceNavigationService],
data: { state: "edit-cipher" },
runGuardsAndResolvers: "always",
},
{
path: "share-cipher",
component: ShareComponent,
canActivate: [AuthGuardService],
data: { state: "share-cipher" },
},
{
path: "collections",
component: CollectionsComponent,
canActivate: [AuthGuardService],
data: { state: "collections" },
},
{
path: "attachments",
component: AttachmentsComponent,
canActivate: [AuthGuardService],
data: { state: "attachments" },
},
{
path: "generator",
component: PasswordGeneratorComponent,
canActivate: [AuthGuardService],
data: { state: "generator" },
},
{
path: "generator-history",
component: PasswordGeneratorHistoryComponent,
canActivate: [AuthGuardService],
data: { state: "generator-history" },
},
{
path: "export",
component: ExportComponent,
canActivate: [AuthGuardService],
data: { state: "export" },
},
{
path: "folders",
component: FoldersComponent,
canActivate: [AuthGuardService],
data: { state: "folders" },
},
{
path: "add-folder",
component: FolderAddEditComponent,
canActivate: [AuthGuardService],
data: { state: "add-folder" },
},
{
path: "edit-folder",
component: FolderAddEditComponent,
canActivate: [AuthGuardService],
data: { state: "edit-folder" },
},
{
path: "sync",
component: SyncComponent,
canActivate: [AuthGuardService],
data: { state: "sync" },
},
{
path: "excluded-domains",
component: ExcludedDomainsComponent,
canActivate: [AuthGuardService],
data: { state: "excluded-domains" },
},
{
path: "premium",
component: PremiumComponent,
canActivate: [AuthGuardService],
data: { state: "premium" },
},
{
path: "options",
component: OptionsComponent,
canActivate: [AuthGuardService],
data: { state: "options" },
},
{
path: "private-mode",
component: PrivateModeComponent,
data: { state: "private-mode" },
},
{
path: "clone-cipher",
component: AddEditComponent,
canActivate: [AuthGuardService],
data: { state: "clone-cipher" },
},
{
path: "send-type",
component: SendTypeComponent,
canActivate: [AuthGuardService],
data: { state: "send-type" },
},
{
path: "add-send",
component: SendAddEditComponent,
canActivate: [AuthGuardService],
data: { state: "add-send" },
},
{
path: "edit-send",
component: SendAddEditComponent,
canActivate: [AuthGuardService],
data: { state: "edit-send" },
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [AuthGuardService],
data: { state: "update-temp-password" },
},
{
path: "tabs",
component: TabsComponent,
data: { state: "tabs" },
children: [
{
path: "",
redirectTo: "/tabs/vault",
pathMatch: "full",
},
{
path: "current",
component: CurrentTabComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'remove-password' }, data: { state: "tabs_current" },
}, runGuardsAndResolvers: "always",
{ },
path: 'register', {
component: RegisterComponent, path: "vault",
canActivate: [LaunchGuardService], component: GroupingsComponent,
data: { state: 'register' },
},
{
path: 'hint',
component: HintComponent,
canActivate: [LaunchGuardService],
data: { state: 'hint' },
},
{
path: 'environment',
component: EnvironmentComponent,
canActivate: [LaunchGuardService],
data: { state: 'environment' },
},
{
path: 'ciphers',
component: CiphersComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'ciphers' }, data: { state: "tabs_vault" },
}, },
{ {
path: 'view-cipher', path: "generator",
component: ViewComponent,
canActivate: [AuthGuardService],
data: { state: 'view-cipher' },
},
{
path: 'cipher-password-history',
component: PasswordHistoryComponent,
canActivate: [AuthGuardService],
data: { state: 'cipher-password-history' },
},
{
path: 'add-cipher',
component: AddEditComponent,
canActivate: [AuthGuardService, DebounceNavigationService],
data: { state: 'add-cipher' },
runGuardsAndResolvers: 'always',
},
{
path: 'edit-cipher',
component: AddEditComponent,
canActivate: [AuthGuardService, DebounceNavigationService],
data: { state: 'edit-cipher' },
runGuardsAndResolvers: 'always',
},
{
path: 'share-cipher',
component: ShareComponent,
canActivate: [AuthGuardService],
data: { state: 'share-cipher' },
},
{
path: 'collections',
component: CollectionsComponent,
canActivate: [AuthGuardService],
data: { state: 'collections' },
},
{
path: 'attachments',
component: AttachmentsComponent,
canActivate: [AuthGuardService],
data: { state: 'attachments' },
},
{
path: 'generator',
component: PasswordGeneratorComponent, component: PasswordGeneratorComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'generator' }, data: { state: "tabs_generator" },
}, },
{ {
path: 'generator-history', path: "settings",
component: PasswordGeneratorHistoryComponent, component: SettingsComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'generator-history' }, data: { state: "tabs_settings" },
}, },
{ {
path: 'export', path: "send",
component: ExportComponent, component: SendGroupingsComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'export' }, data: { state: "tabs_send" },
}, },
{ ],
path: 'folders', },
component: FoldersComponent,
canActivate: [AuthGuardService],
data: { state: 'folders' },
},
{
path: 'add-folder',
component: FolderAddEditComponent,
canActivate: [AuthGuardService],
data: { state: 'add-folder' },
},
{
path: 'edit-folder',
component: FolderAddEditComponent,
canActivate: [AuthGuardService],
data: { state: 'edit-folder' },
},
{
path: 'sync',
component: SyncComponent,
canActivate: [AuthGuardService],
data: { state: 'sync' },
},
{
path: 'excluded-domains',
component: ExcludedDomainsComponent,
canActivate: [AuthGuardService],
data: { state: 'excluded-domains' },
},
{
path: 'premium',
component: PremiumComponent,
canActivate: [AuthGuardService],
data: { state: 'premium' },
},
{
path: 'options',
component: OptionsComponent,
canActivate: [AuthGuardService],
data: { state: 'options' },
},
{
path: 'private-mode',
component: PrivateModeComponent,
data: { state: 'private-mode' },
},
{
path: 'clone-cipher',
component: AddEditComponent,
canActivate: [AuthGuardService],
data: { state: 'clone-cipher' },
},
{
path: 'send-type',
component: SendTypeComponent,
canActivate: [AuthGuardService],
data: { state: 'send-type' },
},
{
path: 'add-send',
component: SendAddEditComponent,
canActivate: [AuthGuardService],
data: { state: 'add-send' },
},
{
path: 'edit-send',
component: SendAddEditComponent,
canActivate: [AuthGuardService],
data: { state: 'edit-send' },
},
{
path: 'update-temp-password',
component: UpdateTempPasswordComponent,
canActivate: [AuthGuardService],
data: { state: 'update-temp-password' },
},
{
path: 'tabs',
component: TabsComponent,
data: { state: 'tabs' },
children: [
{
path: '',
redirectTo: '/tabs/vault',
pathMatch: 'full',
},
{
path: 'current',
component: CurrentTabComponent,
canActivate: [AuthGuardService],
data: { state: 'tabs_current' },
runGuardsAndResolvers: 'always',
},
{
path: 'vault',
component: GroupingsComponent,
canActivate: [AuthGuardService],
data: { state: 'tabs_vault' },
},
{
path: 'generator',
component: PasswordGeneratorComponent,
canActivate: [AuthGuardService],
data: { state: 'tabs_generator' },
},
{
path: 'settings',
component: SettingsComponent,
canActivate: [AuthGuardService],
data: { state: 'tabs_settings' },
},
{
path: 'send',
component: SendGroupingsComponent,
canActivate: [AuthGuardService],
data: { state: 'tabs_send' },
},
],
},
]; ];
@Injectable() @Injectable()
export class NoRouteReuseStrategy implements RouteReuseStrategy { export class NoRouteReuseStrategy implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot) { shouldDetach(route: ActivatedRouteSnapshot) {
return false; return false;
} }
store(route: ActivatedRouteSnapshot, handle: {}) { /* Nothing */ } store(route: ActivatedRouteSnapshot, handle: {}) {
/* Nothing */
}
shouldAttach(route: ActivatedRouteSnapshot) { shouldAttach(route: ActivatedRouteSnapshot) {
return false; return false;
} }
retrieve(route: ActivatedRouteSnapshot): any { retrieve(route: ActivatedRouteSnapshot): any {
return null; return null;
} }
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) { shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) {
return false; return false;
} }
} }
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [
useHash: true, RouterModule.forRoot(routes, {
onSameUrlNavigation: 'reload', useHash: true,
/*enableTracing: true,*/ onSameUrlNavigation: "reload",
})], /*enableTracing: true,*/
exports: [RouterModule], }),
providers: [ ],
{ provide: RouteReuseStrategy, useClass: NoRouteReuseStrategy }, exports: [RouterModule],
], providers: [{ provide: RouteReuseStrategy, useClass: NoRouteReuseStrategy }],
}) })
export class AppRoutingModule { } export class AppRoutingModule {}

View File

@ -1,243 +1,254 @@
import { import { ChangeDetectorRef, Component, NgZone, OnInit, SecurityContext } from "@angular/core";
ChangeDetectorRef, import { DomSanitizer } from "@angular/platform-browser";
Component, import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
NgZone, import { IndividualConfig, ToastrService } from "ngx-toastr";
OnInit, import Swal, { SweetAlertIcon } from "sweetalert2/src/sweetalert2.js";
SecurityContext, import { BrowserApi } from "../browser/browserApi";
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
NavigationEnd,
Router,
RouterOutlet,
} from '@angular/router';
import {
IndividualConfig,
ToastrService,
} from 'ngx-toastr';
import Swal, { SweetAlertIcon } from 'sweetalert2/src/sweetalert2.js';
import { BrowserApi } from '../browser/browserApi';
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { routerTransition } from './app-routing.animations'; import { routerTransition } from "./app-routing.animations";
@Component({ @Component({
selector: 'app-root', selector: "app-root",
styles: [], styles: [],
animations: [routerTransition], animations: [routerTransition],
template: ` template: ` <main [@routerTransition]="getState(o)">
<main [@routerTransition]="getState(o)"> <router-outlet #o="outlet"></router-outlet>
<router-outlet #o="outlet"></router-outlet> </main>`,
</main>`,
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
private lastActivity: number = null;
private lastActivity: number = null; constructor(
private toastrService: ToastrService,
private storageService: StorageService,
private broadcasterService: BroadcasterService,
private authService: AuthService,
private i18nService: I18nService,
private router: Router,
private stateService: StateService,
private messagingService: MessagingService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
private sanitizer: DomSanitizer,
private platformUtilsService: PlatformUtilsService,
private keyConnectoService: KeyConnectorService
) {}
constructor(private toastrService: ToastrService, private storageService: StorageService, ngOnInit() {
private broadcasterService: BroadcasterService, private authService: AuthService, if (BrowserApi.getBackgroundPage() == null) {
private i18nService: I18nService, private router: Router, return;
private stateService: StateService, private messagingService: MessagingService, }
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
private sanitizer: DomSanitizer, private platformUtilsService: PlatformUtilsService,
private keyConnectoService: KeyConnectorService) { }
ngOnInit() { this.ngZone.runOutsideAngular(() => {
if (BrowserApi.getBackgroundPage() == null) { window.onmousemove = () => this.recordActivity();
return; window.onmousedown = () => this.recordActivity();
} window.ontouchstart = () => this.recordActivity();
window.onclick = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
this.ngZone.runOutsideAngular(() => { (window as any).bitwardenPopupMainMessageListener = async (
window.onmousemove = () => this.recordActivity(); msg: any,
window.onmousedown = () => this.recordActivity(); sender: any,
window.ontouchstart = () => this.recordActivity(); sendResponse: any
window.onclick = () => this.recordActivity(); ) => {
window.onscroll = () => this.recordActivity(); if (msg.command === "doneLoggingOut") {
window.onkeypress = () => this.recordActivity(); this.ngZone.run(async () => {
this.authService.logOut(() => {
if (msg.expired) {
this.showToast({
type: "warning",
title: this.i18nService.t("loggedOut"),
text: this.i18nService.t("loginExpired"),
});
}
this.router.navigate(["home"]);
this.stateService.purge();
});
this.changeDetectorRef.detectChanges();
}); });
} else if (msg.command === "authBlocked") {
(window as any).bitwardenPopupMainMessageListener = async (msg: any, sender: any, sendResponse: any) => { this.ngZone.run(() => {
if (msg.command === 'doneLoggingOut') { this.router.navigate(["home"]);
this.ngZone.run(async () => {
this.authService.logOut(() => {
if (msg.expired) {
this.showToast({
type: 'warning',
title: this.i18nService.t('loggedOut'),
text: this.i18nService.t('loginExpired'),
});
}
this.router.navigate(['home']);
this.stateService.purge();
});
this.changeDetectorRef.detectChanges();
});
} else if (msg.command === 'authBlocked') {
this.ngZone.run(() => {
this.router.navigate(['home']);
});
} else if (msg.command === 'locked') {
this.stateService.purge();
this.ngZone.run(() => {
this.router.navigate(['lock']);
});
} else if (msg.command === 'showDialog') {
await this.showDialog(msg);
} else if (msg.command === 'showToast') {
this.ngZone.run(() => {
this.showToast(msg);
});
} else if (msg.command === 'reloadProcess') {
const windowReload = this.platformUtilsService.isSafari() ||
this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera();
if (windowReload) {
// Wait to make sure background has reloaded first.
window.setTimeout(() => BrowserApi.reloadExtension(window), 2000);
}
} else if (msg.command === 'reloadPopup') {
this.ngZone.run(() => {
this.router.navigate(['/']);
});
} else if (msg.command === 'convertAccountToKeyConnector') {
this.ngZone.run(async () => {
await this.keyConnectoService.setConvertAccountRequired(true);
this.router.navigate(['/remove-password']);
});
} else {
msg.webExtSender = sender;
this.broadcasterService.send(msg);
}
};
BrowserApi.messageListener('app.component', (window as any).bitwardenPopupMainMessageListener);
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
const url = event.urlAfterRedirects || event.url || '';
if (url.startsWith('/tabs/') && (window as any).previousPopupUrl != null &&
(window as any).previousPopupUrl.startsWith('/tabs/')) {
this.stateService.remove('GroupingsComponent');
this.stateService.remove('GroupingsComponentScope');
this.stateService.remove('CiphersComponent');
this.stateService.remove('SendGroupingsComponent');
this.stateService.remove('SendGroupingsComponentScope');
this.stateService.remove('SendTypeComponent');
}
if (url.startsWith('/tabs/')) {
this.stateService.remove('addEditCipherInfo');
}
(window as any).previousPopupUrl = url;
// Clear route direction after animation (400ms)
if ((window as any).routeDirection != null) {
window.setTimeout(() => {
(window as any).routeDirection = null;
}, 400);
}
}
}); });
} } else if (msg.command === "locked") {
this.stateService.purge();
getState(outlet: RouterOutlet) { this.ngZone.run(() => {
if (outlet.activatedRouteData.state === 'ciphers') { this.router.navigate(["lock"]);
const routeDirection = (window as any).routeDirection != null ? (window as any).routeDirection : '';
return 'ciphers_direction=' + routeDirection + '_' +
(outlet.activatedRoute.queryParams as any).value.folderId + '_' +
(outlet.activatedRoute.queryParams as any).value.collectionId;
} else {
return outlet.activatedRouteData.state;
}
}
private async recordActivity() {
const now = (new Date()).getTime();
if (this.lastActivity != null && now - this.lastActivity < 250) {
return;
}
this.lastActivity = now;
this.storageService.save(ConstantsService.lastActiveKey, now);
}
private showToast(msg: any) {
let message = '';
const options: Partial<IndividualConfig> = {};
if (typeof (msg.text) === 'string') {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, 'toast-' + msg.type);
}
private async showDialog(msg: any) {
let iconClasses: string = null;
const type = msg.type;
if (type != null) {
// If you add custom types to this part, the type to SweetAlertIcon cast below needs to be changed.
switch (type) {
case 'success':
iconClasses = 'fa-check text-success';
break;
case 'warning':
iconClasses = 'fa-warning text-warning';
break;
case 'error':
iconClasses = 'fa-bolt text-danger';
break;
case 'info':
iconClasses = 'fa-info-circle text-info';
break;
default:
break;
}
}
const cancelText = msg.cancelText;
const confirmText = msg.confirmText;
const confirmed = await Swal.fire({
heightAuto: false,
buttonsStyling: false,
icon: type as SweetAlertIcon, // required to be any of the SweetAlertIcons to output the iconHtml.
iconHtml: iconClasses != null ? `<i class="swal-custom-icon fa ${iconClasses}"></i>` : undefined,
text: msg.text,
html: msg.html,
titleText: msg.title,
showCancelButton: (cancelText != null),
cancelButtonText: cancelText,
showConfirmButton: true,
confirmButtonText: confirmText == null ? this.i18nService.t('ok') : confirmText,
timer: 300000,
}); });
} else if (msg.command === "showDialog") {
this.messagingService.send('showDialogResolve', { await this.showDialog(msg);
dialogId: msg.dialogId, } else if (msg.command === "showToast") {
confirmed: confirmed.value, this.ngZone.run(() => {
this.showToast(msg);
}); });
} else if (msg.command === "reloadProcess") {
const windowReload =
this.platformUtilsService.isSafari() ||
this.platformUtilsService.isFirefox() ||
this.platformUtilsService.isOpera();
if (windowReload) {
// Wait to make sure background has reloaded first.
window.setTimeout(() => BrowserApi.reloadExtension(window), 2000);
}
} else if (msg.command === "reloadPopup") {
this.ngZone.run(() => {
this.router.navigate(["/"]);
});
} else if (msg.command === "convertAccountToKeyConnector") {
this.ngZone.run(async () => {
await this.keyConnectoService.setConvertAccountRequired(true);
this.router.navigate(["/remove-password"]);
});
} else {
msg.webExtSender = sender;
this.broadcasterService.send(msg);
}
};
BrowserApi.messageListener("app.component", (window as any).bitwardenPopupMainMessageListener);
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
const url = event.urlAfterRedirects || event.url || "";
if (
url.startsWith("/tabs/") &&
(window as any).previousPopupUrl != null &&
(window as any).previousPopupUrl.startsWith("/tabs/")
) {
this.stateService.remove("GroupingsComponent");
this.stateService.remove("GroupingsComponentScope");
this.stateService.remove("CiphersComponent");
this.stateService.remove("SendGroupingsComponent");
this.stateService.remove("SendGroupingsComponentScope");
this.stateService.remove("SendTypeComponent");
}
if (url.startsWith("/tabs/")) {
this.stateService.remove("addEditCipherInfo");
}
(window as any).previousPopupUrl = url;
// Clear route direction after animation (400ms)
if ((window as any).routeDirection != null) {
window.setTimeout(() => {
(window as any).routeDirection = null;
}, 400);
}
}
});
}
getState(outlet: RouterOutlet) {
if (outlet.activatedRouteData.state === "ciphers") {
const routeDirection =
(window as any).routeDirection != null ? (window as any).routeDirection : "";
return (
"ciphers_direction=" +
routeDirection +
"_" +
(outlet.activatedRoute.queryParams as any).value.folderId +
"_" +
(outlet.activatedRoute.queryParams as any).value.collectionId
);
} else {
return outlet.activatedRouteData.state;
} }
}
private async recordActivity() {
const now = new Date().getTime();
if (this.lastActivity != null && now - this.lastActivity < 250) {
return;
}
this.lastActivity = now;
this.storageService.save(ConstantsService.lastActiveKey, now);
}
private showToast(msg: any) {
let message = "";
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
}
private async showDialog(msg: any) {
let iconClasses: string = null;
const type = msg.type;
if (type != null) {
// If you add custom types to this part, the type to SweetAlertIcon cast below needs to be changed.
switch (type) {
case "success":
iconClasses = "fa-check text-success";
break;
case "warning":
iconClasses = "fa-warning text-warning";
break;
case "error":
iconClasses = "fa-bolt text-danger";
break;
case "info":
iconClasses = "fa-info-circle text-info";
break;
default:
break;
}
}
const cancelText = msg.cancelText;
const confirmText = msg.confirmText;
const confirmed = await Swal.fire({
heightAuto: false,
buttonsStyling: false,
icon: type as SweetAlertIcon, // required to be any of the SweetAlertIcons to output the iconHtml.
iconHtml:
iconClasses != null ? `<i class="swal-custom-icon fa ${iconClasses}"></i>` : undefined,
text: msg.text,
html: msg.html,
titleText: msg.title,
showCancelButton: cancelText != null,
cancelButtonText: cancelText,
showConfirmButton: true,
confirmButtonText: confirmText == null ? this.i18nService.t("ok") : confirmText,
timer: 300000,
});
this.messagingService.send("showDialogResolve", {
dialogId: msg.dialogId,
confirmed: confirmed.value,
});
}
} }

View File

@ -1,272 +1,265 @@
import { A11yModule } from '@angular/cdk/a11y'; import { A11yModule } from "@angular/cdk/a11y";
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from "@angular/cdk/drag-drop";
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from "@angular/cdk/scrolling";
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from "./app-routing.module";
import { ServicesModule } from './services/services.module'; import { ServicesModule } from "./services/services.module";
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { EnvironmentComponent } from './accounts/environment.component'; import { EnvironmentComponent } from "./accounts/environment.component";
import { HintComponent } from './accounts/hint.component'; import { HintComponent } from "./accounts/hint.component";
import { HomeComponent } from './accounts/home.component'; import { HomeComponent } from "./accounts/home.component";
import { LockComponent } from './accounts/lock.component'; import { LockComponent } from "./accounts/lock.component";
import { LoginComponent } from './accounts/login.component'; import { LoginComponent } from "./accounts/login.component";
import { RegisterComponent } from './accounts/register.component'; import { RegisterComponent } from "./accounts/register.component";
import { RemovePasswordComponent } from './accounts/remove-password.component'; import { RemovePasswordComponent } from "./accounts/remove-password.component";
import { SetPasswordComponent } from './accounts/set-password.component'; import { SetPasswordComponent } from "./accounts/set-password.component";
import { SsoComponent } from './accounts/sso.component'; import { SsoComponent } from "./accounts/sso.component";
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorOptionsComponent } from "./accounts/two-factor-options.component";
import { TwoFactorComponent } from './accounts/two-factor.component'; import { TwoFactorComponent } from "./accounts/two-factor.component";
import { UpdateTempPasswordComponent } from './accounts/update-temp-password.component'; import { UpdateTempPasswordComponent } from "./accounts/update-temp-password.component";
import { PasswordGeneratorHistoryComponent } from './generator/password-generator-history.component'; import { PasswordGeneratorHistoryComponent } from "./generator/password-generator-history.component";
import { PasswordGeneratorComponent } from './generator/password-generator.component'; import { PasswordGeneratorComponent } from "./generator/password-generator.component";
import { AppComponent } from './app.component'; import { AppComponent } from "./app.component";
import { PrivateModeComponent } from './private-mode.component'; import { PrivateModeComponent } from "./private-mode.component";
import { TabsComponent } from './tabs.component'; import { TabsComponent } from "./tabs.component";
import { ExcludedDomainsComponent } from './settings/excluded-domains.component'; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { ExportComponent } from './settings/export.component'; import { ExportComponent } from "./settings/export.component";
import { FolderAddEditComponent } from './settings/folder-add-edit.component'; import { FolderAddEditComponent } from "./settings/folder-add-edit.component";
import { FoldersComponent } from './settings/folders.component'; import { FoldersComponent } from "./settings/folders.component";
import { OptionsComponent } from './settings/options.component'; import { OptionsComponent } from "./settings/options.component";
import { PremiumComponent } from './settings/premium.component'; import { PremiumComponent } from "./settings/premium.component";
import { SettingsComponent } from './settings/settings.component'; import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from './settings/sync.component'; import { SyncComponent } from "./settings/sync.component";
import { VaultTimeoutInputComponent } from './settings/vault-timeout-input.component'; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";
import { AddEditCustomFieldsComponent } from './vault/add-edit-custom-fields.component'; import { AddEditCustomFieldsComponent } from "./vault/add-edit-custom-fields.component";
import { AddEditComponent } from './vault/add-edit.component'; import { AddEditComponent } from "./vault/add-edit.component";
import { AttachmentsComponent } from './vault/attachments.component'; import { AttachmentsComponent } from "./vault/attachments.component";
import { CiphersComponent } from './vault/ciphers.component'; import { CiphersComponent } from "./vault/ciphers.component";
import { CollectionsComponent } from './vault/collections.component'; import { CollectionsComponent } from "./vault/collections.component";
import { CurrentTabComponent } from './vault/current-tab.component'; import { CurrentTabComponent } from "./vault/current-tab.component";
import { GroupingsComponent } from './vault/groupings.component'; import { GroupingsComponent } from "./vault/groupings.component";
import { PasswordHistoryComponent } from './vault/password-history.component'; import { PasswordHistoryComponent } from "./vault/password-history.component";
import { ShareComponent } from './vault/share.component'; import { ShareComponent } from "./vault/share.component";
import { ViewCustomFieldsComponent } from './vault/view-custom-fields.component'; import { ViewCustomFieldsComponent } from "./vault/view-custom-fields.component";
import { ViewComponent } from './vault/view.component'; import { ViewComponent } from "./vault/view.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from './send/efflux-dates.component'; import { EffluxDatesComponent as SendEffluxDatesComponent } from "./send/efflux-dates.component";
import { SendAddEditComponent } from './send/send-add-edit.component'; import { SendAddEditComponent } from "./send/send-add-edit.component";
import { SendGroupingsComponent } from './send/send-groupings.component'; import { SendGroupingsComponent } from "./send/send-groupings.component";
import { SendTypeComponent } from './send/send-type.component'; import { SendTypeComponent } from "./send/send-type.component";
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive'; import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive'; import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive'; import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive'; import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive'; import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
import { CipherListVirtualScroll } from 'jslib-angular/directives/cipherListVirtualScroll.directive'; import { CipherListVirtualScroll } from "jslib-angular/directives/cipherListVirtualScroll.directive";
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive'; import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive";
import { InputVerbatimDirective } from 'jslib-angular/directives/input-verbatim.directive'; import { InputVerbatimDirective } from "jslib-angular/directives/input-verbatim.directive";
import { SelectCopyDirective } from 'jslib-angular/directives/select-copy.directive'; import { SelectCopyDirective } from "jslib-angular/directives/select-copy.directive";
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive'; import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive'; import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive";
import { TrueFalseValueDirective } from 'jslib-angular/directives/true-false-value.directive'; import { TrueFalseValueDirective } from "jslib-angular/directives/true-false-value.directive";
import { ColorPasswordPipe } from 'jslib-angular/pipes/color-password.pipe'; import { ColorPasswordPipe } from "jslib-angular/pipes/color-password.pipe";
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe'; import { I18nPipe } from "jslib-angular/pipes/i18n.pipe";
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe'; import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe";
import { ActionButtonsComponent } from './components/action-buttons.component'; import { ActionButtonsComponent } from "./components/action-buttons.component";
import { CipherRowComponent } from './components/cipher-row.component'; import { CipherRowComponent } from "./components/cipher-row.component";
import { PasswordRepromptComponent } from './components/password-reprompt.component'; import { PasswordRepromptComponent } from "./components/password-reprompt.component";
import { PopOutComponent } from './components/pop-out.component'; import { PopOutComponent } from "./components/pop-out.component";
import { SendListComponent } from './components/send-list.component'; import { SendListComponent } from "./components/send-list.component";
import { SetPinComponent } from './components/set-pin.component'; import { SetPinComponent } from "./components/set-pin.component";
import { VerifyMasterPasswordComponent } from './components/verify-master-password.component'; import { VerifyMasterPasswordComponent } from "./components/verify-master-password.component";
import { CalloutComponent } from 'jslib-angular/components/callout.component'; import { CalloutComponent } from "jslib-angular/components/callout.component";
import { IconComponent } from 'jslib-angular/components/icon.component'; import { IconComponent } from "jslib-angular/components/icon.component";
import { BitwardenToastModule } from 'jslib-angular/components/toastr.component'; import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
import { import { CurrencyPipe, DatePipe, registerLocaleData } from "@angular/common";
CurrencyPipe, import localeAz from "@angular/common/locales/az";
DatePipe, import localeBe from "@angular/common/locales/be";
registerLocaleData, import localeBg from "@angular/common/locales/bg";
} from '@angular/common'; import localeBn from "@angular/common/locales/bn";
import localeAz from '@angular/common/locales/az'; import localeCa from "@angular/common/locales/ca";
import localeBe from '@angular/common/locales/be'; import localeCs from "@angular/common/locales/cs";
import localeBg from '@angular/common/locales/bg'; import localeDa from "@angular/common/locales/da";
import localeBn from '@angular/common/locales/bn'; import localeDe from "@angular/common/locales/de";
import localeCa from '@angular/common/locales/ca'; import localeEl from "@angular/common/locales/el";
import localeCs from '@angular/common/locales/cs'; import localeEnGb from "@angular/common/locales/en-GB";
import localeDa from '@angular/common/locales/da'; import localeEnIn from "@angular/common/locales/en-IN";
import localeDe from '@angular/common/locales/de'; import localeEs from "@angular/common/locales/es";
import localeEl from '@angular/common/locales/el'; import localeEt from "@angular/common/locales/et";
import localeEnGb from '@angular/common/locales/en-GB'; import localeFa from "@angular/common/locales/fa";
import localeEnIn from '@angular/common/locales/en-IN'; import localeFi from "@angular/common/locales/fi";
import localeEs from '@angular/common/locales/es'; import localeFr from "@angular/common/locales/fr";
import localeEt from '@angular/common/locales/et'; import localeHe from "@angular/common/locales/he";
import localeFa from '@angular/common/locales/fa'; import localeHr from "@angular/common/locales/hr";
import localeFi from '@angular/common/locales/fi'; import localeHu from "@angular/common/locales/hu";
import localeFr from '@angular/common/locales/fr'; import localeId from "@angular/common/locales/id";
import localeHe from '@angular/common/locales/he'; import localeIt from "@angular/common/locales/it";
import localeHr from '@angular/common/locales/hr'; import localeJa from "@angular/common/locales/ja";
import localeHu from '@angular/common/locales/hu'; import localeKn from "@angular/common/locales/kn";
import localeId from '@angular/common/locales/id'; import localeKo from "@angular/common/locales/ko";
import localeIt from '@angular/common/locales/it'; import localeLv from "@angular/common/locales/lv";
import localeJa from '@angular/common/locales/ja'; import localeMl from "@angular/common/locales/ml";
import localeKn from '@angular/common/locales/kn'; import localeNb from "@angular/common/locales/nb";
import localeKo from '@angular/common/locales/ko'; import localeNl from "@angular/common/locales/nl";
import localeLv from '@angular/common/locales/lv'; import localePl from "@angular/common/locales/pl";
import localeMl from '@angular/common/locales/ml'; import localePtBr from "@angular/common/locales/pt";
import localeNb from '@angular/common/locales/nb'; import localePtPt from "@angular/common/locales/pt-PT";
import localeNl from '@angular/common/locales/nl'; import localeRo from "@angular/common/locales/ro";
import localePl from '@angular/common/locales/pl'; import localeRu from "@angular/common/locales/ru";
import localePtBr from '@angular/common/locales/pt'; import localeSk from "@angular/common/locales/sk";
import localePtPt from '@angular/common/locales/pt-PT'; import localeSr from "@angular/common/locales/sr";
import localeRo from '@angular/common/locales/ro'; import localeSv from "@angular/common/locales/sv";
import localeRu from '@angular/common/locales/ru'; import localeTh from "@angular/common/locales/th";
import localeSk from '@angular/common/locales/sk'; import localeTr from "@angular/common/locales/tr";
import localeSr from '@angular/common/locales/sr'; import localeUk from "@angular/common/locales/uk";
import localeSv from '@angular/common/locales/sv'; import localeVi from "@angular/common/locales/vi";
import localeTh from '@angular/common/locales/th'; import localeZhCn from "@angular/common/locales/zh-Hans";
import localeTr from '@angular/common/locales/tr'; import localeZhTw from "@angular/common/locales/zh-Hant";
import localeUk from '@angular/common/locales/uk';
import localeVi from '@angular/common/locales/vi';
import localeZhCn from '@angular/common/locales/zh-Hans';
import localeZhTw from '@angular/common/locales/zh-Hant';
registerLocaleData(localeAz, 'az'); registerLocaleData(localeAz, "az");
registerLocaleData(localeBe, 'be'); registerLocaleData(localeBe, "be");
registerLocaleData(localeBg, 'bg'); registerLocaleData(localeBg, "bg");
registerLocaleData(localeBn, 'bn'); registerLocaleData(localeBn, "bn");
registerLocaleData(localeCa, 'ca'); registerLocaleData(localeCa, "ca");
registerLocaleData(localeCs, 'cs'); registerLocaleData(localeCs, "cs");
registerLocaleData(localeDa, 'da'); registerLocaleData(localeDa, "da");
registerLocaleData(localeDe, 'de'); registerLocaleData(localeDe, "de");
registerLocaleData(localeEl, 'el'); registerLocaleData(localeEl, "el");
registerLocaleData(localeEnGb, 'en-GB'); registerLocaleData(localeEnGb, "en-GB");
registerLocaleData(localeEnIn, 'en-IN'); registerLocaleData(localeEnIn, "en-IN");
registerLocaleData(localeEs, 'es'); registerLocaleData(localeEs, "es");
registerLocaleData(localeEt, 'et'); registerLocaleData(localeEt, "et");
registerLocaleData(localeFa, 'fa'); registerLocaleData(localeFa, "fa");
registerLocaleData(localeFi, 'fi'); registerLocaleData(localeFi, "fi");
registerLocaleData(localeFr, 'fr'); registerLocaleData(localeFr, "fr");
registerLocaleData(localeHe, 'he'); registerLocaleData(localeHe, "he");
registerLocaleData(localeHr, 'hr'); registerLocaleData(localeHr, "hr");
registerLocaleData(localeHu, 'hu'); registerLocaleData(localeHu, "hu");
registerLocaleData(localeId, 'id'); registerLocaleData(localeId, "id");
registerLocaleData(localeIt, 'it'); registerLocaleData(localeIt, "it");
registerLocaleData(localeJa, 'ja'); registerLocaleData(localeJa, "ja");
registerLocaleData(localeKo, 'ko'); registerLocaleData(localeKo, "ko");
registerLocaleData(localeKn, 'kn'); registerLocaleData(localeKn, "kn");
registerLocaleData(localeLv, 'lv'); registerLocaleData(localeLv, "lv");
registerLocaleData(localeMl, 'ml'); registerLocaleData(localeMl, "ml");
registerLocaleData(localeNb, 'nb'); registerLocaleData(localeNb, "nb");
registerLocaleData(localeNl, 'nl'); registerLocaleData(localeNl, "nl");
registerLocaleData(localePl, 'pl'); registerLocaleData(localePl, "pl");
registerLocaleData(localePtBr, 'pt-BR'); registerLocaleData(localePtBr, "pt-BR");
registerLocaleData(localePtPt, 'pt-PT'); registerLocaleData(localePtPt, "pt-PT");
registerLocaleData(localeRo, 'ro'); registerLocaleData(localeRo, "ro");
registerLocaleData(localeRu, 'ru'); registerLocaleData(localeRu, "ru");
registerLocaleData(localeSk, 'sk'); registerLocaleData(localeSk, "sk");
registerLocaleData(localeSr, 'sr'); registerLocaleData(localeSr, "sr");
registerLocaleData(localeSv, 'sv'); registerLocaleData(localeSv, "sv");
registerLocaleData(localeTh, 'th'); registerLocaleData(localeTh, "th");
registerLocaleData(localeTr, 'tr'); registerLocaleData(localeTr, "tr");
registerLocaleData(localeUk, 'uk'); registerLocaleData(localeUk, "uk");
registerLocaleData(localeVi, 'vi'); registerLocaleData(localeVi, "vi");
registerLocaleData(localeZhCn, 'zh-CN'); registerLocaleData(localeZhCn, "zh-CN");
registerLocaleData(localeZhTw, 'zh-TW'); registerLocaleData(localeZhTw, "zh-TW");
@NgModule({ @NgModule({
imports: [ imports: [
A11yModule, A11yModule,
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
BrowserModule, BrowserModule,
DragDropModule, DragDropModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ScrollingModule, ScrollingModule,
ServicesModule, ServicesModule,
BitwardenToastModule.forRoot({ BitwardenToastModule.forRoot({
maxOpened: 2, maxOpened: 2,
autoDismiss: true, autoDismiss: true,
closeButton: true, closeButton: true,
positionClass: 'toast-bottom-full-width', positionClass: "toast-bottom-full-width",
}), }),
], ],
declarations: [ declarations: [
A11yTitleDirective, A11yTitleDirective,
ActionButtonsComponent, ActionButtonsComponent,
AddEditComponent, AddEditComponent,
AddEditCustomFieldsComponent, AddEditCustomFieldsComponent,
ApiActionDirective, ApiActionDirective,
AppComponent, AppComponent,
AttachmentsComponent, AttachmentsComponent,
AutofocusDirective, AutofocusDirective,
BlurClickDirective, BlurClickDirective,
BoxRowDirective, BoxRowDirective,
CalloutComponent, CalloutComponent,
CipherListVirtualScroll, CipherListVirtualScroll,
CipherRowComponent, CipherRowComponent,
CiphersComponent, CiphersComponent,
CollectionsComponent, CollectionsComponent,
ColorPasswordPipe, ColorPasswordPipe,
CurrentTabComponent, CurrentTabComponent,
EnvironmentComponent, EnvironmentComponent,
ExcludedDomainsComponent, ExcludedDomainsComponent,
ExportComponent, ExportComponent,
FallbackSrcDirective, FallbackSrcDirective,
FolderAddEditComponent, FolderAddEditComponent,
FoldersComponent, FoldersComponent,
GroupingsComponent, GroupingsComponent,
HintComponent, HintComponent,
HomeComponent, HomeComponent,
I18nPipe, I18nPipe,
IconComponent, IconComponent,
InputVerbatimDirective, InputVerbatimDirective,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
OptionsComponent, OptionsComponent,
PasswordGeneratorComponent, PasswordGeneratorComponent,
PasswordGeneratorHistoryComponent, PasswordGeneratorHistoryComponent,
PasswordHistoryComponent, PasswordHistoryComponent,
PasswordRepromptComponent, PasswordRepromptComponent,
PopOutComponent, PopOutComponent,
PremiumComponent, PremiumComponent,
PrivateModeComponent, PrivateModeComponent,
RegisterComponent, RegisterComponent,
SearchCiphersPipe, SearchCiphersPipe,
SelectCopyDirective, SelectCopyDirective,
SendAddEditComponent, SendAddEditComponent,
SendEffluxDatesComponent, SendEffluxDatesComponent,
SendGroupingsComponent, SendGroupingsComponent,
SendListComponent, SendListComponent,
SendTypeComponent, SendTypeComponent,
SetPasswordComponent, SetPasswordComponent,
SetPinComponent, SetPinComponent,
SettingsComponent, SettingsComponent,
ShareComponent, ShareComponent,
SsoComponent, SsoComponent,
StopClickDirective, StopClickDirective,
StopPropDirective, StopPropDirective,
SyncComponent, SyncComponent,
TabsComponent, TabsComponent,
TrueFalseValueDirective, TrueFalseValueDirective,
TwoFactorComponent, TwoFactorComponent,
TwoFactorOptionsComponent, TwoFactorOptionsComponent,
UpdateTempPasswordComponent, UpdateTempPasswordComponent,
VaultTimeoutInputComponent, VaultTimeoutInputComponent,
VerifyMasterPasswordComponent, VerifyMasterPasswordComponent,
ViewComponent, ViewComponent,
ViewCustomFieldsComponent, ViewCustomFieldsComponent,
RemovePasswordComponent, RemovePasswordComponent,
], ],
entryComponents: [], entryComponents: [],
providers: [ providers: [CurrencyPipe, DatePipe],
CurrencyPipe, bootstrap: [AppComponent],
DatePipe,
],
bootstrap: [AppComponent],
}) })
export class AppModule { } export class AppModule {}

View File

@ -1,41 +1,87 @@
<span class="row-btn" (click)="view()" appStopClick appStopProp appA11yTitle="{{'view' | i18n}}" *ngIf="showView"> <span
<i class="fa fa-lg fa-list-alt" aria-hidden="true"></i> class="row-btn"
(click)="view()"
appStopClick
appStopProp
appA11yTitle="{{ 'view' | i18n }}"
*ngIf="showView"
>
<i class="fa fa-lg fa-list-alt" aria-hidden="true"></i>
</span> </span>
<ng-container *ngIf="cipher.type === cipherType.Login"> <ng-container *ngIf="cipher.type === cipherType.Login">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'launch' | i18n}}" (click)="launchCipher()" <span
*ngIf="!showView" [ngClass]="{disabled: !cipher.login.canLaunch}"> class="row-btn"
<i class="fa fa-lg fa-share-square-o" aria-hidden="true"></i> appStopClick
</span> appStopProp
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyUsername' | i18n}}" appA11yTitle="{{ 'launch' | i18n }}"
(click)="copy(cipher, cipher.login.username, 'username', 'Username')" (click)="launchCipher()"
[ngClass]="{disabled: !cipher.login.username}"> *ngIf="!showView"
<i class="fa fa-lg fa-user" aria-hidden="true"></i> [ngClass]="{ disabled: !cipher.login.canLaunch }"
</span> >
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyPassword' | i18n}}" <i class="fa fa-lg fa-share-square-o" aria-hidden="true"></i>
(click)="copy(cipher, cipher.login.password, 'password', 'Password')" </span>
[ngClass]="{disabled: (!cipher.login.password || !cipher.viewPassword)}"> <span
<i class="fa fa-lg fa-key" aria-hidden="true"></i> class="row-btn"
</span> appStopClick
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyVerificationCode' | i18n}}" appStopProp
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')" appA11yTitle="{{ 'copyUsername' | i18n }}"
[ngClass]="{disabled: (!displayTotpCopyButton(cipher))}"> (click)="copy(cipher, cipher.login.username, 'username', 'Username')"
<i class="fa fa-lg fa-clock-o" aria-hidden="true"></i> [ngClass]="{ disabled: !cipher.login.username }"
</span> >
<i class="fa fa-lg fa-user" aria-hidden="true"></i>
</span>
<span
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher, cipher.login.password, 'password', 'Password')"
[ngClass]="{ disabled: !cipher.login.password || !cipher.viewPassword }"
>
<i class="fa fa-lg fa-key" aria-hidden="true"></i>
</span>
<span
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
[ngClass]="{ disabled: !displayTotpCopyButton(cipher) }"
>
<i class="fa fa-lg fa-clock-o" aria-hidden="true"></i>
</span>
</ng-container> </ng-container>
<ng-container *ngIf="cipher.type === cipherType.Card"> <ng-container *ngIf="cipher.type === cipherType.Card">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyNumber' | i18n}}" <span
(click)="copy(cipher, cipher.card.number, 'number', 'Card Number')" [ngClass]="{disabled: !cipher.card.number}"> class="row-btn"
<i class="fa fa-lg fa-hashtag" aria-hidden="true"></i> appStopClick
</span> appStopProp
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copySecurityCode' | i18n}}" appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')" (click)="copy(cipher, cipher.card.number, 'number', 'Card Number')"
[ngClass]="{disabled: !cipher.card.code}"> [ngClass]="{ disabled: !cipher.card.number }"
<i class="fa fa-lg fa-key" aria-hidden="true"></i> >
</span> <i class="fa fa-lg fa-hashtag" aria-hidden="true"></i>
</span>
<span
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')"
[ngClass]="{ disabled: !cipher.card.code }"
>
<i class="fa fa-lg fa-key" aria-hidden="true"></i>
</span>
</ng-container> </ng-container>
<ng-container *ngIf="cipher.type === cipherType.SecureNote"> <ng-container *ngIf="cipher.type === cipherType.SecureNote">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copyNote' | i18n}}" <span
(click)="copy(cipher, cipher.notes, 'note', 'Note')" [ngClass]="{disabled: !cipher.notes}"> class="row-btn"
<i class="fa fa-lg fa-clone" aria-hidden="true"></i> appStopClick
</span> appStopProp
appA11yTitle="{{ 'copyNote' | i18n }}"
(click)="copy(cipher, cipher.notes, 'note', 'Note')"
[ngClass]="{ disabled: !cipher.notes }"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</span>
</ng-container> </ng-container>

View File

@ -1,82 +1,88 @@
import { import { Component, EventEmitter, Input, Output } from "@angular/core";
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from 'jslib-common/enums/cipherType'; import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from 'jslib-common/enums/eventType'; import { EventType } from "jslib-common/enums/eventType";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
@Component({ @Component({
selector: 'app-action-buttons', selector: "app-action-buttons",
templateUrl: 'action-buttons.component.html', templateUrl: "action-buttons.component.html",
}) })
export class ActionButtonsComponent { export class ActionButtonsComponent {
@Output() onView = new EventEmitter<CipherView>(); @Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>(); @Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView; @Input() cipher: CipherView;
@Input() showView = false; @Input() showView = false;
cipherType = CipherType; cipherType = CipherType;
userHasPremiumAccess = false; userHasPremiumAccess = false;
constructor(private i18nService: I18nService, constructor(
private platformUtilsService: PlatformUtilsService, private eventService: EventService, private i18nService: I18nService,
private totpService: TotpService, private userService: UserService, private platformUtilsService: PlatformUtilsService,
private passwordRepromptService: PasswordRepromptService) { } private eventService: EventService,
private totpService: TotpService,
private userService: UserService,
private passwordRepromptService: PasswordRepromptService
) {}
async ngOnInit() { async ngOnInit() {
this.userHasPremiumAccess = await this.userService.canAccessPremium(); this.userHasPremiumAccess = await this.userService.canAccessPremium();
}
launchCipher() {
this.launchEvent.emit(this.cipher);
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (
this.cipher.reprompt !== CipherRepromptType.None &&
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
} }
launchCipher() { if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
this.launchEvent.emit(this.cipher); return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
} }
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { if (!cipher.viewPassword) {
if (this.cipher.reprompt !== CipherRepromptType.None && this.passwordRepromptService.protectedFields().includes(aType) && return;
!await this.passwordRepromptService.showPasswordPrompt()) {
return;
}
if (value == null || aType === 'TOTP' && !this.displayTotpCopyButton(cipher)) {
return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
}
if (!cipher.viewPassword) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
if (typeI18nKey === 'password' || typeI18nKey === 'verificationCodeTotp') {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id);
} else if (typeI18nKey === 'securityCode') {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
} }
displayTotpCopyButton(cipher: CipherView) { this.platformUtilsService.copyToClipboard(value, { window: window });
return (cipher?.login?.hasTotp ?? false) && this.platformUtilsService.showToast(
(cipher.organizationUseTotp || this.userHasPremiumAccess); "info",
} null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
view() { if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") {
this.onView.emit(this.cipher); this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id);
} else if (typeI18nKey === "securityCode") {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
} }
}
displayTotpCopyButton(cipher: CipherView) {
return (
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
);
}
view() {
this.onView.emit(this.cipher);
}
} }

View File

@ -1,23 +1,38 @@
<button type="button" (click)="selectCipher(cipher)" (dblclick)="launchCipher(cipher)" appStopClick <button
title="{{title}} - {{cipher.name}}" class="box-content-row box-content-row-flex virtual-scroll-item"> type="button"
<div class="row-main"> (click)="selectCipher(cipher)"
<app-vault-icon [cipher]="cipher"></app-vault-icon> (dblclick)="launchCipher(cipher)"
<div class="row-main-content"> appStopClick
<span class="text"> title="{{ title }} - {{ cipher.name }}"
{{cipher.name}} class="box-content-row box-content-row-flex virtual-scroll-item"
<ng-container *ngIf="cipher.organizationId"> >
<i class="fa fa-cube text-muted" title="{{'shared' | i18n}}" aria-hidden="true"></i> <div class="row-main">
<span class="sr-only">{{'shared' | i18n}}</span> <app-vault-icon [cipher]="cipher"></app-vault-icon>
</ng-container> <div class="row-main-content">
<ng-container *ngIf="cipher.hasAttachments"> <span class="text">
<i class="fa fa-paperclip text-muted" title="{{'attachments' | i18n}}" aria-hidden="true"></i> {{ cipher.name }}
<span class="sr-only">{{'attachments' | i18n}}</span> <ng-container *ngIf="cipher.organizationId">
</ng-container> <i class="fa fa-cube text-muted" title="{{ 'shared' | i18n }}" aria-hidden="true"></i>
</span> <span class="sr-only">{{ "shared" | i18n }}</span>
<span class="detail">{{cipher.subTitle}}</span> </ng-container>
</div> <ng-container *ngIf="cipher.hasAttachments">
<i
class="fa fa-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
<span class="detail">{{ cipher.subTitle }}</span>
</div> </div>
<app-action-buttons [cipher]="cipher" [showView]="showView" (onView)="viewCipher(cipher)" (launchEvent)="launchCipher(cipher)" </div>
class="action-buttons"> <app-action-buttons
</app-action-buttons> [cipher]="cipher"
[showView]="showView"
(onView)="viewCipher(cipher)"
(launchEvent)="launchCipher(cipher)"
class="action-buttons"
>
</app-action-buttons>
</button> </button>

View File

@ -1,33 +1,28 @@
import { import { Component, EventEmitter, Input, Output } from "@angular/core";
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
@Component({ @Component({
selector: 'app-cipher-row', selector: "app-cipher-row",
templateUrl: 'cipher-row.component.html', templateUrl: "cipher-row.component.html",
}) })
export class CipherRowComponent { export class CipherRowComponent {
@Output() onSelected = new EventEmitter<CipherView>(); @Output() onSelected = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>(); @Output() launchEvent = new EventEmitter<CipherView>();
@Output() onView = new EventEmitter<CipherView>(); @Output() onView = new EventEmitter<CipherView>();
@Input() cipher: CipherView; @Input() cipher: CipherView;
@Input() showView = false; @Input() showView = false;
@Input() title: string; @Input() title: string;
selectCipher(c: CipherView) { selectCipher(c: CipherView) {
this.onSelected.emit(c); this.onSelected.emit(c);
} }
launchCipher(c: CipherView) { launchCipher(c: CipherView) {
this.launchEvent.emit(c); this.launchEvent.emit(c);
} }
viewCipher(c: CipherView) { viewCipher(c: CipherView) {
this.onView.emit(c); this.onView.emit(c);
} }
} }

View File

@ -1,38 +1,56 @@
<div class="modal fade" role="dialog" aria-modal="true"> <div class="modal fade" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()"> <form class="modal-content" #form (ngSubmit)="submit()">
<div class="modal-body"> <div class="modal-body">
<div class="box"> <div class="box">
<h1 class="box-header">{{'passwordConfirmation' | i18n}}</h1> <h1 class="box-header">{{ "passwordConfirmation" | i18n }}</h1>
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main"> <div class="row-main">
<label for="masterPassword">{{'masterPass' | i18n}}</label> <label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" <input
class="monospaced" [(ngModel)]="masterPassword" required appAutofocus> id="masterPassword"
</div> type="{{ showPassword ? 'text' : 'password' }}"
<div class="action-buttons"> name="MasterPassword"
<button type="button" class="row-btn" appStopClick appBlurClick role="button" class="monospaced"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()" [attr.aria-pressed]="showPassword"> [(ngModel)]="masterPassword"
<i class="fa fa-lg" aria-hidden="true" required
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i> appAutofocus
</button> />
</div> </div>
</div> <div class="action-buttons">
</div> <button
<div class="box-footer"> type="button"
{{'passwordConfirmationDesc' | i18n}} class="row-btn"
</div> appStopClick
</div> appBlurClick
</div> role="button"
<div class="modal-footer"> appA11yTitle="{{ 'toggleVisibility' | i18n }}"
<button type="submit" class="btn btn-primary btn-submit" appBlurClick> (click)="togglePassword()"
<span>{{'ok' | i18n}}</span> [attr.aria-pressed]="showPassword"
</button> >
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> <i
{{'cancel' | i18n}} class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button> </button>
</div>
</div> </div>
</form> </div>
</div> <div class="box-footer">
{{ "passwordConfirmationDesc" | i18n }}
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" appBlurClick>
<span>{{ "ok" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { PasswordRepromptComponent as BasePasswordRepromptComponent } from 'jslib-angular/components/password-reprompt.component'; import { PasswordRepromptComponent as BasePasswordRepromptComponent } from "jslib-angular/components/password-reprompt.component";
@Component({ @Component({
templateUrl: 'password-reprompt.component.html', templateUrl: "password-reprompt.component.html",
}) })
export class PasswordRepromptComponent extends BasePasswordRepromptComponent {} export class PasswordRepromptComponent extends BasePasswordRepromptComponent {}

View File

@ -1,5 +1,5 @@
<ng-container> <ng-container>
<button type="button" (click)="expand()" appA11yTitle="{{'popOutNewWindow' | i18n}}"> <button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}">
<i class="fa fa-external-link fa-rotate-270 fa-lg fa-fw" aria-hidden="true"></i> <i class="fa fa-external-link fa-rotate-270 fa-lg fa-fw" aria-hidden="true"></i>
</button> </button>
</ng-container> </ng-container>

View File

@ -1,32 +1,30 @@
import { import { Component, Input, OnInit } from "@angular/core";
Component,
Input,
OnInit,
} from '@angular/core';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PopupUtilsService } from '../services/popup-utils.service'; import { PopupUtilsService } from "../services/popup-utils.service";
@Component({ @Component({
selector: 'app-pop-out', selector: "app-pop-out",
templateUrl: 'pop-out.component.html', templateUrl: "pop-out.component.html",
}) })
export class PopOutComponent implements OnInit { export class PopOutComponent implements OnInit {
@Input() show = true; @Input() show = true;
constructor(private platformUtilsService: PlatformUtilsService, constructor(
private popupUtilsService: PopupUtilsService) { } private platformUtilsService: PlatformUtilsService,
private popupUtilsService: PopupUtilsService
) {}
ngOnInit() { ngOnInit() {
if (this.show) { if (this.show) {
if (this.popupUtilsService.inSidebar(window) && this.platformUtilsService.isFirefox()) { if (this.popupUtilsService.inSidebar(window) && this.platformUtilsService.isFirefox()) {
this.show = false; this.show = false;
} }
}
} }
}
expand() { expand() {
this.popupUtilsService.popOut(window); this.popupUtilsService.popOut(window);
} }
} }

View File

@ -1,50 +1,90 @@
<button type="button" *ngFor="let s of sends" (click)="selectSend(s)" appStopClick title="{{title}} - {{s.name}}" <button
class="box-content-row box-content-row-flex"> type="button"
<div class="row-main"> *ngFor="let s of sends"
<div class="app-vault-icon"> (click)="selectSend(s)"
<div class="icon" aria-hidden="true"> appStopClick
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type === sendType.Text"></i> title="{{ title }} - {{ s.name }}"
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type === sendType.File"></i> class="box-content-row box-content-row-flex"
</div> >
</div> <div class="row-main">
<div class="row-main-content"> <div class="app-vault-icon">
<span class="text"> <div class="icon" aria-hidden="true">
{{s.name}} <i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type === sendType.Text"></i>
<ng-container *ngIf="s.disabled"> <i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type === sendType.File"></i>
<i class="fa fa-warning text-muted" title="{{'disabled' | i18n}}" aria-hidden="true"></i> </div>
<span class="sr-only">{{'disabled' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i class="fa fa-key text-muted" title="{{'passwordProtected' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'passwordProtected' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i class="fa fa-ban text-muted" title="{{'maxAccessCountReached' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'maxAccessCountReached' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o text-muted" title="{{'expired' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'expired' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i class="fa fa-trash text-muted" title="{{'pendingDeletion' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'pendingDeletion' | i18n}}</span>
</ng-container>
</span>
<span class="detail">{{s.deletionDate | date:'medium'}}</span>
</div>
</div> </div>
<div class="action-buttons"> <div class="row-main-content">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copySendLink' | i18n}}" <span class="text">
(click)="copySendLink(s)"> {{ s.name }}
<i class="fa fa-lg fa-copy" aria-hidden="true"></i> <ng-container *ngIf="s.disabled">
</span> <i
<span class="row-btn" [ngClass]="{'disabled' : disabledByPolicy}" appStopClick appStopProp class="fa fa-warning text-muted"
appA11yTitle="{{'removePassword' | i18n}}" (click)="removePassword(s)" *ngIf="s.password"> title="{{ 'disabled' | i18n }}"
<i class="fa fa-lg fa-undo" aria-hidden="true"></i> aria-hidden="true"
</span> ></i>
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'delete' | i18n}}" (click)="delete(s)"> <span class="sr-only">{{ "disabled" | i18n }}</span>
<i class="fa fa-lg fa-trash-o" aria-hidden="true"></i> </ng-container>
</span> <ng-container *ngIf="s.password">
<i
class="fa fa-key text-muted"
title="{{ 'passwordProtected' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "passwordProtected" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i
class="fa fa-ban text-muted"
title="{{ 'maxAccessCountReached' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "maxAccessCountReached" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o text-muted" title="{{ 'expired' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "expired" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i
class="fa fa-trash text-muted"
title="{{ 'pendingDeletion' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "pendingDeletion" | i18n }}</span>
</ng-container>
</span>
<span class="detail">{{ s.deletionDate | date: "medium" }}</span>
</div> </div>
</div>
<div class="action-buttons">
<span
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copySendLink' | i18n }}"
(click)="copySendLink(s)"
>
<i class="fa fa-lg fa-copy" aria-hidden="true"></i>
</span>
<span
class="row-btn"
[ngClass]="{ disabled: disabledByPolicy }"
appStopClick
appStopProp
appA11yTitle="{{ 'removePassword' | i18n }}"
(click)="removePassword(s)"
*ngIf="s.password"
>
<i class="fa fa-lg fa-undo" aria-hidden="true"></i>
</span>
<span
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'delete' | i18n }}"
(click)="delete(s)"
>
<i class="fa fa-lg fa-trash-o" aria-hidden="true"></i>
</span>
</div>
</button> </button>

View File

@ -1,42 +1,37 @@
import { import { Component, EventEmitter, Input, Output } from "@angular/core";
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { SendView } from 'jslib-common/models/view/sendView'; import { SendView } from "jslib-common/models/view/sendView";
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
@Component({ @Component({
selector: 'app-send-list', selector: "app-send-list",
templateUrl: 'send-list.component.html', templateUrl: "send-list.component.html",
}) })
export class SendListComponent { export class SendListComponent {
@Input() sends: SendView[]; @Input() sends: SendView[];
@Input() title: string; @Input() title: string;
@Input() disabledByPolicy = false; @Input() disabledByPolicy = false;
@Output() onSelected = new EventEmitter<SendView>(); @Output() onSelected = new EventEmitter<SendView>();
@Output() onCopySendLink = new EventEmitter<SendView>(); @Output() onCopySendLink = new EventEmitter<SendView>();
@Output() onRemovePassword = new EventEmitter<SendView>(); @Output() onRemovePassword = new EventEmitter<SendView>();
@Output() onDeleteSend = new EventEmitter<SendView>(); @Output() onDeleteSend = new EventEmitter<SendView>();
sendType = SendType; sendType = SendType;
selectSend(s: SendView) { selectSend(s: SendView) {
this.onSelected.emit(s); this.onSelected.emit(s);
} }
copySendLink(s: SendView) { copySendLink(s: SendView) {
this.onCopySendLink.emit(s); this.onCopySendLink.emit(s);
} }
removePassword(s: SendView) { removePassword(s: SendView) {
this.onRemovePassword.emit(s); this.onRemovePassword.emit(s);
} }
delete(s: SendView) { delete(s: SendView) {
this.onDeleteSend.emit(s); this.onDeleteSend.emit(s);
} }
} }

View File

@ -1,44 +1,65 @@
<div class="modal fade" role="dialog" aria-modal="true"> <div class="modal fade" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()"> <form class="modal-content" #form (ngSubmit)="submit()">
<div class="modal-body"> <div class="modal-body">
<div> <div>
{{'setYourPinCode' | i18n}} {{ "setYourPinCode" | i18n }}
</div> </div>
<div class="box"> <div class="box">
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main"> <div class="row-main">
<label for="pin">{{'pin' | i18n}}</label> <label for="pin">{{ "pin" | i18n }}</label>
<input id="pin" type="{{showPin ? 'text' : 'password'}}" name="Pin" <input
class="monospaced" [(ngModel)]="pin" required appInputVerbatim> id="pin"
</div> type="{{ showPin ? 'text' : 'password' }}"
<div class="action-buttons"> name="Pin"
<button type="button" class="row-btn" appStopClick appBlurClick appA11yTitle="{{'toggleVisibility' | i18n}}" class="monospaced"
(click)="toggleVisibility()" [attr.aria-pressed]="showPin"> [(ngModel)]="pin"
<i class="fa fa-lg" aria-hidden="true" required
[ngClass]="{'fa-eye': !showPin, 'fa-eye-slash': showPin}"></i> appInputVerbatim
</button> />
</div> </div>
</div> <div class="action-buttons">
</div> <button
</div> type="button"
<div class="checkbox" *ngIf="showMasterPassOnRestart"> class="row-btn"
<label for="masterPasswordOnRestart"> appStopClick
<input type="checkbox" id="masterPasswordOnRestart" name="MasterPasswordOnRestart" appBlurClick
[(ngModel)]="masterPassOnRestart"> appA11yTitle="{{ 'toggleVisibility' | i18n }}"
<span>{{'lockWithMasterPassOnRestart' | i18n}}</span> (click)="toggleVisibility()"
</label> [attr.aria-pressed]="showPin"
</div> >
</div> <i
<div class="modal-footer"> class="fa fa-lg"
<button type="submit" class="btn btn-primary btn-submit" appBlurClick> aria-hidden="true"
<span>{{'ok' | i18n}}</span> [ngClass]="{ 'fa-eye': !showPin, 'fa-eye-slash': showPin }"
</button> ></i>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button> </button>
</div>
</div> </div>
</form> </div>
</div> </div>
<div class="checkbox" *ngIf="showMasterPassOnRestart">
<label for="masterPasswordOnRestart">
<input
type="checkbox"
id="masterPasswordOnRestart"
name="MasterPasswordOnRestart"
[(ngModel)]="masterPassOnRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" appBlurClick>
<span>{{ "ok" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { SetPinComponent as BaseSetPinComponent } from 'jslib-angular/components/set-pin.component'; import { SetPinComponent as BaseSetPinComponent } from "jslib-angular/components/set-pin.component";
@Component({ @Component({
templateUrl: 'set-pin.component.html', templateUrl: "set-pin.component.html",
}) })
export class SetPinComponent extends BaseSetPinComponent {} export class SetPinComponent extends BaseSetPinComponent {}

View File

@ -1,25 +1,46 @@
<ng-container *ngIf="!usesKeyConnector"> <ng-container *ngIf="!usesKeyConnector">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="masterPassword">{{'masterPass' | i18n}}</label> <label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" <input
[formControl]="secret" required appAutofocus appInputVerbatim> id="masterPassword"
</div> type="password"
name="MasterPasswordHash"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
</div>
</ng-container> </ng-container>
<ng-container *ngIf="usesKeyConnector"> <ng-container *ngIf="usesKeyConnector">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label class="d-block">{{'sendVerificationCode' | i18n}}</label> <label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button type="button" class="btn btn-outline-secondary" (click)="requestOTP()" [disabled]="disableRequestOTP"> <button
{{'sendCode' | i18n}} type="button"
</button> class="btn btn-outline-secondary"
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode"> (click)="requestOTP()"
<i class="fa fa-check-circle-o" aria-hidden="true"></i> [disabled]="disableRequestOTP"
{{'codeSent' | i18n}} >
</span> {{ "sendCode" | i18n }}
</div> </button>
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
<i class="fa fa-check-circle-o" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="verificationCode">{{'verificationCode' | i18n}}</label> <label for="verificationCode">{{ "verificationCode" | i18n }}</label>
<input id="verificationCode" type="input" name="verificationCode" class="form-control" <input
[formControl]="secret" required appAutofocus appInputVerbatim> id="verificationCode"
</div> type="input"
name="verificationCode"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
</div>
</ng-container> </ng-container>

View File

@ -1,31 +1,23 @@
import { import { animate, style, transition, trigger } from "@angular/animations";
animate, import { Component } from "@angular/core";
style, import { NG_VALUE_ACCESSOR } from "@angular/forms";
transition,
trigger,
} from '@angular/animations';
import { Component } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { VerifyMasterPasswordComponent as BaseComponent } from 'jslib-angular/components/verify-master-password.component'; import { VerifyMasterPasswordComponent as BaseComponent } from "jslib-angular/components/verify-master-password.component";
@Component({ @Component({
selector: 'app-verify-master-password', selector: "app-verify-master-password",
templateUrl: 'verify-master-password.component.html', templateUrl: "verify-master-password.component.html",
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
multi: true, multi: true,
useExisting: VerifyMasterPasswordComponent, useExisting: VerifyMasterPasswordComponent,
}, },
], ],
animations: [ animations: [
trigger('sent', [ trigger("sent", [
transition(':enter', [ transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
style({ opacity: 0 }), ]),
animate('100ms', style({ opacity: 1 })), ],
]),
]),
],
}) })
export class VerifyMasterPasswordComponent extends BaseComponent { } export class VerifyMasterPasswordComponent extends BaseComponent {}

View File

@ -1,40 +1,48 @@
<header> <header>
<div class="left"> <div class="left">
<button type="button" appBlurClick type="button" (click)="close()"> <button type="button" appBlurClick type="button" (click)="close()">
<span class="header-icon" aria-hidden="true"><i class="fa fa-chevron-left"></i></span> <span class="header-icon" aria-hidden="true"><i class="fa fa-chevron-left"></i></span>
<span>{{'back' | i18n}}</span> <span>{{ "back" | i18n }}</span>
</button> </button>
</div> </div>
<h1 class="center"> <h1 class="center">
<span class="title">{{'passwordHistory' | i18n}}</span> <span class="title">{{ "passwordHistory" | i18n }}</span>
</h1> </h1>
<div class="right"> <div class="right">
<button type="button" appBlurClick type="button" (click)="clear()"> <button type="button" appBlurClick type="button" (click)="clear()">
{{'clear' | i18n}} {{ "clear" | i18n }}
</button> </button>
</div> </div>
</header> </header>
<content> <content>
<div class="box list full-list" *ngIf="history && history.length"> <div class="box list full-list" *ngIf="history && history.length">
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history"> <div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main"> <div class="row-main">
<div class="row-main-content"> <div class="row-main-content">
<div class="monospaced password-wrapper" appSelectCopy <div
[innerHTML]="h.password | colorPassword"></div> class="monospaced password-wrapper"
<span class="detail">{{h.date | date:'medium'}}</span> appSelectCopy
</div> [innerHTML]="h.password | colorPassword"
</div> ></div>
<div class="action-buttons"> <span class="detail">{{ h.date | date: "medium" }}</span>
<button type="button" class="row-btn" appStopClick appA11yTitle="{{'copyPassword' | i18n}}" </div>
(click)="copy(h.password)">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div> </div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(h.password)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div> </div>
<div class="no-items" *ngIf="!history || !history.length"> </div>
<p>{{'noPasswordsInList' | i18n}}</p> <div class="no-items" *ngIf="!history || !history.length">
</div> <p>{{ "noPasswordsInList" | i18n }}</p>
</div>
</content> </content>

View File

@ -1,25 +1,27 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "jslib-angular/components/password-generator-history.component";
PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent,
} from 'jslib-angular/components/password-generator-history.component';
@Component({ @Component({
selector: 'app-password-generator-history', selector: "app-password-generator-history",
templateUrl: 'password-generator-history.component.html', templateUrl: "password-generator-history.component.html",
}) })
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent { export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, private location: Location) { passwordGenerationService: PasswordGenerationService,
super(passwordGenerationService, platformUtilsService, i18nService, window); platformUtilsService: PlatformUtilsService,
} i18nService: I18nService,
private location: Location
) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
close() { close() {
this.location.back(); this.location.back();
} }
} }

View File

@ -1,128 +1,229 @@
<header> <header>
<div class="left"> <div class="left">
<app-pop-out [show]="!showSelect"></app-pop-out> <app-pop-out [show]="!showSelect"></app-pop-out>
<button type="button" appBlurClick (click)="close()" *ngIf="showSelect">{{'cancel' | i18n}}</button> <button type="button" appBlurClick (click)="close()" *ngIf="showSelect">
</div> {{ "cancel" | i18n }}
<h1 class="center"> </button>
<span class="title">{{'passGen' | i18n}}</span> </div>
</h1> <h1 class="center">
<div class="right"> <span class="title">{{ "passGen" | i18n }}</span>
<button type="button" appBlurClick (click)="select()" *ngIf="showSelect">{{'select' | i18n}}</button> </h1>
</div> <div class="right">
<button type="button" appBlurClick (click)="select()" *ngIf="showSelect">
{{ "select" | i18n }}
</button>
</div>
</header> </header>
<content> <content>
<app-callout type="info" *ngIf="enforcedPolicyOptions?.inEffect()"> <app-callout type="info" *ngIf="enforcedPolicyOptions?.inEffect()">
{{'passwordGeneratorPolicyInEffect' | i18n}} {{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout> </app-callout>
<div class="password-block"> <div class="password-block">
<div class="password-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div> <div class="password-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
</div>
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row text-primary"
appStopClick
appBlurClick
(click)="regenerate()"
>
{{ "regeneratePassword" | i18n }}
</button>
<button
type="button"
class="box-content-row text-primary"
appStopClick
appBlurClick
(click)="copy()"
>
{{ "copyPassword" | i18n }}
</button>
</div> </div>
<div class="box list"> </div>
<div class="box-content single-line"> <div class="box list">
<button type="button" class="box-content-row text-primary" appStopClick appBlurClick <div class="box-content single-line">
(click)="regenerate()">{{'regeneratePassword' | i18n}}</button> <a class="box-content-row box-content-row-flex" routerLink="/generator-history">
<button type="button" class="box-content-row text-primary" appStopClick appBlurClick <div class="row-main">{{ "passwordHistory" | i18n }}</div>
(click)="copy()">{{'copyPassword' | i18n}}</button> <i class="fa fa-chevron-right fa-lg row-sub-icon" aria-hidden="true"></i>
</div> </a>
</div> </div>
<div class="box list"> </div>
<div class="box-content single-line"> <div class="box">
<a class="box-content-row box-content-row-flex" routerLink="/generator-history"> <h2 class="box-header">
<div class="row-main">{{'passwordHistory' | i18n}}</div> {{ "options" | i18n }}
<i class="fa fa-chevron-right fa-lg row-sub-icon" aria-hidden="true"></i> </h2>
</a> <div class="box-content">
<div class="box-content-row">
<label class="sr-only radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions">
<input
type="radio"
[(ngModel)]="options.type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="saveOptions()"
[checked]="options.type === o.value"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
</label>
</div> </div>
</div>
</div>
</div>
<div class="box" *ngIf="options.type === 'passphrase'">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
type="number"
min="3"
max="20"
(change)="saveOptions()"
[(ngModel)]="options.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
type="text"
maxlength="1"
(input)="saveOptions()"
[(ngModel)]="options.wordSeparator"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="options.capitalize"
[disabled]="enforcedPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="options.includeNumber"
[disabled]="enforcedPolicyOptions?.includeNumber"
/>
</div>
</div>
</div>
<ng-container *ngIf="options.type === 'password'">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
type="number"
min="5"
max="128"
[(ngModel)]="options.length"
(change)="saveOptions()"
/>
<input
id="lengthRange"
type="range"
min="5"
max="128"
step="1"
[(ngModel)]="options.length"
(change)="sliderChanged()"
(input)="sliderInput()"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input
id="uppercase"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'uppercase' | i18n }}"
[disabled]="enforcedPolicyOptions.useUppercase"
[(ngModel)]="options.uppercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input
id="lowercase"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'lowercase' | i18n }}"
[disabled]="enforcedPolicyOptions.useLowercase"
[(ngModel)]="options.lowercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'numbers' | i18n }}"
[disabled]="enforcedPolicyOptions.useNumbers"
[(ngModel)]="options.number"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!@#$%^&*</label>
<input
id="special"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
[disabled]="enforcedPolicyOptions.useSpecial"
[(ngModel)]="options.special"
/>
</div>
</div>
</div> </div>
<div class="box"> <div class="box">
<h2 class="box-header"> <div class="box-content">
{{'options' | i18n}} <div class="box-content-row box-content-row-input" appBoxRow>
</h2> <label for="min-number">{{ "minNumbers" | i18n }}</label>
<div class="box-content"> <input
<div class="box-content-row"> id="min-number"
<label class="sr-only radio-header">{{'type' | i18n}}</label> type="number"
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions"> min="0"
<input type="radio" [(ngModel)]="options.type" name="Type_{{o.value}}" id="type_{{o.value}}" max="9"
[value]="o.value" (change)="saveOptions()" [checked]="options.type === o.value"> (change)="saveOptions()"
<label for="type_{{o.value}}"> [(ngModel)]="options.minNumber"
{{o.name}} />
</label>
</div>
</div>
</div> </div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
type="number"
min="0"
max="9"
(change)="saveOptions()"
[(ngModel)]="options.minSpecial"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "avoidAmbChar" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div> </div>
<div class="box" *ngIf="options.type === 'passphrase'"> </ng-container>
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{'numWords' | i18n}}</label>
<input id="num-words" type="number" min="3" max="20" (change)="saveOptions()"
[(ngModel)]="options.numWords">
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{'wordSeparator' | i18n}}</label>
<input id="word-separator" type="text" maxlength="1" (input)="saveOptions()"
[(ngModel)]="options.wordSeparator">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{'capitalize' | i18n}}</label>
<input id="capitalize" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.capitalize"
[disabled]="enforcedPolicyOptions?.capitalize">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{'includeNumber' | i18n}}</label>
<input id="include-number" type="checkbox" (change)="saveOptions()" [(ngModel)]="options.includeNumber"
[disabled]="enforcedPolicyOptions?.includeNumber">
</div>
</div>
</div>
<ng-container *ngIf="options.type === 'password'">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{'length' | i18n}}</label>
<input id="length" type="number" min="5" max="128" [(ngModel)]="options.length"
(change)="saveOptions()">
<input id="lengthRange" type="range" min="5" max="128" step="1" [(ngModel)]="options.length"
(change)="sliderChanged()" (input)="sliderInput()">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input id="uppercase" type="checkbox" (change)="saveOptions()" attr.aria-label="{{'uppercase' | i18n}}"
[disabled]="enforcedPolicyOptions.useUppercase" [(ngModel)]="options.uppercase">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input id="lowercase" type="checkbox" (change)="saveOptions()" attr.aria-label="{{'lowercase' | i18n}}"
[disabled]="enforcedPolicyOptions.useLowercase" [(ngModel)]="options.lowercase">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input id="numbers" type="checkbox" (change)="saveOptions()" attr.aria-label="{{'numbers' | i18n}}"
[disabled]="enforcedPolicyOptions.useNumbers" [(ngModel)]="options.number">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!@#$%^&*</label>
<input id="special" type="checkbox" (change)="saveOptions()" attr.aria-label="{{'specialCharacters' | i18n}}"
[disabled]="enforcedPolicyOptions.useSpecial" [(ngModel)]="options.special">
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{'minNumbers' | i18n}}</label>
<input id="min-number" type="number" min="0" max="9" (change)="saveOptions()"
[(ngModel)]="options.minNumber">
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{'minSpecial' | i18n}}</label>
<input id="min-special" type="number" min="0" max="9" (change)="saveOptions()"
[(ngModel)]="options.minSpecial">
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{'avoidAmbChar' | i18n}}</label>
<input id="ambiguous" type="checkbox" (change)="saveOptions()" [(ngModel)]="avoidAmbiguous">
</div>
</div>
</div>
</ng-container>
</content> </content>

View File

@ -1,46 +1,48 @@
import { Location } from '@angular/common'; import { Location } from "@angular/common";
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component";
PasswordGeneratorComponent as BasePasswordGeneratorComponent,
} from 'jslib-angular/components/password-generator.component';
@Component({ @Component({
selector: 'app-password-generator', selector: "app-password-generator",
templateUrl: 'password-generator.component.html', templateUrl: "password-generator.component.html",
}) })
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent { export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
private cipherState: CipherView; private cipherState: CipherView;
constructor(passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, constructor(
i18nService: I18nService, private stateService: StateService, passwordGenerationService: PasswordGenerationService,
private location: Location) { platformUtilsService: PlatformUtilsService,
super(passwordGenerationService, platformUtilsService, i18nService, window); i18nService: I18nService,
} private stateService: StateService,
private location: Location
) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
const addEditCipherInfo = await this.stateService.get<any>('addEditCipherInfo'); const addEditCipherInfo = await this.stateService.get<any>("addEditCipherInfo");
if (addEditCipherInfo != null) { if (addEditCipherInfo != null) {
this.cipherState = addEditCipherInfo.cipher; this.cipherState = addEditCipherInfo.cipher;
}
this.showSelect = this.cipherState != null;
} }
this.showSelect = this.cipherState != null;
}
select() { select() {
super.select(); super.select();
this.cipherState.login.password = this.password; this.cipherState.login.password = this.password;
this.close(); this.close();
} }
close() { close() {
this.location.back(); this.location.back();
} }
} }

View File

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html class="__BROWSER__"> <html class="__BROWSER__">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bitwarden</title> <title>Bitwarden</title>
<base href=""> <base href="" />
</head> </head>
<body> <body>
<app-root> <app-root>
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x" aria-hidden="true"></i></div> <div id="loading"><i class="fa fa-spinner fa-spin fa-3x" aria-hidden="true"></i></div>
</app-root> </app-root>
</body> </body>
</html> </html>

View File

@ -1,17 +1,17 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
// tslint:disable-next-line // tslint:disable-next-line
require('./scss/popup.scss'); require("./scss/popup.scss");
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
if (process.env.ENV === 'production') { if (process.env.ENV === "production") {
enableProdMode(); enableProdMode();
} }
function init() { function init() {
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
} }
init(); init();

View File

@ -1,6 +1,6 @@
/* tslint:disable */ /* tslint:disable */
import 'core-js/stable'; import "core-js/stable";
import 'date-input-polyfill'; import "date-input-polyfill";
import 'web-animations-js'; import "web-animations-js";
import 'zone.js/dist/zone'; import "zone.js/dist/zone";
/* tslint:enable */ /* tslint:enable */

View File

@ -1,6 +1,6 @@
<div class="content"> <div class="content">
<p class="text-center">{{privateModeMessage}}</p> <p class="text-center">{{ privateModeMessage }}</p>
<button type="button" class="btn primary block" (click)="learnMore()"> <button type="button" class="btn primary block" (click)="learnMore()">
<b>{{learnMoreMessage}}</b> <b>{{ learnMoreMessage }}</b>
</button> </button>
</div> </div>

View File

@ -1,24 +1,23 @@
import { BrowserApi } from '../browser/browserApi'; import { BrowserApi } from "../browser/browserApi";
import { import { Component, OnInit } from "@angular/core";
Component,
OnInit,
} from '@angular/core';
@Component({ @Component({
selector: 'app-private-mode', selector: "app-private-mode",
templateUrl: 'private-mode.component.html', templateUrl: "private-mode.component.html",
}) })
export class PrivateModeComponent implements OnInit { export class PrivateModeComponent implements OnInit {
privateModeMessage: string; privateModeMessage: string;
learnMoreMessage: string; learnMoreMessage: string;
ngOnInit() { ngOnInit() {
this.privateModeMessage = chrome.i18n.getMessage('privateModeMessage'); this.privateModeMessage = chrome.i18n.getMessage("privateModeMessage");
this.learnMoreMessage = chrome.i18n.getMessage('learnMore'); this.learnMoreMessage = chrome.i18n.getMessage("learnMore");
} }
learnMore() { learnMore() {
BrowserApi.createNewTab('https://help.bitwarden.com/article/extension-wont-load-in-private-mode/'); BrowserApi.createNewTab(
} "https://help.bitwarden.com/article/extension-wont-load-in-private-mode/"
);
}
} }

View File

@ -1,438 +1,461 @@
@import "variables.scss"; @import "variables.scss";
* { * {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
html, body { html,
font-family: $font-family-sans-serif; body {
font-size: $font-size-base; font-family: $font-family-sans-serif;
line-height: $line-height-base; font-size: $font-size-base;
-webkit-font-smoothing: antialiased; line-height: $line-height-base;
-webkit-font-smoothing: antialiased;
} }
body { body {
width: 375px !important;
height: 600px !important;
overflow: hidden;
color: $text-color;
background-color: $background-color;
@include themify($themes) {
color: themed("textColor");
background-color: themed("backgroundColor");
}
&.body-sm {
width: 375px !important; width: 375px !important;
height: 600px !important; height: 500px !important;
overflow: hidden; }
color: $text-color;
background-color: $background-color;
@include themify($themes) { &.body-xs {
color: themed('textColor'); width: 375px !important;
background-color: themed('backgroundColor'); height: 300px !important;
} }
&.body-sm { &.body-full {
width: 375px !important; width: 100% !important;
height: 500px !important; height: 100% !important;
} }
&.body-xs {
width: 375px !important;
height: 300px !important;
}
&.body-full {
width: 100% !important;
height: 100% !important;
}
} }
h1, h2, h3, h4, h5, h6 { h1,
font-family: $font-family-sans-serif; h2,
font-size: $font-size-base; h3,
font-weight: normal; h4,
h5,
h6 {
font-family: $font-family-sans-serif;
font-size: $font-size-base;
font-weight: normal;
} }
p { p {
margin-bottom: 10px; margin-bottom: 10px;
} }
ul, ol { ul,
margin-bottom: 10px; ol {
margin-bottom: 10px;
} }
img { img {
border: none; border: none;
} }
a { a {
text-decoration: none; text-decoration: none;
@include themify($themes) {
color: themed("primaryColor");
}
&:hover,
&:focus {
@include themify($themes) { @include themify($themes) {
color: themed('primaryColor'); color: darken(themed("primaryColor"), 6%);
}
&:hover, &:focus {
@include themify($themes) {
color: darken(themed('primaryColor'), 6%);
}
} }
}
} }
input, select, textarea { input,
@include themify($themes) { select,
color: themed('textColor'); textarea {
background-color: themed('inputBackgroundColor'); @include themify($themes) {
} color: themed("textColor");
background-color: themed("inputBackgroundColor");
}
} }
input, select, textarea, button { input,
font-size: $font-size-base; select,
font-family: $font-family-sans-serif; textarea,
button {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
} }
button { button {
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
} }
textarea { textarea {
resize: vertical; resize: vertical;
} }
main { main {
height: 100%; height: 100%;
} }
content::-webkit-scrollbar, cdk-virtual-scroll-viewport::-webkit-scrollbar { content::-webkit-scrollbar,
width: 10px; cdk-virtual-scroll-viewport::-webkit-scrollbar {
height: 10px; width: 10px;
height: 10px;
} }
content::-webkit-scrollbar-track { content::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
} }
cdk-virtual-scroll-viewport::-webkit-scrollbar-track { cdk-virtual-scroll-viewport::-webkit-scrollbar-track {
@include themify($themes) { @include themify($themes) {
background-color: themed('backgroundColor'); background-color: themed("backgroundColor");
} }
} }
content::-webkit-scrollbar-thumb, cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb { content::-webkit-scrollbar-thumb,
border-radius: 10px; cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb {
margin-right: 1px; border-radius: 10px;
margin-right: 1px;
@include themify($themes) {
background-color: themed("scrollbarColor");
}
&:hover {
@include themify($themes) { @include themify($themes) {
background-color: themed('scrollbarColor'); background-color: themed("scrollbarHoverColor");
}
&:hover {
@include themify($themes) {
background-color: themed('scrollbarHoverColor');
}
} }
}
} }
header { header {
min-height: 44px; min-height: 44px;
max-height: 44px; max-height: 44px;
display: flex;
border-bottom: 1px solid #000000;
@include themify($themes) {
color: themed("headerColor");
background-color: themed("headerBackgroundColor");
border-bottom-color: themed("headerBorderColor");
}
.left,
.right {
flex: 1;
display: flex; display: flex;
border-bottom: 1px solid #000000; min-width: -webkit-min-content; /* Workaround to Chrome bug */
.header-icon {
margin-right: 5px;
}
}
.right {
justify-content: flex-end;
}
.center {
display: flex;
align-items: center;
text-align: center;
min-width: 0;
}
app-pop-out > button,
div > button,
div > a {
border: none;
padding: 0 10px;
text-decoration: none;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
@include themify($themes) { @include themify($themes) {
color: themed('headerColor'); color: themed("headerColor");
background-color: themed('headerBackgroundColor'); background-color: themed("headerBackgroundColor");
border-bottom-color: themed('headerBorderColor');
} }
.left, .right { &:hover,
flex: 1; &:focus {
display: flex; @include themify($themes) {
min-width: -webkit-min-content; /* Workaround to Chrome bug */ background-color: themed("headerBackgroundHoverColor");
.header-icon { color: themed("headerColor");
margin-right: 5px; }
}
} }
.right { &:focus {
justify-content: flex-end; text-decoration: underline;
} }
.center { &[disabled] {
display: flex; opacity: 0.65;
align-items: center; cursor: default !important;
text-align: center;
min-width: 0;
} }
app-pop-out > button, div > button, div > a { i + span {
border: none; margin-left: 5px;
padding: 0 10px; }
text-decoration: none; }
display: flex;
flex-direction: row; app-pop-out {
justify-content: center; display: flex;
align-items: center; }
.title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search {
padding: 7px 10px;
width: 100%;
text-align: left;
position: relative;
display: flex;
.fa {
position: absolute;
top: 15px;
left: 20px;
@include themify($themes) {
color: themed("headerInputPlaceholderColor");
}
}
input {
width: 100%;
margin: 0;
border: none;
padding: 5px 10px 5px 30px;
border-radius: $border-radius;
@include themify($themes) {
background-color: themed("headerInputBackgroundColor");
color: themed("headerInputColor");
}
&:focus {
border-radius: $border-radius;
outline: none;
@include themify($themes) { @include themify($themes) {
color: themed('headerColor'); background-color: themed("headerInputBackgroundFocusColor");
background-color: themed('headerBackgroundColor');
} }
}
&:hover, &:focus { &::-webkit-input-placeholder {
@include themify($themes) { @include themify($themes) {
background-color: themed('headerBackgroundHoverColor'); color: themed("headerInputPlaceholderColor");
color: themed('headerColor');
}
}
&:focus {
text-decoration: underline;
}
&[disabled] {
opacity: 0.65;
cursor: default !important;
}
i + span {
margin-left: 5px;
} }
}
} }
}
app-pop-out { .left + .search {
display: flex; padding-left: 0;
.fa {
left: 10px;
} }
}
.title { .search + .right {
font-weight: bold; margin-left: -10px;
white-space: nowrap; }
overflow: hidden;
text-overflow: ellipsis;
}
.search {
padding: 7px 10px;
width: 100%;
text-align: left;
position: relative;
display: flex;
.fa {
position: absolute;
top: 15px;
left: 20px;
@include themify($themes) {
color: themed('headerInputPlaceholderColor');
}
}
input {
width: 100%;
margin: 0;
border: none;
padding: 5px 10px 5px 30px;
border-radius: $border-radius;
@include themify($themes) {
background-color: themed('headerInputBackgroundColor');
color: themed('headerInputColor');
}
&:focus {
border-radius: $border-radius;
outline: none;
@include themify($themes) {
background-color: themed('headerInputBackgroundFocusColor');
}
}
&::-webkit-input-placeholder {
@include themify($themes) {
color: themed('headerInputPlaceholderColor');
}
}
}
}
.left + .search {
padding-left: 0;
.fa {
left: 10px;
}
}
.search + .right {
margin-left: -10px;
}
} }
.content { .content {
padding: 15px; padding: 15px;
} }
.tabs { .tabs {
width: 100%; width: 100%;
height: 55px; height: 55px;
border-top: 1px solid #000000; border-top: 1px solid #000000;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
overflow: hidden; overflow: hidden;
@include themify($themes) { @include themify($themes) {
background-color: themed('tabBackgroundColor'); background-color: themed("tabBackgroundColor");
border-top-color: themed('borderColor'); border-top-color: themed("borderColor");
} }
ul { ul {
display: flex; display: flex;
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
li { li {
flex: 1; flex: 1;
display: inline-block; display: inline-block;
padding: 0; padding: 0;
margin: 0; margin: 0;
a { a {
text-align: center; text-align: center;
display: block; display: block;
padding: 7px 0; padding: 7px 0;
text-decoration: none; text-decoration: none;
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@include themify($themes) { @include themify($themes) {
color: themed('mutedColor'); color: themed("mutedColor");
}
&:hover, &:focus {
@include themify($themes) {
background-color: themed('tabBackgroundHoverColor');
}
}
i {
display: block;
margin-bottom: 2px;
text-align: center;
}
}
&.active {
a {
@include themify($themes) {
color: themed('primaryColor');
}
}
}
} }
&:hover,
&:focus {
@include themify($themes) {
background-color: themed("tabBackgroundHoverColor");
}
}
i {
display: block;
margin-bottom: 2px;
text-align: center;
}
}
&.active {
a {
@include themify($themes) {
color: themed("primaryColor");
}
}
}
} }
}
} }
app-root { app-root {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 980; z-index: 980;
@include themify($themes) { @include themify($themes) {
background-color: themed('backgroundColor'); background-color: themed("backgroundColor");
} }
} }
@media only screen and (min-width: 601px) { @media only screen and (min-width: 601px) {
app-login header { app-login header {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
app-login content { app-login content {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
app-two-factor header { app-two-factor header {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
app-two-factor content { app-two-factor content {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
app-lock header { app-lock header {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
app-lock content { app-lock content {
padding: 0 calc((100% - 500px) / 2); padding: 0 calc((100% - 500px) / 2);
} }
} }
content { content {
position: absolute; position: absolute;
top: 44px; top: 44px;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@include themify($themes) { @include themify($themes) {
background-color: themed('backgroundColor'); background-color: themed("backgroundColor");
} }
&.no-header { &.no-header {
top: 0; top: 0;
} }
&.flex { &.flex {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
height: calc(100% - 44px); height: calc(100% - 44px);
&.tab-page { &.tab-page {
height: calc(100% - 99px); height: calc(100% - 99px);
}
} }
}
} }
.tab-page { .tab-page {
content { content {
bottom: 55px; bottom: 55px;
} }
} }
.center-content, .no-items, .full-loading-spinner { .center-content,
display: flex; .no-items,
justify-content: center; .full-loading-spinner {
align-items: center; display: flex;
height: 100%; justify-content: center;
flex-direction: column; align-items: center;
flex-grow: 1; height: 100%;
flex-direction: column;
flex-grow: 1;
} }
.no-items, .full-loading-spinner { .no-items,
text-align: center; .full-loading-spinner {
text-align: center;
.fa { .fa {
margin-bottom: 10px; margin-bottom: 10px;
@include themify($themes) { @include themify($themes) {
color: themed('disabledIconColor'); color: themed("disabledIconColor");
}
} }
}
} }
// cdk-virtual-scroll // cdk-virtual-scroll
.cdk-virtual-scroll-viewport { .cdk-virtual-scroll-viewport {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
} }
.cdk-virtual-scroll-content-wrapper { .cdk-virtual-scroll-content-wrapper {
width: 100%; width: 100%;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +1,101 @@
@import "variables.scss"; @import "variables.scss";
.btn { .btn {
border-radius: $border-radius; border-radius: $border-radius;
padding: 7px 15px; padding: 7px 15px;
border: 1px solid #000000; border: 1px solid #000000;
font-size: $font-size-base; font-size: $font-size-base;
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
cursor: pointer;
@include themify($themes) {
background-color: themed("buttonBackgroundColor");
border-color: themed("buttonBorderColor");
color: themed("buttonColor");
}
&.primary {
@include themify($themes) {
color: themed("buttonPrimaryColor");
}
}
&.danger {
@include themify($themes) {
color: themed("buttonDangerColor");
}
}
&:hover:not([disabled]) {
cursor: pointer; cursor: pointer;
@include themify($themes) { @include themify($themes) {
background-color: themed('buttonBackgroundColor'); background-color: darken(themed("buttonBackgroundColor"), 1.5%);
border-color: themed('buttonBorderColor'); border-color: darken(themed("buttonBorderColor"), 17%);
color: themed('buttonColor'); color: darken(themed("buttonColor"), 10%);
} }
&.primary { &.primary {
@include themify($themes) { @include themify($themes) {
color: themed('buttonPrimaryColor'); color: darken(themed("buttonPrimaryColor"), 6%);
} }
} }
&.danger { &.danger {
@include themify($themes) { @include themify($themes) {
color: themed('buttonDangerColor'); color: darken(themed("buttonDangerColor"), 6%);
} }
} }
}
&:hover:not([disabled]) { &:focus:not([disabled]) {
cursor: pointer; cursor: pointer;
outline: 0;
@include themify($themes) { @include themify($themes) {
background-color: darken(themed('buttonBackgroundColor'), 1.5%); background-color: darken(themed("buttonBackgroundColor"), 6%);
border-color: darken(themed('buttonBorderColor'), 17%); border-color: darken(themed("buttonBorderColor"), 25%);
color: darken(themed('buttonColor'), 10%);
}
&.primary {
@include themify($themes) {
color: darken(themed('buttonPrimaryColor'), 6%);
}
}
&.danger {
@include themify($themes) {
color: darken(themed('buttonDangerColor'), 6%);
}
}
} }
}
&:focus:not([disabled]) { &[disabled] {
cursor: pointer; opacity: 0.65;
outline: 0; cursor: default !important;
}
@include themify($themes) { &.block {
background-color: darken(themed('buttonBackgroundColor'), 6%); display: block;
border-color: darken(themed('buttonBorderColor'), 25%); width: 100%;
} }
}
&.link,
&[disabled] { &.neutral {
opacity: 0.65; border: none !important;
cursor: default !important; background: none !important;
}
&:focus {
&.block { text-decoration: underline;
display: block;
width: 100%;
}
&.link,
&.neutral {
border: none !important;
background: none !important;
&:focus {
text-decoration: underline;
}
} }
}
} }
.action-buttons { .action-buttons {
.btn { .btn {
&:focus { &:focus {
outline: auto; outline: auto;
}
} }
}
} }
button.box-content-row { button.box-content-row {
display: block; display: block;
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
button { button {
border: none; border: none;
background: transparent; background: transparent;
color: inherit; color: inherit;
} }

View File

@ -1,39 +1,39 @@
@import "variables.scss"; @import "variables.scss";
html.browser_safari { html.browser_safari {
body { body {
height: 360px !important; height: 360px !important;
&.body-xs { &.body-xs {
height: 300px !important; height: 300px !important;
}
&.body-full {
height: 100% !important;
}
} }
header { &.body-full {
.search .fa { height: 100% !important;
left: 20px; }
} }
.left + .search .fa { header {
left: 10px; .search .fa {
} left: 20px;
} }
app-root { .left + .search .fa {
border-width: 1px; left: 10px;
border-style: solid;
border-color: #000000;
} }
}
&.theme_light app-root { app-root {
border-color: #777777; border-width: 1px;
} border-style: solid;
border-color: #000000;
}
&.theme_nord app-root { &.theme_light app-root {
border-color: #2e3440; border-color: #777777;
} }
&.theme_nord app-root {
border-color: #2e3440;
}
} }

View File

@ -1,11 +1,11 @@
.row { .row {
display: flex; display: flex;
margin: 0 -15px; margin: 0 -15px;
width: 100%; width: 100%;
} }
.col { .col {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
padding: 0 15px; padding: 0 15px;
} }

View File

@ -1,375 +1,378 @@
@import "variables.scss"; @import "variables.scss";
small, .small { small,
font-size: $font-size-small; .small {
font-size: $font-size-small;
} }
.bg-primary { .bg-primary {
@include themify($themes) { @include themify($themes) {
background-color: themed('primaryColor') !important; background-color: themed("primaryColor") !important;
} }
} }
.bg-success { .bg-success {
@include themify($themes) { @include themify($themes) {
background-color: themed('successColor') !important; background-color: themed("successColor") !important;
} }
} }
.bg-danger { .bg-danger {
@include themify($themes) { @include themify($themes) {
background-color: themed('dangerColor') !important; background-color: themed("dangerColor") !important;
} }
} }
.bg-info { .bg-info {
@include themify($themes) { @include themify($themes) {
background-color: themed('infoColor') !important; background-color: themed("infoColor") !important;
} }
} }
.bg-warning { .bg-warning {
@include themify($themes) { @include themify($themes) {
background-color: themed('warningColor') !important; background-color: themed("warningColor") !important;
} }
} }
.text-primary { .text-primary {
@include themify($themes) { @include themify($themes) {
color: themed('primaryColor') !important; color: themed("primaryColor") !important;
} }
} }
.text-success { .text-success {
@include themify($themes) { @include themify($themes) {
color: themed('successColor') !important; color: themed("successColor") !important;
} }
} }
.text-muted { .text-muted {
@include themify($themes) { @include themify($themes) {
color: themed('mutedColor') !important; color: themed("mutedColor") !important;
} }
} }
.text-default { .text-default {
@include themify($themes) { @include themify($themes) {
color: themed('textColor') !important; color: themed("textColor") !important;
} }
} }
.text-danger { .text-danger {
@include themify($themes) { @include themify($themes) {
color: themed('dangerColor') !important; color: themed("dangerColor") !important;
} }
} }
.text-info { .text-info {
@include themify($themes) { @include themify($themes) {
color: themed('infoColor') !important; color: themed("infoColor") !important;
} }
} }
.text-warning { .text-warning {
@include themify($themes) { @include themify($themes) {
color: themed('warningColor') !important; color: themed("warningColor") !important;
} }
} }
.text-center { .text-center {
text-align: center; text-align: center;
} }
.font-weight-semibold { .font-weight-semibold {
font-weight: 600; font-weight: 600;
} }
p.lead { p.lead {
font-size: $font-size-large; font-size: $font-size-large;
margin-bottom: 20px; margin-bottom: 20px;
font-weight: normal; font-weight: normal;
} }
.flex-right { .flex-right {
margin-left: auto; margin-left: auto;
} }
.flex-bottom { .flex-bottom {
margin-top: auto; margin-top: auto;
} }
.no-margin { .no-margin {
margin: 0 !important; margin: 0 !important;
} }
.no-vmargin { .no-vmargin {
margin-top: 0 !important; margin-top: 0 !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.no-vpad { .no-vpad {
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
.display-block { .display-block {
display: block !important; display: block !important;
} }
.monospaced { .monospaced {
font-family: $font-family-monospace; font-family: $font-family-monospace;
} }
.show-whitespace { .show-whitespace {
white-space: pre-wrap; white-space: pre-wrap;
} }
.img-responsive { .img-responsive {
display: block; display: block;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
.img-rounded { .img-rounded {
border-radius: 6px; border-radius: 6px;
} }
.sr-only { .sr-only {
position: absolute !important; position: absolute !important;
width: 1px !important; width: 1px !important;
height: 1px !important; height: 1px !important;
padding: 0 !important; padding: 0 !important;
margin: -1px !important; margin: -1px !important;
overflow: hidden !important; overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important; clip: rect(0, 0, 0, 0) !important;
border: 0 !important; border: 0 !important;
} }
.password-wrapper { .password-wrapper {
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
min-width: 0; min-width: 0;
} }
.password-number { .password-number {
@include themify($themes) { @include themify($themes) {
color: themed('passwordNumberColor'); color: themed("passwordNumberColor");
} }
} }
.password-special { .password-special {
@include themify($themes) { @include themify($themes) {
color: themed('passwordSpecialColor'); color: themed("passwordSpecialColor");
} }
} }
#duo-frame { #duo-frame {
background: url('../images/loading.svg') 0 0 no-repeat; background: url("../images/loading.svg") 0 0 no-repeat;
width: 100%; width: 100%;
height: 470px; height: 470px;
margin-bottom: -10px; margin-bottom: -10px;
iframe { iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
} }
#web-authn-frame { #web-authn-frame {
background: url('../images/loading.svg') 0 0 no-repeat; background: url("../images/loading.svg") 0 0 no-repeat;
width: 100%; width: 100%;
height: 310px; height: 310px;
margin-bottom: -10px; margin-bottom: -10px;
iframe { iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
} }
#hcaptcha_iframe { #hcaptcha_iframe {
width: 100%; width: 100%;
border: none; border: none;
transition: height 0.25s linear; transition: height 0.25s linear;
} }
body.linux-webauthn { body.linux-webauthn {
width: 485px !important; width: 485px !important;
#web-authn-frame { #web-authn-frame {
iframe { iframe {
width: 375px; width: 375px;
margin: 0 55px; margin: 0 55px;
}
} }
}
} }
app-root > #loading { app-root > #loading {
display: flex; display: flex;
text-align: center; text-align: center;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
width: 100%; width: 100%;
color: $text-muted; color: $text-muted;
@include themify($themes) { @include themify($themes) {
color: themed('mutedColor'); color: themed("mutedColor");
} }
} }
app-vault-icon { app-vault-icon {
display: flex; display: flex;
} }
.logo-image { .logo-image {
margin: 0 auto; margin: 0 auto;
width: 142px; width: 142px;
height: 21px; height: 21px;
background-size: 142px 21px; background-size: 142px 21px;
background-repeat: no-repeat; background-repeat: no-repeat;
@include themify($themes) { @include themify($themes) {
background-image: url('../images/logo-' + themed('logoSuffix') + '@2x.png'); background-image: url("../images/logo-" + themed("logoSuffix") + "@2x.png");
} }
@media (min-width: 219px) { @media (min-width: 219px) {
width: 189px; width: 189px;
height: 28px; height: 28px;
background-size: 189px 28px; background-size: 189px 28px;
} }
@media (min-width: 314px) { @media (min-width: 314px) {
width: 284px; width: 284px;
height: 43px; height: 43px;
background-size: 284px 43px; background-size: 284px 43px;
} }
} }
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
.draggable { .draggable {
cursor: move; cursor: move;
} }
.callout { .callout {
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
border: 1px solid #000000; border: 1px solid #000000;
border-left-width: 5px; border-left-width: 5px;
border-radius: 3px; border-radius: 3px;
@include themify($themes) {
border-color: themed("calloutBorderColor");
background-color: themed("calloutBackgroundColor");
}
.callout-heading {
margin-top: 0;
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
}
&.callout-primary {
@include themify($themes) { @include themify($themes) {
border-color: themed('calloutBorderColor'); border-left-color: themed("primaryColor");
background-color: themed('calloutBackgroundColor');
} }
.callout-heading { .callout-heading {
margin-top: 0; @include themify($themes) {
color: themed("primaryColor");
}
}
}
&.callout-info {
@include themify($themes) {
border-left-color: themed("infoColor");
} }
h3.callout-heading { .callout-heading {
font-weight: bold; @include themify($themes) {
text-transform: uppercase; color: themed("infoColor");
}
}
}
&.callout-danger {
@include themify($themes) {
border-left-color: themed("dangerColor");
} }
&.callout-primary { .callout-heading {
@include themify($themes) { @include themify($themes) {
border-left-color: themed('primaryColor'); color: themed("dangerColor");
} }
}
}
.callout-heading { &.callout-success {
@include themify($themes) { @include themify($themes) {
color: themed('primaryColor'); border-left-color: themed("successColor");
}
}
} }
&.callout-info { .callout-heading {
@include themify($themes) { @include themify($themes) {
border-left-color: themed('infoColor'); color: themed("successColor");
} }
}
}
.callout-heading { &.callout-warning {
@include themify($themes) { @include themify($themes) {
color: themed('infoColor'); border-left-color: themed("warningColor");
}
}
} }
&.callout-danger { .callout-heading {
@include themify($themes) { @include themify($themes) {
border-left-color: themed('dangerColor'); color: themed("warningColor");
} }
.callout-heading {
@include themify($themes) {
color: themed('dangerColor');
}
}
} }
}
&.callout-success { &.clickable {
@include themify($themes) { &:hover,
border-left-color: themed('successColor'); &:focus,
} &.active {
@include themify($themes) {
.callout-heading { background-color: themed("boxBackgroundHoverColor");
@include themify($themes) { }
color: themed('successColor');
}
}
} }
}
&.callout-warning { .enforced-policy-options ul {
@include themify($themes) { padding-left: 30px;
border-left-color: themed('warningColor'); margin: 0;
} }
.callout-heading {
@include themify($themes) {
color: themed('warningColor');
}
}
}
&.clickable {
&:hover, &:focus, &.active {
@include themify($themes) {
background-color: themed('boxBackgroundHoverColor');
}
}
}
.enforced-policy-options ul {
padding-left: 30px;
margin: 0;
}
} }
input[type="password"]::-ms-reveal { input[type="password"]::-ms-reveal {
display: none; display: none;
} }
.flex { .flex {
display: flex; display: flex;
&.flex-grow { &.flex-grow {
> * { > * {
flex: 1; flex: 1;
}
} }
}
} }
// Workaround for slow performance on external monitors on Chrome + MacOS // Workaround for slow performance on external monitors on Chrome + MacOS
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64 // See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
@keyframes redraw { @keyframes redraw {
0% { 0% {
opacity: 0.99; opacity: 0.99;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
html.force_redraw { html.force_redraw {
animation: redraw 1s linear infinite; animation: redraw 1s linear infinite;
} }

Some files were not shown because too many files have changed in this diff Show More