mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
Merge pull request #2824 from bitwarden/jslib
This commit is contained in:
@@ -7,6 +7,7 @@ root = true
|
|||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
# Set default charset
|
# Set default charset
|
||||||
[*.{js,ts,scss,html}]
|
[*.{js,ts,scss,html}]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
**/build
|
**/build
|
||||||
**/dist
|
**/dist
|
||||||
**/jslib
|
|
||||||
|
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,8 @@
|
|||||||
56477eb39cfd8a73c9920577d24d75fed36e2cf5
|
56477eb39cfd8a73c9920577d24d75fed36e2cf5
|
||||||
# Web: Monorepository https://github.com/bitwarden/clients/commit/02fe7159034b04d763a61fcf0200869e3209fa33
|
# Web: Monorepository https://github.com/bitwarden/clients/commit/02fe7159034b04d763a61fcf0200869e3209fa33
|
||||||
02fe7159034b04d763a61fcf0200869e3209fa33
|
02fe7159034b04d763a61fcf0200869e3209fa33
|
||||||
|
|
||||||
|
# Jslib: Apply Prettier https://github.com/bitwarden/jslib/pull/581
|
||||||
|
193434461dbd9c48fe5dcbad95693470aec422ac
|
||||||
|
# Jslib: Monorepository https://github.com/bitwarden/clients/pull/2824/commits/d7492e3cf320410e74ebd0e0675ab994e64bd01a
|
||||||
|
d7492e3cf320410e74ebd0e0675ab994e64bd01a
|
||||||
|
|||||||
18
.gitmodules
vendored
18
.gitmodules
vendored
@@ -1,18 +0,0 @@
|
|||||||
[submodule "apps/browser/jslib"]
|
|
||||||
path = apps/browser/jslib
|
|
||||||
url = https://github.com/bitwarden/jslib.git
|
|
||||||
branch = master
|
|
||||||
|
|
||||||
[submodule "apps/desktop/jslib"]
|
|
||||||
path = apps/desktop/jslib
|
|
||||||
url = https://github.com/bitwarden/jslib.git
|
|
||||||
branch = master
|
|
||||||
|
|
||||||
[submodule "apps/cli/jslib"]
|
|
||||||
path = apps/cli/jslib
|
|
||||||
url = https://github.com/bitwarden/jslib.git
|
|
||||||
branch = master
|
|
||||||
[submodule "apps/web/jslib"]
|
|
||||||
path = apps/web/jslib
|
|
||||||
url = https://github.com/bitwarden/jslib.git
|
|
||||||
branch = master
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
**/dist
|
**/dist
|
||||||
**/coverage
|
**/coverage
|
||||||
|
|
||||||
**/jslib
|
|
||||||
|
|
||||||
# External libraries / auto synced locales
|
# External libraries / auto synced locales
|
||||||
apps/browser/src/_locales
|
apps/browser/src/_locales
|
||||||
apps/browser/src/scripts/duo.js
|
apps/browser/src/scripts/duo.js
|
||||||
@@ -23,5 +21,7 @@ apps/web/.github
|
|||||||
apps/web/src/404/bootstrap.min.css
|
apps/web/src/404/bootstrap.min.css
|
||||||
apps/web/src/locales
|
apps/web/src/locales
|
||||||
|
|
||||||
|
libs/.github
|
||||||
|
|
||||||
# Github Workflows
|
# Github Workflows
|
||||||
.github/workflows
|
.github/workflows
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -111,3 +111,24 @@ git merge clients/master
|
|||||||
|
|
||||||
# Push to clients or your own fork
|
# Push to clients or your own fork
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Jslib
|
||||||
|
|
||||||
|
```
|
||||||
|
# Merge master
|
||||||
|
git merge master
|
||||||
|
|
||||||
|
# Merge branch mono-repo
|
||||||
|
git merge d7492e3cf320410e74ebd0e0675ab994e64bd01a
|
||||||
|
|
||||||
|
# Verify files are placed in libs
|
||||||
|
|
||||||
|
# Add remote
|
||||||
|
git remote add clients git@github.com:bitwarden/clients.git
|
||||||
|
|
||||||
|
# Merge against clients master
|
||||||
|
git fetch clients
|
||||||
|
git merge clients/master
|
||||||
|
|
||||||
|
# Push to clients or your own fork
|
||||||
|
```
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ module.exports = {
|
|||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
prefix: "<rootDir>/",
|
prefix: "<rootDir>/",
|
||||||
}),
|
}),
|
||||||
modulePathIgnorePatterns: ["jslib"],
|
|
||||||
};
|
};
|
||||||
|
|||||||
Submodule apps/browser/jslib deleted from 1ea2824c24
@@ -1,10 +1,10 @@
|
|||||||
$icomoon-font-path: "../../../jslib/angular/src/scss/bwicons/fonts/";
|
$icomoon-font-path: "../../../../../libs/angular/src/scss/bwicons/fonts/";
|
||||||
$card-icons-base: "../../../jslib/angular/src/images/cards/";
|
$card-icons-base: "../../../../../libs/angular/src/images/cards/";
|
||||||
|
|
||||||
@import "../../../jslib/angular/src/scss/webfonts.css";
|
@import "../../../../../libs/angular/src/scss/webfonts.css";
|
||||||
@import "../../../jslib/angular/src/scss/bwicons/styles/style.scss";
|
@import "../../../../../libs/angular/src/scss/bwicons/styles/style.scss";
|
||||||
@import "variables.scss";
|
@import "variables.scss";
|
||||||
@import "../../../jslib/angular/src/scss/icons.scss";
|
@import "../../../../../libs/angular/src/scss/icons.scss";
|
||||||
@import "base.scss";
|
@import "base.scss";
|
||||||
@import "grid.scss";
|
@import "grid.scss";
|
||||||
@import "box.scss";
|
@import "box.scss";
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"jslib-common/*": ["jslib/common/src/*"],
|
"jslib-common/*": ["../../libs/common/src/*"],
|
||||||
"jslib-angular/*": ["jslib/angular/src/*"]
|
"jslib-angular/*": ["../../libs/angular/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
|
|||||||
@@ -12,5 +12,4 @@ module.exports = {
|
|||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
prefix: "<rootDir>/",
|
prefix: "<rootDir>/",
|
||||||
}),
|
}),
|
||||||
modulePathIgnorePatterns: ["jslib"],
|
|
||||||
};
|
};
|
||||||
|
|||||||
Submodule apps/cli/jslib deleted from 77ca5762e1
@@ -12,8 +12,8 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"jslib-common/*": ["jslib/common/src/*"],
|
"jslib-common/*": ["../../libs/common/src/*"],
|
||||||
"jslib-node/*": ["jslib/node/src/*"]
|
"jslib-node/*": ["../../libs/node/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
Submodule apps/desktop/jslib deleted from 1ea2824c24
@@ -1,8 +1,8 @@
|
|||||||
$icomoon-font-path: "../../jslib/angular/src/scss/bwicons/fonts/";
|
$icomoon-font-path: "../../../../libs/angular/src/scss/bwicons/fonts/";
|
||||||
$card-icons-base: "../../jslib/angular/src/images/cards/";
|
$card-icons-base: "../../../../libs/angular/src/images/cards/";
|
||||||
|
|
||||||
@import "../../jslib/angular/src/scss/webfonts.css";
|
@import "../../../../libs/angular/src/scss/webfonts.css";
|
||||||
@import "../../jslib/angular/src/scss/bwicons/styles/style.scss";
|
@import "../../../../libs/angular/src/scss/bwicons/styles/style.scss";
|
||||||
@import "~@angular/cdk/overlay-prebuilt.css";
|
@import "~@angular/cdk/overlay-prebuilt.css";
|
||||||
@import "variables.scss";
|
@import "variables.scss";
|
||||||
@import "base.scss";
|
@import "base.scss";
|
||||||
@@ -19,4 +19,4 @@ $card-icons-base: "../../jslib/angular/src/images/cards/";
|
|||||||
@import "header.scss";
|
@import "header.scss";
|
||||||
@import "left-nav.scss";
|
@import "left-nav.scss";
|
||||||
@import "loading.scss";
|
@import "loading.scss";
|
||||||
@import "../../jslib/angular/src/scss/icons.scss";
|
@import "../../../../libs/angular/src/scss/icons.scss";
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
"types": [],
|
"types": [],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"tldjs": ["jslib/common/src/misc/tldjs.noop"],
|
"tldjs": ["../../libs/common/src/misc/tldjs.noop"],
|
||||||
"jslib-common/*": ["jslib/common/src/*"],
|
"jslib-common/*": ["../../libs/common/src/*"],
|
||||||
"jslib-angular/*": ["jslib/angular/src/*"],
|
"jslib-angular/*": ["../../libs/angular/src/*"],
|
||||||
"jslib-electron/*": ["jslib/electron/src/*"]
|
"jslib-electron/*": ["../../libs/electron/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["src/entry.ts", "src/main.ts", "src/main", "src/proxy", "jslib/**/*.main.ts"]
|
"exclude": ["src/entry.ts", "src/main.ts", "src/main", "src/proxy"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ const common = {
|
|||||||
plugins: [],
|
plugins: [],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".tsx", ".ts", ".js"],
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
alias: {
|
|
||||||
jslib: path.join(__dirname, "jslib/src"),
|
|
||||||
},
|
|
||||||
symlinks: false,
|
symlinks: false,
|
||||||
modules: [path.resolve("../../node_modules")],
|
modules: [path.resolve("../../node_modules")],
|
||||||
},
|
},
|
||||||
|
|||||||
Submodule apps/web/jslib deleted from f4066b4f58
@@ -1,10 +1,10 @@
|
|||||||
$icomoon-font-path: "../../jslib/angular/src/scss/bwicons/fonts/";
|
$icomoon-font-path: "../../../../libs/angular/src/scss/bwicons/fonts/";
|
||||||
$card-icons-base: "../../jslib/angular/src/images/cards/";
|
$card-icons-base: "../../../../libs/angular/src/images/cards/";
|
||||||
|
|
||||||
@import "../../jslib/angular/src/scss/webfonts.css";
|
@import "../../../../libs/angular/src/scss/webfonts.css";
|
||||||
@import "./variables";
|
@import "./variables";
|
||||||
@import "../../jslib/angular/src/scss/bwicons/styles/style.scss";
|
@import "../../../../libs/angular/src/scss/bwicons/styles/style.scss";
|
||||||
@import "../../jslib/angular/src/scss/icons.scss";
|
@import "../../../../libs/angular/src/scss/icons.scss";
|
||||||
@import "@angular/cdk/overlay-prebuilt.css";
|
@import "@angular/cdk/overlay-prebuilt.css";
|
||||||
|
|
||||||
//@import "~bootstrap/scss/bootstrap";
|
//@import "~bootstrap/scss/bootstrap";
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import "../../jslib/components/src/tw-theme.css";
|
@import "../../../../libs/components/src/tw-theme.css";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
|
||||||
const config = require("./jslib/components/tailwind.config.base");
|
const config = require("../../libs/components/tailwind.config.base");
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "./jslib/shared/tsconfig",
|
"extends": "../../libs/shared/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"tldjs": ["jslib/common/src/misc/tldjs.noop"],
|
"tldjs": ["../../libs/common/src/misc/tldjs.noop"],
|
||||||
"jslib-common/*": ["jslib/common/src/*"],
|
"jslib-common/*": ["../../libs/common/src/*"],
|
||||||
"jslib-angular/*": ["jslib/angular/src/*"],
|
"jslib-angular/*": ["../../libs/angular/src/*"],
|
||||||
"@bitwarden/components": ["jslib/components/src"],
|
"@bitwarden/components": ["../../libs/components/src"],
|
||||||
"src/*": ["src/*"]
|
"src/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
107
libs/.github/workflows/build.yml
vendored
Normal file
107
libs/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cloc:
|
||||||
|
name: CLOC
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
|
- name: Set up cloc
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install cloc
|
||||||
|
|
||||||
|
- name: Print lines of code
|
||||||
|
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build jslib
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-2019, macos-10.15, ubuntu-20.04]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
|
- name: Install node-gyp
|
||||||
|
run: |
|
||||||
|
npm install -g node-gyp
|
||||||
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
if: runner.os != 'Linux'
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
|
- name: Upload test coverage artifact
|
||||||
|
if: runner.os != 'Linux'
|
||||||
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
||||||
|
with:
|
||||||
|
name: test-coverage
|
||||||
|
path: coverage/
|
||||||
|
|
||||||
|
check-failures:
|
||||||
|
name: Check for failures
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs:
|
||||||
|
- cloc
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Check if any job failed
|
||||||
|
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||||
|
env:
|
||||||
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
|
BUILD_STATUS: ${{ needs.build.result }}
|
||||||
|
run: |
|
||||||
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$BUILD_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Notify Slack on failure
|
||||||
|
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
41
libs/.github/workflows/chromatic.yml
vendored
Normal file
41
libs/.github/workflows/chromatic.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Chromatic
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
chromatic:
|
||||||
|
name: Chromatic
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea # v2.1.5
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: "~/.npm"
|
||||||
|
key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: ./components
|
||||||
|
|
||||||
|
- name: Publish to Chromatic
|
||||||
|
uses: chromaui/action@c72f0b48c8887c0ef0abe18ad865a6c1e01e73c6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
workingDir: ./components
|
||||||
|
exitOnceUploaded: true
|
||||||
|
onlyChanged: true
|
||||||
|
externals: "[\"components/**/*.scss\", \"components/tailwind.config*.js\"]"
|
||||||
9
libs/.gitignore
vendored
Normal file
9
libs/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.vs
|
||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
vwd.webinfo
|
||||||
|
*.crx
|
||||||
|
*.pem
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
42
libs/README.md
Normal file
42
libs/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[](https://github.com/bitwarden/jslib/actions/workflows/build.yml?query=branch:master)
|
||||||
|
|
||||||
|
# Bitwarden JavaScript Library
|
||||||
|
|
||||||
|
Common code referenced across Bitwarden JavaScript projects.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org) v16.13.1 or greater
|
||||||
|
- NPM v8
|
||||||
|
- Git
|
||||||
|
- node-gyp
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- _Microsoft Build Tools 2015_ in Visual Studio Installer
|
||||||
|
- [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/)
|
||||||
|
either by downloading it seperately or through the Visual Studio Installer.
|
||||||
|
|
||||||
|
## We're Hiring!
|
||||||
|
|
||||||
|
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
|
||||||
|
|
||||||
|
## Prettier
|
||||||
|
|
||||||
|
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
|
||||||
|
|
||||||
|
1. Check out your local Branch
|
||||||
|
2. Run `git merge 8b2dfc6cdcb8ff5b604364c2ea6d343473aee7cd`
|
||||||
|
3. Resolve any merge conflicts, commit.
|
||||||
|
4. Run `npm run prettier`
|
||||||
|
5. Commit
|
||||||
|
6. Run `git merge -Xours 193434461dbd9c48fe5dcbad95693470aec422ac`
|
||||||
|
7. Push
|
||||||
|
|
||||||
|
### Git blame
|
||||||
|
|
||||||
|
We also recommend that you configure git to ignore the prettier revision using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
```
|
||||||
17
libs/angular/jest.config.js
Normal file
17
libs/angular/jest.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||||
|
|
||||||
|
const { compilerOptions } = require("./tsconfig");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "angular",
|
||||||
|
displayName: "libs/angular tests",
|
||||||
|
preset: "jest-preset-angular",
|
||||||
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
||||||
|
collectCoverage: true,
|
||||||
|
coverageReporters: ["html", "lcov"],
|
||||||
|
coverageDirectory: "coverage",
|
||||||
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
|
prefix: "<rootDir>/",
|
||||||
|
}),
|
||||||
|
};
|
||||||
23
libs/angular/package.json
Normal file
23
libs/angular/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@bitwarden/jslib-angular",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Common code used across Bitwarden JavaScript projects.",
|
||||||
|
"keywords": [
|
||||||
|
"bitwarden"
|
||||||
|
],
|
||||||
|
"author": "Bitwarden Inc.",
|
||||||
|
"homepage": "https://bitwarden.com",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/bitwarden/jslib"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist/**/*",
|
||||||
|
"build": "npm run clean && tsc",
|
||||||
|
"build:watch": "npm run clean && tsc -watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bitwarden/jslib-common": "file:../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
libs/angular/spec/test.ts
Normal file
28
libs/angular/spec/test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { webcrypto } from "crypto";
|
||||||
|
import "jest-preset-angular/setup-jest";
|
||||||
|
|
||||||
|
Object.defineProperty(window, "CSS", { value: null });
|
||||||
|
Object.defineProperty(window, "getComputedStyle", {
|
||||||
|
value: () => {
|
||||||
|
return {
|
||||||
|
display: "none",
|
||||||
|
appearance: ["-webkit-appearance"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(document, "doctype", {
|
||||||
|
value: "<!DOCTYPE html>",
|
||||||
|
});
|
||||||
|
Object.defineProperty(document.body.style, "transform", {
|
||||||
|
value: () => {
|
||||||
|
return {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, "crypto", {
|
||||||
|
value: webcrypto,
|
||||||
|
});
|
||||||
116
libs/angular/src/components/add-edit-custom-fields.component.ts
Normal file
116
libs/angular/src/components/add-edit-custom-fields.component.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
|
||||||
|
import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
|
||||||
|
|
||||||
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { FieldType } from "jslib-common/enums/fieldType";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { FieldView } from "jslib-common/models/view/fieldView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class AddEditCustomFieldsComponent implements OnChanges {
|
||||||
|
@Input() cipher: CipherView;
|
||||||
|
@Input() thisCipherType: CipherType;
|
||||||
|
@Input() editMode: boolean;
|
||||||
|
|
||||||
|
addFieldType: FieldType = FieldType.Text;
|
||||||
|
addFieldTypeOptions: any[];
|
||||||
|
addFieldLinkedTypeOption: any;
|
||||||
|
linkedFieldOptions: any[] = [];
|
||||||
|
|
||||||
|
cipherType = CipherType;
|
||||||
|
fieldType = FieldType;
|
||||||
|
eventType = EventType;
|
||||||
|
|
||||||
|
constructor(private i18nService: I18nService, private eventService: EventService) {
|
||||||
|
this.addFieldTypeOptions = [
|
||||||
|
{ name: i18nService.t("cfTypeText"), value: FieldType.Text },
|
||||||
|
{ name: i18nService.t("cfTypeHidden"), value: FieldType.Hidden },
|
||||||
|
{ name: i18nService.t("cfTypeBoolean"), value: FieldType.Boolean },
|
||||||
|
];
|
||||||
|
this.addFieldLinkedTypeOption = {
|
||||||
|
name: this.i18nService.t("cfTypeLinked"),
|
||||||
|
value: FieldType.Linked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
if (changes.thisCipherType != null) {
|
||||||
|
this.setLinkedFieldOptions();
|
||||||
|
|
||||||
|
if (!changes.thisCipherType.firstChange) {
|
||||||
|
this.resetCipherLinkedFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addField() {
|
||||||
|
if (this.cipher.fields == null) {
|
||||||
|
this.cipher.fields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const f = new FieldView();
|
||||||
|
f.type = this.addFieldType;
|
||||||
|
f.newField = true;
|
||||||
|
|
||||||
|
if (f.type === FieldType.Linked) {
|
||||||
|
f.linkedId = this.linkedFieldOptions[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cipher.fields.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeField(field: FieldView) {
|
||||||
|
const i = this.cipher.fields.indexOf(field);
|
||||||
|
if (i > -1) {
|
||||||
|
this.cipher.fields.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFieldValue(field: FieldView) {
|
||||||
|
const f = field as any;
|
||||||
|
f.showValue = !f.showValue;
|
||||||
|
if (this.editMode && f.showValue) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipher.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByFunction(index: number, item: any) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(event: CdkDragDrop<string[]>) {
|
||||||
|
moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setLinkedFieldOptions() {
|
||||||
|
if (this.cipher.linkedFieldOptions == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: any = [];
|
||||||
|
this.cipher.linkedFieldOptions.forEach((linkedFieldOption, id) =>
|
||||||
|
options.push({ name: this.i18nService.t(linkedFieldOption.i18nKey), value: id })
|
||||||
|
);
|
||||||
|
this.linkedFieldOptions = options.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetCipherLinkedFields() {
|
||||||
|
if (this.cipher.fields == null || this.cipher.fields.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any Linked custom fields if the item type does not support them
|
||||||
|
if (this.cipher.linkedFieldOptions == null) {
|
||||||
|
this.cipher.fields = this.cipher.fields.filter((f) => f.type !== FieldType.Linked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cipher.fields
|
||||||
|
.filter((f) => f.type === FieldType.Linked)
|
||||||
|
.forEach((f) => (f.linkedId = this.linkedFieldOptions[0].value));
|
||||||
|
}
|
||||||
|
}
|
||||||
577
libs/angular/src/components/add-edit.component.ts
Normal file
577
libs/angular/src/components/add-edit.component.ts
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { AuditService } from "jslib-common/abstractions/audit.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||||
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
|
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
|
||||||
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
|
||||||
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
import { SecureNoteType } from "jslib-common/enums/secureNoteType";
|
||||||
|
import { UriMatchType } from "jslib-common/enums/uriMatchType";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { Cipher } from "jslib-common/models/domain/cipher";
|
||||||
|
import { CardView } from "jslib-common/models/view/cardView";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||||
|
import { FolderView } from "jslib-common/models/view/folderView";
|
||||||
|
import { IdentityView } from "jslib-common/models/view/identityView";
|
||||||
|
import { LoginUriView } from "jslib-common/models/view/loginUriView";
|
||||||
|
import { LoginView } from "jslib-common/models/view/loginView";
|
||||||
|
import { SecureNoteView } from "jslib-common/models/view/secureNoteView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class AddEditComponent implements OnInit {
|
||||||
|
@Input() cloneMode = false;
|
||||||
|
@Input() folderId: string = null;
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Input() type: CipherType;
|
||||||
|
@Input() collectionIds: string[];
|
||||||
|
@Input() organizationId: string = null;
|
||||||
|
@Output() onSavedCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onCancelled = new EventEmitter<CipherView>();
|
||||||
|
@Output() onEditAttachments = new EventEmitter<CipherView>();
|
||||||
|
@Output() onShareCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onEditCollections = new EventEmitter<CipherView>();
|
||||||
|
@Output() onGeneratePassword = new EventEmitter();
|
||||||
|
@Output() onGenerateUsername = new EventEmitter();
|
||||||
|
|
||||||
|
editMode = false;
|
||||||
|
cipher: CipherView;
|
||||||
|
folders: FolderView[];
|
||||||
|
collections: CollectionView[] = [];
|
||||||
|
title: string;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
deletePromise: Promise<any>;
|
||||||
|
restorePromise: Promise<any>;
|
||||||
|
checkPasswordPromise: Promise<number>;
|
||||||
|
showPassword = false;
|
||||||
|
showCardNumber = false;
|
||||||
|
showCardCode = false;
|
||||||
|
cipherType = CipherType;
|
||||||
|
typeOptions: any[];
|
||||||
|
cardBrandOptions: any[];
|
||||||
|
cardExpMonthOptions: any[];
|
||||||
|
identityTitleOptions: any[];
|
||||||
|
uriMatchOptions: any[];
|
||||||
|
ownershipOptions: any[] = [];
|
||||||
|
autofillOnPageLoadOptions: any[];
|
||||||
|
currentDate = new Date();
|
||||||
|
allowPersonal = true;
|
||||||
|
reprompt = false;
|
||||||
|
canUseReprompt = true;
|
||||||
|
|
||||||
|
protected writeableCollections: CollectionView[];
|
||||||
|
private previousCipherId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected folderService: FolderService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected auditService: AuditService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected collectionService: CollectionService,
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
protected eventService: EventService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
|
) {
|
||||||
|
this.typeOptions = [
|
||||||
|
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||||
|
{ name: i18nService.t("typeCard"), value: CipherType.Card },
|
||||||
|
{ name: i18nService.t("typeIdentity"), value: CipherType.Identity },
|
||||||
|
{ name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote },
|
||||||
|
];
|
||||||
|
this.cardBrandOptions = [
|
||||||
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||||
|
{ name: "Visa", value: "Visa" },
|
||||||
|
{ name: "Mastercard", value: "Mastercard" },
|
||||||
|
{ name: "American Express", value: "Amex" },
|
||||||
|
{ name: "Discover", value: "Discover" },
|
||||||
|
{ name: "Diners Club", value: "Diners Club" },
|
||||||
|
{ name: "JCB", value: "JCB" },
|
||||||
|
{ name: "Maestro", value: "Maestro" },
|
||||||
|
{ name: "UnionPay", value: "UnionPay" },
|
||||||
|
{ name: "RuPay", value: "RuPay" },
|
||||||
|
{ name: i18nService.t("other"), value: "Other" },
|
||||||
|
];
|
||||||
|
this.cardExpMonthOptions = [
|
||||||
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||||
|
{ name: "01 - " + i18nService.t("january"), value: "1" },
|
||||||
|
{ name: "02 - " + i18nService.t("february"), value: "2" },
|
||||||
|
{ name: "03 - " + i18nService.t("march"), value: "3" },
|
||||||
|
{ name: "04 - " + i18nService.t("april"), value: "4" },
|
||||||
|
{ name: "05 - " + i18nService.t("may"), value: "5" },
|
||||||
|
{ name: "06 - " + i18nService.t("june"), value: "6" },
|
||||||
|
{ name: "07 - " + i18nService.t("july"), value: "7" },
|
||||||
|
{ name: "08 - " + i18nService.t("august"), value: "8" },
|
||||||
|
{ name: "09 - " + i18nService.t("september"), value: "9" },
|
||||||
|
{ name: "10 - " + i18nService.t("october"), value: "10" },
|
||||||
|
{ name: "11 - " + i18nService.t("november"), value: "11" },
|
||||||
|
{ name: "12 - " + i18nService.t("december"), value: "12" },
|
||||||
|
];
|
||||||
|
this.identityTitleOptions = [
|
||||||
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||||
|
{ name: i18nService.t("mr"), value: i18nService.t("mr") },
|
||||||
|
{ name: i18nService.t("mrs"), value: i18nService.t("mrs") },
|
||||||
|
{ name: i18nService.t("ms"), value: i18nService.t("ms") },
|
||||||
|
{ name: i18nService.t("dr"), value: i18nService.t("dr") },
|
||||||
|
];
|
||||||
|
this.uriMatchOptions = [
|
||||||
|
{ name: i18nService.t("defaultMatchDetection"), value: null },
|
||||||
|
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain },
|
||||||
|
{ name: i18nService.t("host"), value: UriMatchType.Host },
|
||||||
|
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith },
|
||||||
|
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression },
|
||||||
|
{ name: i18nService.t("exact"), value: UriMatchType.Exact },
|
||||||
|
{ name: i18nService.t("never"), value: UriMatchType.Never },
|
||||||
|
];
|
||||||
|
this.autofillOnPageLoadOptions = [
|
||||||
|
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },
|
||||||
|
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
|
||||||
|
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.ownershipOptions.length) {
|
||||||
|
this.ownershipOptions = [];
|
||||||
|
}
|
||||||
|
if (await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)) {
|
||||||
|
this.allowPersonal = false;
|
||||||
|
} else {
|
||||||
|
const myEmail = await this.stateService.getEmail();
|
||||||
|
this.ownershipOptions.push({ name: myEmail, value: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgs = await this.organizationService.getAll();
|
||||||
|
orgs.sort(Utils.getSortFunction(this.i18nService, "name")).forEach((o) => {
|
||||||
|
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
|
||||||
|
this.ownershipOptions.push({ name: o.name, value: o.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!this.allowPersonal) {
|
||||||
|
this.organizationId = this.ownershipOptions[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writeableCollections = await this.loadCollections();
|
||||||
|
|
||||||
|
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.editMode = this.cipherId != null;
|
||||||
|
if (this.editMode) {
|
||||||
|
this.editMode = true;
|
||||||
|
if (this.cloneMode) {
|
||||||
|
this.cloneMode = true;
|
||||||
|
this.title = this.i18nService.t("addItem");
|
||||||
|
} else {
|
||||||
|
this.title = this.i18nService.t("editItem");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.title = this.i18nService.t("addItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
|
||||||
|
if (addEditCipherInfo != null) {
|
||||||
|
this.cipher = addEditCipherInfo.cipher;
|
||||||
|
this.collectionIds = addEditCipherInfo.collectionIds;
|
||||||
|
}
|
||||||
|
await this.stateService.setAddEditCipherInfo(null);
|
||||||
|
|
||||||
|
if (this.cipher == null) {
|
||||||
|
if (this.editMode) {
|
||||||
|
const cipher = await this.loadCipher();
|
||||||
|
this.cipher = await cipher.decrypt();
|
||||||
|
|
||||||
|
// Adjust Cipher Name if Cloning
|
||||||
|
if (this.cloneMode) {
|
||||||
|
this.cipher.name += " - " + this.i18nService.t("clone");
|
||||||
|
// If not allowing personal ownership, update cipher's org Id to prompt downstream changes
|
||||||
|
if (this.cipher.organizationId == null && !this.allowPersonal) {
|
||||||
|
this.cipher.organizationId = this.organizationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.cipher = new CipherView();
|
||||||
|
this.cipher.organizationId = this.organizationId == null ? null : this.organizationId;
|
||||||
|
this.cipher.folderId = this.folderId;
|
||||||
|
this.cipher.type = this.type == null ? CipherType.Login : this.type;
|
||||||
|
this.cipher.login = new LoginView();
|
||||||
|
this.cipher.login.uris = [new LoginUriView()];
|
||||||
|
this.cipher.card = new CardView();
|
||||||
|
this.cipher.identity = new IdentityView();
|
||||||
|
this.cipher.secureNote = new SecureNoteView();
|
||||||
|
this.cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
this.cipher.reprompt = CipherRepromptType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) {
|
||||||
|
await this.organizationChanged();
|
||||||
|
if (
|
||||||
|
this.collectionIds != null &&
|
||||||
|
this.collectionIds.length > 0 &&
|
||||||
|
this.collections.length > 0
|
||||||
|
) {
|
||||||
|
this.collections.forEach((c) => {
|
||||||
|
if (this.collectionIds.indexOf(c.id) > -1) {
|
||||||
|
(c as any).checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.folders = await this.folderService.getAllDecrypted();
|
||||||
|
|
||||||
|
if (this.editMode && this.previousCipherId !== this.cipherId) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||||
|
}
|
||||||
|
this.previousCipherId = this.cipherId;
|
||||||
|
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(): Promise<boolean> {
|
||||||
|
if (this.cipher.isDeleted) {
|
||||||
|
return this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher.name == null || this.cipher.name === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("nameRequired")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!this.editMode || this.cloneMode) &&
|
||||||
|
!this.allowPersonal &&
|
||||||
|
this.cipher.organizationId == null
|
||||||
|
) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("personalOwnershipSubmitError")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!this.editMode || this.cloneMode) &&
|
||||||
|
this.cipher.type === CipherType.Login &&
|
||||||
|
this.cipher.login.uris != null &&
|
||||||
|
this.cipher.login.uris.length === 1 &&
|
||||||
|
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||||
|
) {
|
||||||
|
this.cipher.login.uris = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows saving of selected collections during "Add" and "Clone" flows
|
||||||
|
if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) {
|
||||||
|
this.cipher.collectionIds =
|
||||||
|
this.collections == null
|
||||||
|
? []
|
||||||
|
: this.collections.filter((c) => (c as any).checked).map((c) => c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear current Cipher Id to trigger "Add" cipher flow
|
||||||
|
if (this.cloneMode) {
|
||||||
|
this.cipher.id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = await this.encryptCipher();
|
||||||
|
try {
|
||||||
|
this.formPromise = this.saveCipher(cipher);
|
||||||
|
await this.formPromise;
|
||||||
|
this.cipher.id = cipher.id;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem")
|
||||||
|
);
|
||||||
|
this.onSavedCipher.emit(this.cipher);
|
||||||
|
this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher");
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addUri() {
|
||||||
|
if (this.cipher.type !== CipherType.Login) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher.login.uris == null) {
|
||||||
|
this.cipher.login.uris = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cipher.login.uris.push(new LoginUriView());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUri(uri: LoginUriView) {
|
||||||
|
if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i = this.cipher.login.uris.indexOf(uri);
|
||||||
|
if (i > -1) {
|
||||||
|
this.cipher.login.uris.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByFunction(index: number, item: any) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.onCancelled.emit(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments() {
|
||||||
|
this.onEditAttachments.emit(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
share() {
|
||||||
|
this.onShareCipher.emit(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
editCollections() {
|
||||||
|
this.onEditCollections.emit(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<boolean> {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t(
|
||||||
|
this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation"
|
||||||
|
),
|
||||||
|
this.i18nService.t("deleteItem"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePromise = this.deleteCipher();
|
||||||
|
await this.deletePromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem")
|
||||||
|
);
|
||||||
|
this.onDeletedCipher.emit(this.cipher);
|
||||||
|
this.messagingService.send(
|
||||||
|
this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(): Promise<boolean> {
|
||||||
|
if (!this.cipher.isDeleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("restoreItemConfirmation"),
|
||||||
|
this.i18nService.t("restoreItem"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.restorePromise = this.restoreCipher();
|
||||||
|
await this.restorePromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
||||||
|
this.onRestoredCipher.emit(this.cipher);
|
||||||
|
this.messagingService.send("restoredCipher");
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateUsername(): Promise<boolean> {
|
||||||
|
if (this.cipher.login?.username?.length) {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("overwriteUsernameConfirmation"),
|
||||||
|
this.i18nService.t("overwriteUsername"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no")
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onGenerateUsername.emit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generatePassword(): Promise<boolean> {
|
||||||
|
if (this.cipher.login?.password?.length) {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("overwritePasswordConfirmation"),
|
||||||
|
this.i18nService.t("overwritePassword"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no")
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onGeneratePassword.emit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById("loginPassword").focus();
|
||||||
|
if (this.editMode && this.showPassword) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleCardNumber() {
|
||||||
|
this.showCardNumber = !this.showCardNumber;
|
||||||
|
if (this.showCardNumber) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCardCode() {
|
||||||
|
this.showCardCode = !this.showCardCode;
|
||||||
|
document.getElementById("cardCode").focus();
|
||||||
|
if (this.editMode && this.showCardCode) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleUriOptions(uri: LoginUriView) {
|
||||||
|
const u = uri as any;
|
||||||
|
u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUriMatchChanged(uri: LoginUriView) {
|
||||||
|
const u = uri as any;
|
||||||
|
u.showOptions = u.showOptions == null ? true : u.showOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async organizationChanged() {
|
||||||
|
if (this.writeableCollections != null) {
|
||||||
|
this.writeableCollections.forEach((c) => ((c as any).checked = false));
|
||||||
|
}
|
||||||
|
if (this.cipher.organizationId != null) {
|
||||||
|
this.collections = this.writeableCollections.filter(
|
||||||
|
(c) => c.organizationId === this.cipher.organizationId
|
||||||
|
);
|
||||||
|
const org = await this.organizationService.get(this.cipher.organizationId);
|
||||||
|
if (org != null) {
|
||||||
|
this.cipher.organizationUseTotp = org.useTotp;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.collections = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPassword() {
|
||||||
|
if (this.checkPasswordPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.cipher.login == null ||
|
||||||
|
this.cipher.login.password == null ||
|
||||||
|
this.cipher.login.password === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
|
||||||
|
const matches = await this.checkPasswordPromise;
|
||||||
|
this.checkPasswordPromise = null;
|
||||||
|
|
||||||
|
if (matches > 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"warning",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("passwordExposed", matches.toString())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repromptChanged() {
|
||||||
|
this.reprompt = !this.reprompt;
|
||||||
|
if (this.reprompt) {
|
||||||
|
this.cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
} else {
|
||||||
|
this.cipher.reprompt = CipherRepromptType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadCollections() {
|
||||||
|
const allCollections = await this.collectionService.getAllDecrypted();
|
||||||
|
return allCollections.filter((c) => !c.readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadCipher() {
|
||||||
|
return this.cipherService.get(this.cipherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected encryptCipher() {
|
||||||
|
return this.cipherService.encrypt(this.cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveCipher(cipher: Cipher) {
|
||||||
|
return this.cipherService.saveWithServer(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteCipher() {
|
||||||
|
return this.cipher.isDeleted
|
||||||
|
? this.cipherService.deleteWithServer(this.cipher.id)
|
||||||
|
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected restoreCipher() {
|
||||||
|
return this.cipherService.restoreWithServer(this.cipher.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
289
libs/angular/src/components/attachments.component.ts
Normal file
289
libs/angular/src/components/attachments.component.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { Cipher } from "jslib-common/models/domain/cipher";
|
||||||
|
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||||
|
import { AttachmentView } from "jslib-common/models/view/attachmentView";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class AttachmentsComponent implements OnInit {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Output() onUploadedAttachment = new EventEmitter();
|
||||||
|
@Output() onDeletedAttachment = new EventEmitter();
|
||||||
|
@Output() onReuploadedAttachment = new EventEmitter();
|
||||||
|
|
||||||
|
cipher: CipherView;
|
||||||
|
cipherDomain: Cipher;
|
||||||
|
hasUpdatedKey: boolean;
|
||||||
|
canAccessAttachments: boolean;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
deletePromises: { [id: string]: Promise<any> } = {};
|
||||||
|
reuploadPromises: { [id: string]: Promise<any> } = {};
|
||||||
|
emergencyAccessId?: string = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected win: Window,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!this.hasUpdatedKey) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("updateKey")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||||
|
const files = fileEl.files;
|
||||||
|
if (files == null || files.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("selectFile")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files[0].size > 524288000) {
|
||||||
|
// 500 MB
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("maxFileSize")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.saveCipherAttachment(files[0]);
|
||||||
|
this.cipherDomain = await this.formPromise;
|
||||||
|
this.cipher = await this.cipherDomain.decrypt();
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved"));
|
||||||
|
this.onUploadedAttachment.emit();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset file input
|
||||||
|
// ref: https://stackoverflow.com/a/20552042
|
||||||
|
fileEl.type = "";
|
||||||
|
fileEl.type = "file";
|
||||||
|
fileEl.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(attachment: AttachmentView) {
|
||||||
|
if (this.deletePromises[attachment.id] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("deleteAttachmentConfirmation"),
|
||||||
|
this.i18nService.t("deleteAttachment"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||||
|
await this.deletePromises[attachment.id];
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedAttachment"));
|
||||||
|
const i = this.cipher.attachments.indexOf(attachment);
|
||||||
|
if (i > -1) {
|
||||||
|
this.cipher.attachments.splice(i, 1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deletePromises[attachment.id] = null;
|
||||||
|
this.onDeletedAttachment.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(attachment: AttachmentView) {
|
||||||
|
const a = attachment as any;
|
||||||
|
if (a.downloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.canAccessAttachments) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("premiumRequired"),
|
||||||
|
this.i18nService.t("premiumRequiredDesc")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
try {
|
||||||
|
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||||
|
this.cipher.id,
|
||||||
|
attachment.id,
|
||||||
|
this.emergencyAccessId
|
||||||
|
);
|
||||||
|
url = attachmentDownloadResponse.url;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||||
|
url = attachment.url;
|
||||||
|
} else if (e instanceof ErrorResponse) {
|
||||||
|
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.downloading = true;
|
||||||
|
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||||
|
if (response.status !== 200) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
a.downloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buf = await response.arrayBuffer();
|
||||||
|
const key =
|
||||||
|
attachment.key != null
|
||||||
|
? attachment.key
|
||||||
|
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||||
|
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||||
|
this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
a.downloading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init() {
|
||||||
|
this.cipherDomain = await this.loadCipher();
|
||||||
|
this.cipher = await this.cipherDomain.decrypt();
|
||||||
|
|
||||||
|
this.hasUpdatedKey = await this.cryptoService.hasEncKey();
|
||||||
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
|
||||||
|
|
||||||
|
if (!this.canAccessAttachments) {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("premiumRequiredDesc"),
|
||||||
|
this.i18nService.t("premiumRequired"),
|
||||||
|
this.i18nService.t("learnMore"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase");
|
||||||
|
}
|
||||||
|
} else if (!this.hasUpdatedKey) {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("updateKey"),
|
||||||
|
this.i18nService.t("featureUnavailable"),
|
||||||
|
this.i18nService.t("learnMore"),
|
||||||
|
this.i18nService.t("cancel"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.platformUtilsService.launchUri(
|
||||||
|
"https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) {
|
||||||
|
const a = attachment as any;
|
||||||
|
if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => {
|
||||||
|
// 1. Download
|
||||||
|
a.downloading = true;
|
||||||
|
const response = await fetch(new Request(attachment.url, { cache: "no-store" }));
|
||||||
|
if (response.status !== 200) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
a.downloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Resave
|
||||||
|
const buf = await response.arrayBuffer();
|
||||||
|
const key =
|
||||||
|
attachment.key != null
|
||||||
|
? attachment.key
|
||||||
|
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||||
|
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||||
|
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||||
|
this.cipherDomain,
|
||||||
|
attachment.fileName,
|
||||||
|
decBuf,
|
||||||
|
admin
|
||||||
|
);
|
||||||
|
this.cipher = await this.cipherDomain.decrypt();
|
||||||
|
|
||||||
|
// 3. Delete old
|
||||||
|
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||||
|
await this.deletePromises[attachment.id];
|
||||||
|
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
|
||||||
|
if (foundAttachment.length > 0) {
|
||||||
|
const i = this.cipher.attachments.indexOf(foundAttachment[0]);
|
||||||
|
if (i > -1) {
|
||||||
|
this.cipher.attachments.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("attachmentSaved")
|
||||||
|
);
|
||||||
|
this.onReuploadedAttachment.emit();
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
a.downloading = false;
|
||||||
|
});
|
||||||
|
await this.reuploadPromises[attachment.id];
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadCipher() {
|
||||||
|
return this.cipherService.get(this.cipherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveCipherAttachment(file: File) {
|
||||||
|
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteCipherAttachment(attachmentId: string) {
|
||||||
|
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
libs/angular/src/components/avatar.component.ts
Normal file
140
libs/angular/src/components/avatar.component.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Component, Input, OnChanges, OnInit } from "@angular/core";
|
||||||
|
import { DomSanitizer } from "@angular/platform-browser";
|
||||||
|
|
||||||
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-avatar",
|
||||||
|
template:
|
||||||
|
'<img *ngIf="src" [src]="sanitizer.bypassSecurityTrustResourceUrl(src)" title="{{data}}" ' +
|
||||||
|
"[ngClass]=\"{'rounded-circle': circle}\">",
|
||||||
|
})
|
||||||
|
export class AvatarComponent implements OnChanges, OnInit {
|
||||||
|
@Input() data: string;
|
||||||
|
@Input() email: string;
|
||||||
|
@Input() size = 45;
|
||||||
|
@Input() charCount = 2;
|
||||||
|
@Input() textColor = "#ffffff";
|
||||||
|
@Input() fontSize = 20;
|
||||||
|
@Input() fontWeight = 300;
|
||||||
|
@Input() dynamic = false;
|
||||||
|
@Input() circle = false;
|
||||||
|
|
||||||
|
src: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public sanitizer: DomSanitizer,
|
||||||
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.dynamic) {
|
||||||
|
this.generate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
if (this.dynamic) {
|
||||||
|
this.generate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generate() {
|
||||||
|
const enableGravatars = await this.stateService.getEnableGravitars();
|
||||||
|
if (enableGravatars && this.email != null) {
|
||||||
|
const hashBytes = await this.cryptoFunctionService.hash(
|
||||||
|
this.email.toLowerCase().trim(),
|
||||||
|
"md5"
|
||||||
|
);
|
||||||
|
const hash = Utils.fromBufferToHex(hashBytes).toLowerCase();
|
||||||
|
this.src = "https://www.gravatar.com/avatar/" + hash + "?s=" + this.size + "&r=pg&d=retro";
|
||||||
|
} else {
|
||||||
|
let chars: string = null;
|
||||||
|
const upperData = this.data.toUpperCase();
|
||||||
|
|
||||||
|
if (this.charCount > 1) {
|
||||||
|
chars = this.getFirstLetters(upperData, this.charCount);
|
||||||
|
}
|
||||||
|
if (chars == null) {
|
||||||
|
chars = this.unicodeSafeSubstring(upperData, this.charCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the chars contain an emoji, only show it.
|
||||||
|
if (chars.match(Utils.regexpEmojiPresentation)) {
|
||||||
|
chars = chars.match(Utils.regexpEmojiPresentation)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const charObj = this.getCharText(chars);
|
||||||
|
const color = this.stringToColor(upperData);
|
||||||
|
const svg = this.getSvg(this.size, color);
|
||||||
|
svg.appendChild(charObj);
|
||||||
|
const html = window.document.createElement("div").appendChild(svg).outerHTML;
|
||||||
|
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
|
||||||
|
this.src = "data:image/svg+xml;base64," + svgHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stringToColor(str: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
let color = "#";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
color += ("00" + value.toString(16)).substr(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFirstLetters(data: string, count: number): string {
|
||||||
|
const parts = data.split(" ");
|
||||||
|
if (parts.length > 1) {
|
||||||
|
let text = "";
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
text += this.unicodeSafeSubstring(parts[i], 1);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSvg(size: number, color: string): HTMLElement {
|
||||||
|
const svgTag = window.document.createElement("svg");
|
||||||
|
svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
svgTag.setAttribute("pointer-events", "none");
|
||||||
|
svgTag.setAttribute("width", size.toString());
|
||||||
|
svgTag.setAttribute("height", size.toString());
|
||||||
|
svgTag.style.backgroundColor = color;
|
||||||
|
svgTag.style.width = size + "px";
|
||||||
|
svgTag.style.height = size + "px";
|
||||||
|
return svgTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCharText(character: string): HTMLElement {
|
||||||
|
const textTag = window.document.createElement("text");
|
||||||
|
textTag.setAttribute("text-anchor", "middle");
|
||||||
|
textTag.setAttribute("y", "50%");
|
||||||
|
textTag.setAttribute("x", "50%");
|
||||||
|
textTag.setAttribute("dy", "0.35em");
|
||||||
|
textTag.setAttribute("pointer-events", "auto");
|
||||||
|
textTag.setAttribute("fill", this.textColor);
|
||||||
|
textTag.setAttribute(
|
||||||
|
"font-family",
|
||||||
|
'"Open Sans","Helvetica Neue",Helvetica,Arial,' +
|
||||||
|
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||||
|
);
|
||||||
|
textTag.textContent = character;
|
||||||
|
textTag.style.fontWeight = this.fontWeight.toString();
|
||||||
|
textTag.style.fontSize = this.fontSize + "px";
|
||||||
|
return textTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unicodeSafeSubstring(str: string, count: number) {
|
||||||
|
const characters = str.match(/./gu);
|
||||||
|
return characters != null ? characters.slice(0, count).join("") : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
35
libs/angular/src/components/callout.component.html
Normal file
35
libs/angular/src/components/callout.component.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<div
|
||||||
|
#callout
|
||||||
|
class="callout callout-{{ calloutStyle }}"
|
||||||
|
[ngClass]="{ clickable: clickable }"
|
||||||
|
[attr.role]="useAlertRole ? 'alert' : null"
|
||||||
|
>
|
||||||
|
<h3 class="callout-heading" *ngIf="title">
|
||||||
|
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<div class="enforced-policy-options" *ngIf="enforcedPolicyOptions">
|
||||||
|
{{ enforcedPolicyMessage }}
|
||||||
|
<ul>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||||
|
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||||
|
{{ "policyInEffectMinLength" | i18n: enforcedPolicyOptions?.minLength.toString() }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||||
|
{{ "policyInEffectUppercase" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||||
|
{{ "policyInEffectLowercase" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||||
|
{{ "policyInEffectNumbers" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||||
|
{{ "policyInEffectSpecial" | i18n: "!@#$%^&*" }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
78
libs/angular/src/components/callout.component.ts
Normal file
78
libs/angular/src/components/callout.component.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-callout",
|
||||||
|
templateUrl: "callout.component.html",
|
||||||
|
})
|
||||||
|
export class CalloutComponent implements OnInit {
|
||||||
|
@Input() type = "info";
|
||||||
|
@Input() icon: string;
|
||||||
|
@Input() title: string;
|
||||||
|
@Input() clickable: boolean;
|
||||||
|
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
|
@Input() enforcedPolicyMessage: string;
|
||||||
|
@Input() useAlertRole = false;
|
||||||
|
|
||||||
|
calloutStyle: string;
|
||||||
|
|
||||||
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.calloutStyle = this.type;
|
||||||
|
|
||||||
|
if (this.enforcedPolicyMessage === undefined) {
|
||||||
|
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === "warning" || this.type === "danger") {
|
||||||
|
if (this.type === "danger") {
|
||||||
|
this.calloutStyle = "danger";
|
||||||
|
}
|
||||||
|
if (this.title === undefined) {
|
||||||
|
this.title = this.i18nService.t("warning");
|
||||||
|
}
|
||||||
|
if (this.icon === undefined) {
|
||||||
|
this.icon = "bwi-exclamation-triangle";
|
||||||
|
}
|
||||||
|
} else if (this.type === "error") {
|
||||||
|
this.calloutStyle = "danger";
|
||||||
|
if (this.title === undefined) {
|
||||||
|
this.title = this.i18nService.t("error");
|
||||||
|
}
|
||||||
|
if (this.icon === undefined) {
|
||||||
|
this.icon = "bwi-error";
|
||||||
|
}
|
||||||
|
} else if (this.type === "tip") {
|
||||||
|
this.calloutStyle = "success";
|
||||||
|
if (this.title === undefined) {
|
||||||
|
this.title = this.i18nService.t("tip");
|
||||||
|
}
|
||||||
|
if (this.icon === undefined) {
|
||||||
|
this.icon = "bwi-lightbulb";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPasswordScoreAlertDisplay() {
|
||||||
|
if (this.enforcedPolicyOptions == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let str: string;
|
||||||
|
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||||
|
case 4:
|
||||||
|
str = this.i18nService.t("strong");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
str = this.i18nService.t("good");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
str = this.i18nService.t("weak");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
53
libs/angular/src/components/captchaProtected.component.ts
Normal file
53
libs/angular/src/components/captchaProtected.component.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Directive, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { CaptchaIFrame } from "jslib-common/misc/captcha_iframe";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export abstract class CaptchaProtectedComponent {
|
||||||
|
@Input() captchaSiteKey: string = null;
|
||||||
|
captchaToken: string = null;
|
||||||
|
captcha: CaptchaIFrame;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async setupCaptcha() {
|
||||||
|
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
|
||||||
|
this.captcha = new CaptchaIFrame(
|
||||||
|
window,
|
||||||
|
webVaultUrl,
|
||||||
|
this.i18nService,
|
||||||
|
(token: string) => {
|
||||||
|
this.captchaToken = token;
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error);
|
||||||
|
},
|
||||||
|
(info: string) => {
|
||||||
|
this.platformUtilsService.showToast("info", this.i18nService.t("info"), info);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showCaptcha() {
|
||||||
|
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleCaptchaRequired(response: { captchaSiteKey: string }): boolean {
|
||||||
|
if (Utils.isNullOrWhitespace(response.captchaSiteKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.captchaSiteKey = response.captchaSiteKey;
|
||||||
|
this.captcha.init(response.captchaSiteKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
195
libs/angular/src/components/change-password.component.ts
Normal file
195
libs/angular/src/components/change-password.component.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { KdfType } from "jslib-common/enums/kdfType";
|
||||||
|
import { EncString } from "jslib-common/models/domain/encString";
|
||||||
|
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
|
||||||
|
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class ChangePasswordComponent implements OnInit {
|
||||||
|
masterPassword: string;
|
||||||
|
masterPasswordRetype: string;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
masterPasswordScore: number;
|
||||||
|
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
|
|
||||||
|
protected email: string;
|
||||||
|
protected kdf: KdfType;
|
||||||
|
protected kdfIterations: number;
|
||||||
|
|
||||||
|
private masterPasswordStrengthTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.email = await this.stateService.getEmail();
|
||||||
|
this.enforcedPolicyOptions ??= await this.policyService.getMasterPasswordPolicyOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!(await this.strongPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.setupSubmitActions())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = await this.stateService.getEmail();
|
||||||
|
if (this.kdf == null) {
|
||||||
|
this.kdf = await this.stateService.getKdfType();
|
||||||
|
}
|
||||||
|
if (this.kdfIterations == null) {
|
||||||
|
this.kdfIterations = await this.stateService.getKdfIterations();
|
||||||
|
}
|
||||||
|
const key = await this.cryptoService.makeKey(
|
||||||
|
this.masterPassword,
|
||||||
|
email.trim().toLowerCase(),
|
||||||
|
this.kdf,
|
||||||
|
this.kdfIterations
|
||||||
|
);
|
||||||
|
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||||
|
|
||||||
|
let encKey: [SymmetricCryptoKey, EncString] = null;
|
||||||
|
const existingEncKey = await this.cryptoService.getEncKey();
|
||||||
|
if (existingEncKey == null) {
|
||||||
|
encKey = await this.cryptoService.makeEncKey(key);
|
||||||
|
} else {
|
||||||
|
encKey = await this.cryptoService.remakeEncKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.performSubmitActions(masterPasswordHash, key, encKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupSubmitActions(): Promise<boolean> {
|
||||||
|
// Override in sub-class
|
||||||
|
// Can be used for additional validation and/or other processes the should occur before changing passwords
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSubmitActions(
|
||||||
|
masterPasswordHash: string,
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
encKey: [SymmetricCryptoKey, EncString]
|
||||||
|
) {
|
||||||
|
// Override in sub-class
|
||||||
|
}
|
||||||
|
|
||||||
|
async strongPassword(): Promise<boolean> {
|
||||||
|
if (this.masterPassword == null || this.masterPassword === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassRequired")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.masterPassword.length < 8) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassLength")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.masterPassword !== this.masterPasswordRetype) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassDoesntMatch")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||||
|
this.masterPassword,
|
||||||
|
this.getPasswordStrengthUserInput()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.enforcedPolicyOptions != null &&
|
||||||
|
!this.policyService.evaluateMasterPassword(
|
||||||
|
strengthResult.score,
|
||||||
|
this.masterPassword,
|
||||||
|
this.enforcedPolicyOptions
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strengthResult != null && strengthResult.score < 3) {
|
||||||
|
const result = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("weakMasterPasswordDesc"),
|
||||||
|
this.i18nService.t("weakMasterPassword"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePasswordStrength() {
|
||||||
|
if (this.masterPasswordStrengthTimeout != null) {
|
||||||
|
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||||
|
}
|
||||||
|
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||||
|
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||||
|
this.masterPassword,
|
||||||
|
this.getPasswordStrengthUserInput()
|
||||||
|
);
|
||||||
|
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
async logOut() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("logOutConfirmation"),
|
||||||
|
this.i18nService.t("logOut"),
|
||||||
|
this.i18nService.t("logOut"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPasswordStrengthUserInput() {
|
||||||
|
let userInput: string[] = [];
|
||||||
|
const atPosition = this.email.indexOf("@");
|
||||||
|
if (atPosition > -1) {
|
||||||
|
userInput = userInput.concat(
|
||||||
|
this.email
|
||||||
|
.substr(0, atPosition)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^A-Za-z0-9]/)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return userInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
libs/angular/src/components/ciphers.component.ts
Normal file
92
libs/angular/src/components/ciphers.component.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class CiphersComponent {
|
||||||
|
@Input() activeCipherId: string = null;
|
||||||
|
@Output() onCipherClicked = new EventEmitter<CipherView>();
|
||||||
|
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
|
||||||
|
@Output() onAddCipher = new EventEmitter();
|
||||||
|
@Output() onAddCipherOptions = new EventEmitter();
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
ciphers: CipherView[] = [];
|
||||||
|
searchText: string;
|
||||||
|
searchPlaceholder: string = null;
|
||||||
|
filter: (cipher: CipherView) => boolean = null;
|
||||||
|
deleted = false;
|
||||||
|
|
||||||
|
protected searchPending = false;
|
||||||
|
|
||||||
|
private searchTimeout: any = null;
|
||||||
|
|
||||||
|
constructor(protected searchService: SearchService) {}
|
||||||
|
|
||||||
|
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||||
|
this.deleted = deleted || false;
|
||||||
|
await this.applyFilter(filter);
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||||
|
this.loaded = false;
|
||||||
|
await this.load(filter, deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await this.reload(this.filter, this.deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||||
|
this.filter = filter;
|
||||||
|
await this.search(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(timeout: number = null, indexedCiphers?: CipherView[]) {
|
||||||
|
this.searchPending = false;
|
||||||
|
if (this.searchTimeout != null) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
if (timeout == null) {
|
||||||
|
await this.doSearch(indexedCiphers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.searchPending = true;
|
||||||
|
this.searchTimeout = setTimeout(async () => {
|
||||||
|
await this.doSearch(indexedCiphers);
|
||||||
|
this.searchPending = false;
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCipher(cipher: CipherView) {
|
||||||
|
this.onCipherClicked.emit(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
rightClickCipher(cipher: CipherView) {
|
||||||
|
this.onCipherRightClicked.emit(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipher() {
|
||||||
|
this.onAddCipher.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
addCipherOptions() {
|
||||||
|
this.onAddCipherOptions.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching() {
|
||||||
|
return !this.searchPending && this.searchService.isSearchable(this.searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||||
|
|
||||||
|
protected async doSearch(indexedCiphers?: CipherView[]) {
|
||||||
|
this.ciphers = await this.searchService.searchCiphers(
|
||||||
|
this.searchText,
|
||||||
|
[this.filter, this.deletedFilter],
|
||||||
|
indexedCiphers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
libs/angular/src/components/collections.component.ts
Normal file
92
libs/angular/src/components/collections.component.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { Cipher } from "jslib-common/models/domain/cipher";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class CollectionsComponent implements OnInit {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Input() allowSelectNone = false;
|
||||||
|
@Output() onSavedCollections = new EventEmitter();
|
||||||
|
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
cipher: CipherView;
|
||||||
|
collectionIds: string[];
|
||||||
|
collections: CollectionView[] = [];
|
||||||
|
|
||||||
|
protected cipherDomain: Cipher;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected collectionService: CollectionService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.cipherDomain = await this.loadCipher();
|
||||||
|
this.collectionIds = this.loadCipherCollections();
|
||||||
|
this.cipher = await this.cipherDomain.decrypt();
|
||||||
|
this.collections = await this.loadCollections();
|
||||||
|
|
||||||
|
this.collections.forEach((c) => ((c as any).checked = false));
|
||||||
|
if (this.collectionIds != null) {
|
||||||
|
this.collections.forEach((c) => {
|
||||||
|
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const selectedCollectionIds = this.collections
|
||||||
|
.filter((c) => !!(c as any).checked)
|
||||||
|
.map((c) => c.id);
|
||||||
|
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("selectOneCollection")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cipherDomain.collectionIds = selectedCollectionIds;
|
||||||
|
try {
|
||||||
|
this.formPromise = this.saveCollections();
|
||||||
|
await this.formPromise;
|
||||||
|
this.onSavedCollections.emit();
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem"));
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadCipher() {
|
||||||
|
return this.cipherService.get(this.cipherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadCipherCollections() {
|
||||||
|
return this.cipherDomain.collectionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadCollections() {
|
||||||
|
const allCollections = await this.collectionService.getAllDecrypted();
|
||||||
|
return allCollections.filter(
|
||||||
|
(c) => !c.readOnly && c.organizationId === this.cipher.organizationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveCollections() {
|
||||||
|
return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
libs/angular/src/components/environment.component.ts
Normal file
63
libs/angular/src/components/environment.component.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class EnvironmentComponent {
|
||||||
|
@Output() onSaved = new EventEmitter();
|
||||||
|
|
||||||
|
iconsUrl: string;
|
||||||
|
identityUrl: string;
|
||||||
|
apiUrl: string;
|
||||||
|
webVaultUrl: string;
|
||||||
|
notificationsUrl: string;
|
||||||
|
baseUrl: string;
|
||||||
|
showCustom = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected i18nService: I18nService
|
||||||
|
) {
|
||||||
|
const urls = this.environmentService.getUrls();
|
||||||
|
|
||||||
|
this.baseUrl = urls.base || "";
|
||||||
|
this.webVaultUrl = urls.webVault || "";
|
||||||
|
this.apiUrl = urls.api || "";
|
||||||
|
this.identityUrl = urls.identity || "";
|
||||||
|
this.iconsUrl = urls.icons || "";
|
||||||
|
this.notificationsUrl = urls.notifications || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const resUrls = await this.environmentService.setUrls({
|
||||||
|
base: this.baseUrl,
|
||||||
|
api: this.apiUrl,
|
||||||
|
identity: this.identityUrl,
|
||||||
|
webVault: this.webVaultUrl,
|
||||||
|
icons: this.iconsUrl,
|
||||||
|
notifications: this.notificationsUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// re-set urls since service can change them, ex: prefixing https://
|
||||||
|
this.baseUrl = resUrls.base;
|
||||||
|
this.apiUrl = resUrls.api;
|
||||||
|
this.identityUrl = resUrls.identity;
|
||||||
|
this.webVaultUrl = resUrls.webVault;
|
||||||
|
this.iconsUrl = resUrls.icons;
|
||||||
|
this.notificationsUrl = resUrls.notifications;
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||||
|
this.saved();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCustom() {
|
||||||
|
this.showCustom = !this.showCustom;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saved() {
|
||||||
|
this.onSaved.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<ng-container *ngIf="show">
|
||||||
|
<app-callout type="info" title="{{ scopeConfig.title | i18n }}">
|
||||||
|
{{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }}
|
||||||
|
</app-callout>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-export-scope-callout",
|
||||||
|
templateUrl: "export-scope-callout.component.html",
|
||||||
|
})
|
||||||
|
export class ExportScopeCalloutComponent implements OnInit {
|
||||||
|
@Input() organizationId: string = null;
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
scopeConfig: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
scopeIdentifier: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected organizationService: OrganizationService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
if (!(await this.organizationService.hasOrganizations())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scopeConfig =
|
||||||
|
this.organizationId != null
|
||||||
|
? {
|
||||||
|
title: "exportingOrganizationVaultTitle",
|
||||||
|
description: "exportingOrganizationVaultDescription",
|
||||||
|
scopeIdentifier: (await this.organizationService.get(this.organizationId)).name,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: "exportingPersonalVaultTitle",
|
||||||
|
description: "exportingPersonalVaultDescription",
|
||||||
|
scopeIdentifier: await this.stateService.getEmail(),
|
||||||
|
};
|
||||||
|
this.show = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
libs/angular/src/components/export.component.ts
Normal file
155
libs/angular/src/components/export.component.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
|
import { ExportService } from "jslib-common/abstractions/export.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
||||||
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class ExportComponent implements OnInit {
|
||||||
|
@Output() onSaved = new EventEmitter();
|
||||||
|
|
||||||
|
formPromise: Promise<string>;
|
||||||
|
disabledByPolicy = false;
|
||||||
|
|
||||||
|
exportForm = this.formBuilder.group({
|
||||||
|
format: ["json"],
|
||||||
|
secret: [""],
|
||||||
|
});
|
||||||
|
|
||||||
|
formatOptions = [
|
||||||
|
{ name: ".json", value: "json" },
|
||||||
|
{ name: ".csv", value: "csv" },
|
||||||
|
{ name: ".json (Encrypted)", value: "encrypted_json" },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected exportService: ExportService,
|
||||||
|
protected eventService: EventService,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
protected win: Window,
|
||||||
|
private logService: LogService,
|
||||||
|
private userVerificationService: UserVerificationService,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.checkExportDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExportDisabled() {
|
||||||
|
this.disabledByPolicy = await this.policyService.policyAppliesToUser(
|
||||||
|
PolicyType.DisablePersonalVaultExport
|
||||||
|
);
|
||||||
|
if (this.disabledByPolicy) {
|
||||||
|
this.exportForm.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get encryptedFormat() {
|
||||||
|
return this.format === "encrypted_json";
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (this.disabledByPolicy) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("personalVaultExportPolicyInEffect")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptedWarning = await this.warningDialog();
|
||||||
|
if (!acceptedWarning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = this.exportForm.get("secret").value;
|
||||||
|
try {
|
||||||
|
await this.userVerificationService.verifyUser(secret);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.getExportData();
|
||||||
|
const data = await this.formPromise;
|
||||||
|
this.downloadFile(data);
|
||||||
|
this.saved();
|
||||||
|
await this.collectEvent();
|
||||||
|
this.exportForm.get("secret").setValue("");
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async warningDialog() {
|
||||||
|
if (this.encryptedFormat) {
|
||||||
|
return await this.platformUtilsService.showDialog(
|
||||||
|
"<p>" +
|
||||||
|
this.i18nService.t("encExportKeyWarningDesc") +
|
||||||
|
"<p>" +
|
||||||
|
this.i18nService.t("encExportAccountWarningDesc"),
|
||||||
|
this.i18nService.t("confirmVaultExport"),
|
||||||
|
this.i18nService.t("exportVault"),
|
||||||
|
this.i18nService.t("cancel"),
|
||||||
|
"warning",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("exportWarningDesc"),
|
||||||
|
this.i18nService.t("confirmVaultExport"),
|
||||||
|
this.i18nService.t("exportVault"),
|
||||||
|
this.i18nService.t("cancel"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saved() {
|
||||||
|
this.onSaved.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getExportData() {
|
||||||
|
return this.exportService.getExport(this.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFileName(prefix?: string) {
|
||||||
|
let extension = this.format;
|
||||||
|
if (this.format === "encrypted_json") {
|
||||||
|
if (prefix == null) {
|
||||||
|
prefix = "encrypted";
|
||||||
|
} else {
|
||||||
|
prefix = "encrypted_" + prefix;
|
||||||
|
}
|
||||||
|
extension = "json";
|
||||||
|
}
|
||||||
|
return this.exportService.getFileName(prefix, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async collectEvent(): Promise<any> {
|
||||||
|
await this.eventService.collect(EventType.User_ClientExportedVault);
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() {
|
||||||
|
return this.exportForm.get("format").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private downloadFile(csv: string): void {
|
||||||
|
const fileName = this.getFileName();
|
||||||
|
this.platformUtilsService.saveFile(this.win, csv, { type: "text/plain" }, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
libs/angular/src/components/folder-add-edit.component.ts
Normal file
96
libs/angular/src/components/folder-add-edit.component.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { FolderView } from "jslib-common/models/view/folderView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class FolderAddEditComponent implements OnInit {
|
||||||
|
@Input() folderId: string;
|
||||||
|
@Output() onSavedFolder = new EventEmitter<FolderView>();
|
||||||
|
@Output() onDeletedFolder = new EventEmitter<FolderView>();
|
||||||
|
|
||||||
|
editMode = false;
|
||||||
|
folder: FolderView = new FolderView();
|
||||||
|
title: string;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
deletePromise: Promise<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected folderService: FolderService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(): Promise<boolean> {
|
||||||
|
if (this.folder.name == null || this.folder.name === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("nameRequired")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const folder = await this.folderService.encrypt(this.folder);
|
||||||
|
this.formPromise = this.folderService.saveWithServer(folder);
|
||||||
|
await this.formPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder")
|
||||||
|
);
|
||||||
|
this.onSavedFolder.emit(this.folder);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<boolean> {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("deleteFolderConfirmation"),
|
||||||
|
this.i18nService.t("deleteFolder"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePromise = this.folderService.deleteWithServer(this.folder.id);
|
||||||
|
await this.deletePromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedFolder"));
|
||||||
|
this.onDeletedFolder.emit(this.folder);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init() {
|
||||||
|
this.editMode = this.folderId != null;
|
||||||
|
|
||||||
|
if (this.editMode) {
|
||||||
|
this.editMode = true;
|
||||||
|
this.title = this.i18nService.t("editFolder");
|
||||||
|
const folder = await this.folderService.get(this.folderId);
|
||||||
|
this.folder = await folder.decrypt();
|
||||||
|
} else {
|
||||||
|
this.title = this.i18nService.t("addFolder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
240
libs/angular/src/components/generator.component.ts
Normal file
240
libs/angular/src/components/generator.component.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { UsernameGenerationService } from "jslib-common/abstractions/usernameGeneration.service";
|
||||||
|
import { PasswordGeneratorPolicyOptions } from "jslib-common/models/domain/passwordGeneratorPolicyOptions";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class GeneratorComponent implements OnInit {
|
||||||
|
@Input() comingFromAddEdit = false;
|
||||||
|
@Input() type: string;
|
||||||
|
@Output() onSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
|
usernameGeneratingPromise: Promise<string>;
|
||||||
|
typeOptions: any[];
|
||||||
|
passTypeOptions: any[];
|
||||||
|
usernameTypeOptions: any[];
|
||||||
|
subaddressOptions: any[];
|
||||||
|
catchallOptions: any[];
|
||||||
|
forwardOptions: any[];
|
||||||
|
usernameOptions: any = {};
|
||||||
|
passwordOptions: any = {};
|
||||||
|
username = "-";
|
||||||
|
password = "-";
|
||||||
|
showOptions = false;
|
||||||
|
avoidAmbiguous = false;
|
||||||
|
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
|
usernameWebsite: string = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
protected usernameGenerationService: UsernameGenerationService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
private win: Window
|
||||||
|
) {
|
||||||
|
this.typeOptions = [
|
||||||
|
{ name: i18nService.t("password"), value: "password" },
|
||||||
|
{ name: i18nService.t("username"), value: "username" },
|
||||||
|
];
|
||||||
|
this.passTypeOptions = [
|
||||||
|
{ name: i18nService.t("password"), value: "password" },
|
||||||
|
{ name: i18nService.t("passphrase"), value: "passphrase" },
|
||||||
|
];
|
||||||
|
this.usernameTypeOptions = [
|
||||||
|
{
|
||||||
|
name: i18nService.t("plusAddressedEmail"),
|
||||||
|
value: "subaddress",
|
||||||
|
desc: i18nService.t("plusAddressedEmailDesc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18nService.t("catchallEmail"),
|
||||||
|
value: "catchall",
|
||||||
|
desc: i18nService.t("catchallEmailDesc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18nService.t("forwardedEmail"),
|
||||||
|
value: "forwarded",
|
||||||
|
desc: i18nService.t("forwardedEmailDesc"),
|
||||||
|
},
|
||||||
|
{ name: i18nService.t("randomWord"), value: "word" },
|
||||||
|
];
|
||||||
|
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
|
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
|
this.forwardOptions = [
|
||||||
|
{ name: "SimpleLogin", value: "simplelogin" },
|
||||||
|
{ name: "AnonAddy", value: "anonaddy" },
|
||||||
|
{ name: "Firefox Relay", value: "firefoxrelay" },
|
||||||
|
// { name: "FastMail", value: "fastmail" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
|
const passwordOptionsResponse = await this.passwordGenerationService.getOptions();
|
||||||
|
this.passwordOptions = passwordOptionsResponse[0];
|
||||||
|
this.enforcedPasswordPolicyOptions = passwordOptionsResponse[1];
|
||||||
|
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||||
|
this.passwordOptions.type =
|
||||||
|
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||||
|
|
||||||
|
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||||
|
if (this.usernameOptions.type == null) {
|
||||||
|
this.usernameOptions.type = "word";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.usernameOptions.subaddressEmail == null ||
|
||||||
|
this.usernameOptions.subaddressEmail === ""
|
||||||
|
) {
|
||||||
|
this.usernameOptions.subaddressEmail = await this.stateService.getEmail();
|
||||||
|
}
|
||||||
|
if (this.usernameWebsite == null) {
|
||||||
|
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||||
|
} else {
|
||||||
|
this.usernameOptions.website = this.usernameWebsite;
|
||||||
|
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||||
|
this.subaddressOptions.push(websiteOption);
|
||||||
|
this.catchallOptions.push(websiteOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type !== "username" && this.type !== "password") {
|
||||||
|
if (qParams.type === "username" || qParams.type === "password") {
|
||||||
|
this.type = qParams.type;
|
||||||
|
} else {
|
||||||
|
const generatorOptions = await this.stateService.getGeneratorOptions();
|
||||||
|
this.type = generatorOptions?.type ?? "password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regenerate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeChanged() {
|
||||||
|
await this.stateService.setGeneratorOptions({ type: this.type });
|
||||||
|
if (this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regenerate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async regenerate() {
|
||||||
|
if (this.type === "password") {
|
||||||
|
await this.regeneratePassword();
|
||||||
|
} else if (this.type === "username") {
|
||||||
|
await this.regenerateUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sliderChanged() {
|
||||||
|
this.savePasswordOptions(false);
|
||||||
|
await this.passwordGenerationService.addHistory(this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sliderInput() {
|
||||||
|
this.normalizePasswordOptions();
|
||||||
|
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePasswordOptions(regenerate = true) {
|
||||||
|
this.normalizePasswordOptions();
|
||||||
|
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||||
|
|
||||||
|
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regeneratePassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveUsernameOptions(regenerate = true) {
|
||||||
|
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||||
|
if (this.usernameOptions.type === "forwarded") {
|
||||||
|
this.username = "-";
|
||||||
|
}
|
||||||
|
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||||
|
await this.regenerateUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async regeneratePassword() {
|
||||||
|
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||||
|
await this.passwordGenerationService.addHistory(this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateUsername() {
|
||||||
|
return this.generateUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateUsername() {
|
||||||
|
try {
|
||||||
|
this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
|
||||||
|
this.usernameOptions
|
||||||
|
);
|
||||||
|
this.username = await this.usernameGeneratingPromise;
|
||||||
|
if (this.username === "" || this.username === null) {
|
||||||
|
this.username = "-";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy() {
|
||||||
|
const password = this.type === "password";
|
||||||
|
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||||
|
this.platformUtilsService.copyToClipboard(
|
||||||
|
password ? this.password : this.username,
|
||||||
|
copyOptions
|
||||||
|
);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("valueCopied", this.i18nService.t(password ? "password" : "username"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
select() {
|
||||||
|
this.onSelected.emit(this.type === "password" ? this.password : this.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateWithoutButtonPress() {
|
||||||
|
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePasswordOptions() {
|
||||||
|
// Application level normalize options depedent on class variables
|
||||||
|
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.passwordOptions.uppercase &&
|
||||||
|
!this.passwordOptions.lowercase &&
|
||||||
|
!this.passwordOptions.number &&
|
||||||
|
!this.passwordOptions.special
|
||||||
|
) {
|
||||||
|
this.passwordOptions.lowercase = true;
|
||||||
|
if (this.win != null) {
|
||||||
|
const lowercase = this.win.document.querySelector("#lowercase") as HTMLInputElement;
|
||||||
|
if (lowercase) {
|
||||||
|
lowercase.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.passwordGenerationService.normalizeOptions(
|
||||||
|
this.passwordOptions,
|
||||||
|
this.enforcedPasswordPolicyOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
libs/angular/src/components/hint.component.ts
Normal file
55
libs/angular/src/components/hint.component.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PasswordHintRequest } from "jslib-common/models/request/passwordHintRequest";
|
||||||
|
|
||||||
|
export class HintComponent {
|
||||||
|
email = "";
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
|
protected successRoute = "login";
|
||||||
|
protected onSuccessfulSubmit: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (this.email == null || this.email === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("emailRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.email.indexOf("@") === -1) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidEmail")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
|
||||||
|
await this.formPromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("masterPassSent"));
|
||||||
|
if (this.onSuccessfulSubmit != null) {
|
||||||
|
this.onSuccessfulSubmit();
|
||||||
|
} else if (this.router != null) {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/angular/src/components/icon.component.html
Normal file
11
libs/angular/src/components/icon.component.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="icon" aria-hidden="true">
|
||||||
|
<img
|
||||||
|
[src]="image"
|
||||||
|
appFallbackSrc="{{ fallbackImage }}"
|
||||||
|
*ngIf="imageEnabled && image"
|
||||||
|
alt=""
|
||||||
|
decoding="async"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<i class="bwi bwi-fw bwi-lg {{ icon }}" *ngIf="!imageEnabled || !image"></i>
|
||||||
|
</div>
|
||||||
113
libs/angular/src/components/icon.component.ts
Normal file
113
libs/angular/src/components/icon.component.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Component, Input, OnChanges } from "@angular/core";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mapping from supported card brands to
|
||||||
|
* the filenames of icon that should be present in images/cards folder of clients.
|
||||||
|
*/
|
||||||
|
const cardIcons: Record<string, string> = {
|
||||||
|
Visa: "card-visa",
|
||||||
|
Mastercard: "card-mastercard",
|
||||||
|
Amex: "card-amex",
|
||||||
|
Discover: "card-discover",
|
||||||
|
"Diners Club": "card-diners-club",
|
||||||
|
JCB: "card-jcb",
|
||||||
|
Maestro: "card-maestro",
|
||||||
|
UnionPay: "card-union-pay",
|
||||||
|
RuPay: "card-ru-pay",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-icon",
|
||||||
|
templateUrl: "icon.component.html",
|
||||||
|
})
|
||||||
|
export class IconComponent implements OnChanges {
|
||||||
|
@Input() cipher: CipherView;
|
||||||
|
icon: string;
|
||||||
|
image: string;
|
||||||
|
fallbackImage: string;
|
||||||
|
imageEnabled: boolean;
|
||||||
|
|
||||||
|
private iconsUrl: string;
|
||||||
|
|
||||||
|
constructor(environmentService: EnvironmentService, private stateService: StateService) {
|
||||||
|
this.iconsUrl = environmentService.getIconsUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnChanges() {
|
||||||
|
// Components may be re-used when using cdk-virtual-scroll. Which puts the component in a weird state,
|
||||||
|
// to avoid this we reset all state variables.
|
||||||
|
this.image = null;
|
||||||
|
this.fallbackImage = null;
|
||||||
|
this.imageEnabled = !(await this.stateService.getDisableFavicon());
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected load() {
|
||||||
|
switch (this.cipher.type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
this.icon = "bwi-globe";
|
||||||
|
this.setLoginIcon();
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
this.icon = "bwi-sticky-note";
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
this.icon = "bwi-credit-card";
|
||||||
|
this.setCardIcon();
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
this.icon = "bwi-id-card";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setLoginIcon() {
|
||||||
|
if (this.cipher.login.uri) {
|
||||||
|
let hostnameUri = this.cipher.login.uri;
|
||||||
|
let isWebsite = false;
|
||||||
|
|
||||||
|
if (hostnameUri.indexOf("androidapp://") === 0) {
|
||||||
|
this.icon = "bwi-android";
|
||||||
|
this.image = null;
|
||||||
|
} else if (hostnameUri.indexOf("iosapp://") === 0) {
|
||||||
|
this.icon = "bwi-apple";
|
||||||
|
this.image = null;
|
||||||
|
} else if (
|
||||||
|
this.imageEnabled &&
|
||||||
|
hostnameUri.indexOf("://") === -1 &&
|
||||||
|
hostnameUri.indexOf(".") > -1
|
||||||
|
) {
|
||||||
|
hostnameUri = "http://" + hostnameUri;
|
||||||
|
isWebsite = true;
|
||||||
|
} else if (this.imageEnabled) {
|
||||||
|
isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.imageEnabled && isWebsite) {
|
||||||
|
try {
|
||||||
|
this.image = this.iconsUrl + "/" + Utils.getHostname(hostnameUri) + "/icon.png";
|
||||||
|
this.fallbackImage = "images/bwi-globe.png";
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore error since the fallback icon will be shown if image is null.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCardIcon() {
|
||||||
|
const brand = this.cipher.card.brand;
|
||||||
|
if (this.imageEnabled && brand in cardIcons) {
|
||||||
|
this.icon = "credit-card-icon " + cardIcons[brand];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
277
libs/angular/src/components/lock.component.ts
Normal file
277
libs/angular/src/components/lock.component.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { Directive, NgZone, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
|
||||||
|
import { HashPurpose } from "jslib-common/enums/hashPurpose";
|
||||||
|
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { EncString } from "jslib-common/models/domain/encString";
|
||||||
|
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||||
|
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class LockComponent implements OnInit {
|
||||||
|
masterPassword = "";
|
||||||
|
pin = "";
|
||||||
|
showPassword = false;
|
||||||
|
email: string;
|
||||||
|
pinLock = false;
|
||||||
|
webVaultHostname = "";
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
supportsBiometric: boolean;
|
||||||
|
biometricLock: boolean;
|
||||||
|
biometricText: string;
|
||||||
|
hideInput: boolean;
|
||||||
|
|
||||||
|
protected successRoute = "vault";
|
||||||
|
protected onSuccessfulSubmit: () => Promise<void>;
|
||||||
|
|
||||||
|
private invalidPinAttempts = 0;
|
||||||
|
private pinSet: [boolean, boolean];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected vaultTimeoutService: VaultTimeoutService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected logService: LogService,
|
||||||
|
private keyConnectorService: KeyConnectorService,
|
||||||
|
protected ngZone: NgZone
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
// Load the first and observe updates
|
||||||
|
await this.load();
|
||||||
|
this.stateService.activeAccount.subscribe(async () => {
|
||||||
|
await this.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (this.pinLock && (this.pin == null || this.pin === "")) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("pinRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.pinLock && (this.masterPassword == null || this.masterPassword === "")) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kdf = await this.stateService.getKdfType();
|
||||||
|
const kdfIterations = await this.stateService.getKdfIterations();
|
||||||
|
|
||||||
|
if (this.pinLock) {
|
||||||
|
let failed = true;
|
||||||
|
try {
|
||||||
|
if (this.pinSet[0]) {
|
||||||
|
const key = await this.cryptoService.makeKeyFromPin(
|
||||||
|
this.pin,
|
||||||
|
this.email,
|
||||||
|
kdf,
|
||||||
|
kdfIterations,
|
||||||
|
await this.stateService.getDecryptedPinProtected()
|
||||||
|
);
|
||||||
|
const encKey = await this.cryptoService.getEncKey(key);
|
||||||
|
const protectedPin = await this.stateService.getProtectedPin();
|
||||||
|
const decPin = await this.cryptoService.decryptToUtf8(
|
||||||
|
new EncString(protectedPin),
|
||||||
|
encKey
|
||||||
|
);
|
||||||
|
failed = decPin !== this.pin;
|
||||||
|
if (!failed) {
|
||||||
|
await this.setKeyAndContinue(key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const key = await this.cryptoService.makeKeyFromPin(
|
||||||
|
this.pin,
|
||||||
|
this.email,
|
||||||
|
kdf,
|
||||||
|
kdfIterations
|
||||||
|
);
|
||||||
|
failed = false;
|
||||||
|
await this.setKeyAndContinue(key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
this.invalidPinAttempts++;
|
||||||
|
if (this.invalidPinAttempts >= 5) {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidPin")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const key = await this.cryptoService.makeKey(
|
||||||
|
this.masterPassword,
|
||||||
|
this.email,
|
||||||
|
kdf,
|
||||||
|
kdfIterations
|
||||||
|
);
|
||||||
|
const storedKeyHash = await this.cryptoService.getKeyHash();
|
||||||
|
|
||||||
|
let passwordValid = false;
|
||||||
|
|
||||||
|
if (storedKeyHash != null) {
|
||||||
|
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key);
|
||||||
|
} else {
|
||||||
|
const request = new SecretVerificationRequest();
|
||||||
|
const serverKeyHash = await this.cryptoService.hashPassword(
|
||||||
|
this.masterPassword,
|
||||||
|
key,
|
||||||
|
HashPurpose.ServerAuthorization
|
||||||
|
);
|
||||||
|
request.masterPasswordHash = serverKeyHash;
|
||||||
|
try {
|
||||||
|
this.formPromise = this.apiService.postAccountVerifyPassword(request);
|
||||||
|
await this.formPromise;
|
||||||
|
passwordValid = true;
|
||||||
|
const localKeyHash = await this.cryptoService.hashPassword(
|
||||||
|
this.masterPassword,
|
||||||
|
key,
|
||||||
|
HashPurpose.LocalAuthorization
|
||||||
|
);
|
||||||
|
await this.cryptoService.setKeyHash(localKeyHash);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordValid) {
|
||||||
|
if (this.pinSet[0]) {
|
||||||
|
const protectedPin = await this.stateService.getProtectedPin();
|
||||||
|
const encKey = await this.cryptoService.getEncKey(key);
|
||||||
|
const decPin = await this.cryptoService.decryptToUtf8(
|
||||||
|
new EncString(protectedPin),
|
||||||
|
encKey
|
||||||
|
);
|
||||||
|
const pinKey = await this.cryptoService.makePinKey(
|
||||||
|
decPin,
|
||||||
|
this.email,
|
||||||
|
kdf,
|
||||||
|
kdfIterations
|
||||||
|
);
|
||||||
|
await this.stateService.setDecryptedPinProtected(
|
||||||
|
await this.cryptoService.encrypt(key.key, pinKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.setKeyAndContinue(key);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidMasterPassword")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logOut() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("logOutConfirmation"),
|
||||||
|
this.i18nService.t("logOut"),
|
||||||
|
this.i18nService.t("logOut"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlockBiometric(): Promise<boolean> {
|
||||||
|
if (!this.biometricLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = (await this.cryptoService.getKey(KeySuffixOptions.Biometric)) != null;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await this.doContinue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
const input = document.getElementById(this.pinLock ? "pin" : "masterPassword");
|
||||||
|
if (this.ngZone.isStable) {
|
||||||
|
input.focus();
|
||||||
|
} else {
|
||||||
|
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setKeyAndContinue(key: SymmetricCryptoKey) {
|
||||||
|
await this.cryptoService.setKey(key);
|
||||||
|
await this.doContinue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doContinue() {
|
||||||
|
await this.stateService.setBiometricLocked(false);
|
||||||
|
await this.stateService.setEverBeenUnlocked(true);
|
||||||
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
|
this.messagingService.send("unlocked");
|
||||||
|
if (this.onSuccessfulSubmit != null) {
|
||||||
|
await this.onSuccessfulSubmit();
|
||||||
|
} else if (this.router != null) {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async load() {
|
||||||
|
this.pinSet = await this.vaultTimeoutService.isPinLockSet();
|
||||||
|
this.pinLock =
|
||||||
|
(this.pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) ||
|
||||||
|
this.pinSet[1];
|
||||||
|
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
||||||
|
this.biometricLock =
|
||||||
|
(await this.vaultTimeoutService.isBiometricLockSet()) &&
|
||||||
|
((await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric)) ||
|
||||||
|
!this.platformUtilsService.supportsSecureStorage());
|
||||||
|
this.biometricText = await this.stateService.getBiometricText();
|
||||||
|
this.email = await this.stateService.getEmail();
|
||||||
|
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
|
||||||
|
this.hideInput = usesKeyConnector && !this.pinLock;
|
||||||
|
|
||||||
|
// Users with key connector and without biometric or pin has no MP to unlock using
|
||||||
|
if (usesKeyConnector && !(this.biometricLock || this.pinLock)) {
|
||||||
|
await this.vaultTimeoutService.logOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
const vaultUrl =
|
||||||
|
webVaultUrl === "https://vault.bitwarden.com" ? "https://bitwarden.com" : webVaultUrl;
|
||||||
|
this.webVaultHostname = Utils.getHostname(vaultUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
libs/angular/src/components/login.component.ts
Normal file
192
libs/angular/src/components/login.component.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { Directive, Input, NgZone, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||||
|
import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class LoginComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
|
@Input() email = "";
|
||||||
|
@Input() rememberEmail = true;
|
||||||
|
|
||||||
|
masterPassword = "";
|
||||||
|
showPassword = false;
|
||||||
|
formPromise: Promise<AuthResult>;
|
||||||
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||||
|
|
||||||
|
protected twoFactorRoute = "2fa";
|
||||||
|
protected successRoute = "vault";
|
||||||
|
protected forcePasswordResetRoute = "update-temp-password";
|
||||||
|
protected alwaysRememberEmail = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected router: Router,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected ngZone: NgZone
|
||||||
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (this.email == null || this.email === "") {
|
||||||
|
this.email = await this.stateService.getRememberedEmail();
|
||||||
|
if (this.email == null) {
|
||||||
|
this.email = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.alwaysRememberEmail) {
|
||||||
|
this.rememberEmail = (await this.stateService.getRememberedEmail()) != null;
|
||||||
|
}
|
||||||
|
if (Utils.isBrowser && !Utils.isNode) {
|
||||||
|
this.focusInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
await this.setupCaptcha();
|
||||||
|
|
||||||
|
if (this.email == null || this.email === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("emailRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.email.indexOf("@") === -1) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidEmail")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.masterPassword == null || this.masterPassword === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credentials = new PasswordLogInCredentials(
|
||||||
|
this.email,
|
||||||
|
this.masterPassword,
|
||||||
|
this.captchaToken,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
this.formPromise = this.authService.logIn(credentials);
|
||||||
|
const response = await this.formPromise;
|
||||||
|
if (this.rememberEmail || this.alwaysRememberEmail) {
|
||||||
|
await this.stateService.setRememberedEmail(this.email);
|
||||||
|
} else {
|
||||||
|
await this.stateService.setRememberedEmail(null);
|
||||||
|
}
|
||||||
|
if (this.handleCaptchaRequired(response)) {
|
||||||
|
return;
|
||||||
|
} else if (response.requiresTwoFactor) {
|
||||||
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.twoFactorRoute]);
|
||||||
|
}
|
||||||
|
} else if (response.forcePasswordReset) {
|
||||||
|
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||||
|
this.onSuccessfulLoginForceResetNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.forcePasswordResetRoute]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
|
if (this.onSuccessfulLogin != null) {
|
||||||
|
this.onSuccessfulLogin();
|
||||||
|
}
|
||||||
|
if (this.onSuccessfulLoginNavigate != null) {
|
||||||
|
this.onSuccessfulLoginNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
if (this.ngZone.isStable) {
|
||||||
|
document.getElementById("masterPassword").focus();
|
||||||
|
} else {
|
||||||
|
this.ngZone.onStable
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe(() => document.getElementById("masterPassword").focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||||
|
// Generate necessary sso params
|
||||||
|
const passwordOptions: any = {
|
||||||
|
type: "password",
|
||||||
|
length: 64,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
numbers: true,
|
||||||
|
special: false,
|
||||||
|
};
|
||||||
|
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||||
|
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||||
|
|
||||||
|
// Save sso params
|
||||||
|
await this.stateService.setSsoState(state);
|
||||||
|
await this.stateService.setSsoCodeVerifier(ssoCodeVerifier);
|
||||||
|
|
||||||
|
// Build URI
|
||||||
|
const webUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
|
||||||
|
// Launch browser
|
||||||
|
this.platformUtilsService.launchUri(
|
||||||
|
webUrl +
|
||||||
|
"/#/sso?clientId=" +
|
||||||
|
clientId +
|
||||||
|
"&redirectUri=" +
|
||||||
|
encodeURIComponent(ssoRedirectUri) +
|
||||||
|
"&state=" +
|
||||||
|
state +
|
||||||
|
"&codeChallenge=" +
|
||||||
|
codeChallenge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected focusInput() {
|
||||||
|
document
|
||||||
|
.getElementById(this.email == null || this.email === "" ? "email" : "masterPassword")
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
libs/angular/src/components/modal/dynamic-modal.component.ts
Normal file
79
libs/angular/src/components/modal/dynamic-modal.component.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from "@angular/cdk/a11y";
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ComponentRef,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
Type,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import { ModalService } from "../../services/modal.service";
|
||||||
|
|
||||||
|
import { ModalRef } from "./modal.ref";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-modal",
|
||||||
|
template: "<ng-template #modalContent></ng-template>",
|
||||||
|
})
|
||||||
|
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
|
||||||
|
componentRef: ComponentRef<any>;
|
||||||
|
|
||||||
|
@ViewChild("modalContent", { read: ViewContainerRef, static: true })
|
||||||
|
modalContentRef: ViewContainerRef;
|
||||||
|
|
||||||
|
childComponentType: Type<any>;
|
||||||
|
setComponentParameters: (component: any) => void;
|
||||||
|
|
||||||
|
private focusTrap: ConfigurableFocusTrap;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private modalService: ModalService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private el: ElementRef<HTMLElement>,
|
||||||
|
private focusTrapFactory: ConfigurableFocusTrapFactory,
|
||||||
|
public modalRef: ModalRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.loadChildComponent(this.childComponentType);
|
||||||
|
if (this.setComponentParameters != null) {
|
||||||
|
this.setComponentParameters(this.componentRef.instance);
|
||||||
|
}
|
||||||
|
this.cd.detectChanges();
|
||||||
|
|
||||||
|
this.modalRef.created(this.el.nativeElement);
|
||||||
|
this.focusTrap = this.focusTrapFactory.create(
|
||||||
|
this.el.nativeElement.querySelector(".modal-dialog")
|
||||||
|
);
|
||||||
|
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
|
||||||
|
this.focusTrap.focusFirstTabbableElementWhenReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChildComponent(componentType: Type<any>) {
|
||||||
|
const componentFactory = this.modalService.resolveComponentFactory(componentType);
|
||||||
|
|
||||||
|
this.modalContentRef.clear();
|
||||||
|
this.componentRef = this.modalContentRef.createComponent(componentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.componentRef) {
|
||||||
|
this.componentRef.destroy();
|
||||||
|
}
|
||||||
|
this.focusTrap.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFocus() {
|
||||||
|
const autoFocusEl = this.el.nativeElement.querySelector("[appAutoFocus]") as HTMLElement;
|
||||||
|
autoFocusEl?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
libs/angular/src/components/modal/modal-injector.ts
Normal file
10
libs/angular/src/components/modal/modal-injector.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { InjectFlags, InjectionToken, Injector, Type } from "@angular/core";
|
||||||
|
|
||||||
|
export class ModalInjector implements Injector {
|
||||||
|
constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap<any, any>) {}
|
||||||
|
|
||||||
|
get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
|
||||||
|
get(token: any, notFoundValue?: any, flags?: any) {
|
||||||
|
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
libs/angular/src/components/modal/modal.ref.ts
Normal file
50
libs/angular/src/components/modal/modal.ref.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Observable, Subject } from "rxjs";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
export class ModalRef {
|
||||||
|
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
|
||||||
|
onClose: Observable<any>; // Initiated close.
|
||||||
|
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
|
||||||
|
onShow: Observable<void>; // Start showing modal
|
||||||
|
onShown: Observable<void>; // Modal is fully visible
|
||||||
|
|
||||||
|
private readonly _onCreated = new Subject<HTMLElement>();
|
||||||
|
private readonly _onClose = new Subject<any>();
|
||||||
|
private readonly _onClosed = new Subject<any>();
|
||||||
|
private readonly _onShow = new Subject<void>();
|
||||||
|
private readonly _onShown = new Subject<void>();
|
||||||
|
private lastResult: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.onCreated = this._onCreated.asObservable();
|
||||||
|
this.onClose = this._onClose.asObservable();
|
||||||
|
this.onClosed = this._onClosed.asObservable();
|
||||||
|
this.onShow = this._onShow.asObservable();
|
||||||
|
this.onShown = this._onShow.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this._onShow.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
shown() {
|
||||||
|
this._onShown.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(result?: any) {
|
||||||
|
this.lastResult = result;
|
||||||
|
this._onClose.next(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
closed() {
|
||||||
|
this._onClosed.next(this.lastResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
created(el: HTMLElement) {
|
||||||
|
this._onCreated.next(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosedPromise(): Promise<any> {
|
||||||
|
return this.onClosed.pipe(first()).toPromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { GeneratedPasswordHistory } from "jslib-common/models/domain/generatedPasswordHistory";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class PasswordGeneratorHistoryComponent implements OnInit {
|
||||||
|
history: GeneratedPasswordHistory[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
private win: Window
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.history = await this.passwordGenerationService.getHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.history = [];
|
||||||
|
this.passwordGenerationService.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(password: string) {
|
||||||
|
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||||
|
this.platformUtilsService.copyToClipboard(password, copyOptions);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("valueCopied", this.i18nService.t("password"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
libs/angular/src/components/password-history.component.ts
Normal file
39
libs/angular/src/components/password-history.component.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PasswordHistoryView } from "jslib-common/models/view/passwordHistoryView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class PasswordHistoryComponent implements OnInit {
|
||||||
|
cipherId: string;
|
||||||
|
history: PasswordHistoryView[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
private win: Window
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(password: string) {
|
||||||
|
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||||
|
this.platformUtilsService.copyToClipboard(password, copyOptions);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("valueCopied", this.i18nService.t("password"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init() {
|
||||||
|
const cipher = await this.cipherService.get(this.cipherId);
|
||||||
|
const decCipher = await cipher.decrypt();
|
||||||
|
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
libs/angular/src/components/password-reprompt.component.ts
Normal file
41
libs/angular/src/components/password-reprompt.component.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Directive } from "@angular/core";
|
||||||
|
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
import { ModalRef } from "./modal/modal.ref";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||||
|
* See UserVerificationComponent for any other situation where you need to verify the user's identity.
|
||||||
|
*/
|
||||||
|
@Directive()
|
||||||
|
export class PasswordRepromptComponent {
|
||||||
|
showPassword = false;
|
||||||
|
masterPassword = "";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private modalRef: ModalRef,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
togglePassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!(await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null))) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidMasterPassword")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modalRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
libs/angular/src/components/premium.component.ts
Normal file
61
libs/angular/src/components/premium.component.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class PremiumComponent implements OnInit {
|
||||||
|
isPremium = false;
|
||||||
|
price = 10;
|
||||||
|
refreshPromise: Promise<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.isPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
try {
|
||||||
|
this.refreshPromise = this.apiService.refreshIdentityToken();
|
||||||
|
await this.refreshPromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete"));
|
||||||
|
this.isPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async purchase() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("premiumPurchaseAlert"),
|
||||||
|
this.i18nService.t("premiumPurchase"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async manage() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("premiumManageAlert"),
|
||||||
|
this.i18nService.t("premiumManage"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=manage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
libs/angular/src/components/register.component.ts
Normal file
248
libs/angular/src/components/register.component.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { DEFAULT_KDF_ITERATIONS, DEFAULT_KDF_TYPE } from "jslib-common/enums/kdfType";
|
||||||
|
import { KeysRequest } from "jslib-common/models/request/keysRequest";
|
||||||
|
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
|
||||||
|
import { RegisterRequest } from "jslib-common/models/request/registerRequest";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class RegisterComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
|
name = "";
|
||||||
|
email = "";
|
||||||
|
masterPassword = "";
|
||||||
|
confirmMasterPassword = "";
|
||||||
|
hint = "";
|
||||||
|
showPassword = false;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
masterPasswordScore: number;
|
||||||
|
referenceData: ReferenceEventRequest;
|
||||||
|
showTerms = true;
|
||||||
|
acceptPolicies = false;
|
||||||
|
|
||||||
|
protected successRoute = "login";
|
||||||
|
private masterPasswordStrengthTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected router: Router,
|
||||||
|
i18nService: I18nService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
protected logService: LogService
|
||||||
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
this.showTerms = !platformUtilsService.isSelfHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.setupCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
|
get masterPasswordScoreWidth() {
|
||||||
|
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
get masterPasswordScoreColor() {
|
||||||
|
switch (this.masterPasswordScore) {
|
||||||
|
case 4:
|
||||||
|
return "success";
|
||||||
|
case 3:
|
||||||
|
return "primary";
|
||||||
|
case 2:
|
||||||
|
return "warning";
|
||||||
|
default:
|
||||||
|
return "danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get masterPasswordScoreText() {
|
||||||
|
switch (this.masterPasswordScore) {
|
||||||
|
case 4:
|
||||||
|
return this.i18nService.t("strong");
|
||||||
|
case 3:
|
||||||
|
return this.i18nService.t("good");
|
||||||
|
case 2:
|
||||||
|
return this.i18nService.t("weak");
|
||||||
|
default:
|
||||||
|
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!this.acceptPolicies && this.showTerms) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("acceptPoliciesError")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.email == null || this.email === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("emailRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.email.indexOf("@") === -1) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidEmail")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.masterPassword == null || this.masterPassword === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.masterPassword.length < 8) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassLength")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.masterPassword !== this.confirmMasterPassword) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassDoesntMatch")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||||
|
this.masterPassword,
|
||||||
|
this.getPasswordStrengthUserInput()
|
||||||
|
);
|
||||||
|
if (strengthResult != null && strengthResult.score < 3) {
|
||||||
|
const result = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("weakMasterPasswordDesc"),
|
||||||
|
this.i18nService.t("weakMasterPassword"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hint === this.masterPassword) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("hintEqualsPassword")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = this.name === "" ? null : this.name;
|
||||||
|
this.email = this.email.trim().toLowerCase();
|
||||||
|
const kdf = DEFAULT_KDF_TYPE;
|
||||||
|
const kdfIterations = DEFAULT_KDF_ITERATIONS;
|
||||||
|
const key = await this.cryptoService.makeKey(
|
||||||
|
this.masterPassword,
|
||||||
|
this.email,
|
||||||
|
kdf,
|
||||||
|
kdfIterations
|
||||||
|
);
|
||||||
|
const encKey = await this.cryptoService.makeEncKey(key);
|
||||||
|
const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||||
|
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||||
|
const request = new RegisterRequest(
|
||||||
|
this.email,
|
||||||
|
this.name,
|
||||||
|
hashedPassword,
|
||||||
|
this.hint,
|
||||||
|
encKey[1].encryptedString,
|
||||||
|
kdf,
|
||||||
|
kdfIterations,
|
||||||
|
this.referenceData,
|
||||||
|
this.captchaToken
|
||||||
|
);
|
||||||
|
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
||||||
|
const orgInvite = await this.stateService.getOrganizationInvitation();
|
||||||
|
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
|
||||||
|
request.token = orgInvite.token;
|
||||||
|
request.organizationUserId = orgInvite.organizationUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.apiService.postRegister(request);
|
||||||
|
try {
|
||||||
|
await this.formPromise;
|
||||||
|
} catch (e) {
|
||||||
|
if (this.handleCaptchaRequired(e)) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("newAccountCreated"));
|
||||||
|
this.router.navigate([this.successRoute], { queryParams: { email: this.email } });
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword(confirmField: boolean) {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePasswordStrength() {
|
||||||
|
if (this.masterPasswordStrengthTimeout != null) {
|
||||||
|
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||||
|
}
|
||||||
|
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||||
|
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||||
|
this.masterPassword,
|
||||||
|
this.getPasswordStrengthUserInput()
|
||||||
|
);
|
||||||
|
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPasswordStrengthUserInput() {
|
||||||
|
let userInput: string[] = [];
|
||||||
|
const atPosition = this.email.indexOf("@");
|
||||||
|
if (atPosition > -1) {
|
||||||
|
userInput = userInput.concat(
|
||||||
|
this.email
|
||||||
|
.substr(0, atPosition)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^A-Za-z0-9]/)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.name != null && this.name !== "") {
|
||||||
|
userInput = userInput.concat(this.name.trim().toLowerCase().split(" "));
|
||||||
|
}
|
||||||
|
return userInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
libs/angular/src/components/remove-password.component.ts
Normal file
82
libs/angular/src/components/remove-password.component.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class RemovePasswordComponent implements OnInit {
|
||||||
|
actionPromise: Promise<any>;
|
||||||
|
continuing = false;
|
||||||
|
leaving = false;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
organization: Organization;
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private stateService: StateService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private keyConnectorService: KeyConnectorService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.organization = await this.keyConnectorService.getManagingOrganization();
|
||||||
|
this.email = await this.stateService.getEmail();
|
||||||
|
await this.syncService.fullSync(false);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert() {
|
||||||
|
this.continuing = true;
|
||||||
|
this.actionPromise = this.keyConnectorService.migrateUser();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("removedMasterPassword")
|
||||||
|
);
|
||||||
|
await this.keyConnectorService.removeConvertAccountRequired();
|
||||||
|
this.router.navigate([""]);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async leave() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("leaveOrganizationConfirmation"),
|
||||||
|
this.organization.name,
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.leaving = true;
|
||||||
|
this.actionPromise = this.apiService.postLeaveOrganization(this.organization.id).then(() => {
|
||||||
|
return this.syncService.fullSync(true);
|
||||||
|
});
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
|
||||||
|
await this.keyConnectorService.removeConvertAccountRequired();
|
||||||
|
this.router.navigate([""]);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
293
libs/angular/src/components/send/add-edit.component.ts
Normal file
293
libs/angular/src/components/send/add-edit.component.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { DatePipe } from "@angular/common";
|
||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { SendService } from "jslib-common/abstractions/send.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
import { SendType } from "jslib-common/enums/sendType";
|
||||||
|
import { EncArrayBuffer } from "jslib-common/models/domain/encArrayBuffer";
|
||||||
|
import { Send } from "jslib-common/models/domain/send";
|
||||||
|
import { SendFileView } from "jslib-common/models/view/sendFileView";
|
||||||
|
import { SendTextView } from "jslib-common/models/view/sendTextView";
|
||||||
|
import { SendView } from "jslib-common/models/view/sendView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class AddEditComponent implements OnInit {
|
||||||
|
@Input() sendId: string;
|
||||||
|
@Input() type: SendType;
|
||||||
|
|
||||||
|
@Output() onSavedSend = new EventEmitter<SendView>();
|
||||||
|
@Output() onDeletedSend = new EventEmitter<SendView>();
|
||||||
|
@Output() onCancelled = new EventEmitter<SendView>();
|
||||||
|
|
||||||
|
copyLink = false;
|
||||||
|
disableSend = false;
|
||||||
|
disableHideEmail = false;
|
||||||
|
send: SendView;
|
||||||
|
deletionDate: string;
|
||||||
|
expirationDate: string;
|
||||||
|
hasPassword: boolean;
|
||||||
|
password: string;
|
||||||
|
showPassword = false;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
deletePromise: Promise<any>;
|
||||||
|
sendType = SendType;
|
||||||
|
typeOptions: any[];
|
||||||
|
canAccessPremium = true;
|
||||||
|
emailVerified = true;
|
||||||
|
alertShown = false;
|
||||||
|
showOptions = false;
|
||||||
|
|
||||||
|
private sendLinkBaseUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected datePipe: DatePipe,
|
||||||
|
protected sendService: SendService,
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {
|
||||||
|
this.typeOptions = [
|
||||||
|
{ name: i18nService.t("sendTypeFile"), value: SendType.File },
|
||||||
|
{ name: i18nService.t("sendTypeText"), value: SendType.Text },
|
||||||
|
];
|
||||||
|
this.sendLinkBaseUrl = this.environmentService.getSendUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
get link(): string {
|
||||||
|
if (this.send.id != null && this.send.accessId != null) {
|
||||||
|
return this.sendLinkBaseUrl + this.send.accessId + "/" + this.send.urlB64Key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSafari() {
|
||||||
|
return this.platformUtilsService.isSafari();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDateTimeLocalSupported(): boolean {
|
||||||
|
return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari());
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
get editMode(): boolean {
|
||||||
|
return this.sendId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title(): string {
|
||||||
|
return this.i18nService.t(this.editMode ? "editSend" : "createSend");
|
||||||
|
}
|
||||||
|
|
||||||
|
setDates(event: { deletionDate: string; expirationDate: string }) {
|
||||||
|
this.deletionDate = event.deletionDate;
|
||||||
|
this.expirationDate = event.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend);
|
||||||
|
this.disableHideEmail = await this.policyService.policyAppliesToUser(
|
||||||
|
PolicyType.SendOptions,
|
||||||
|
(p) => p.data.disableHideEmail
|
||||||
|
);
|
||||||
|
|
||||||
|
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
this.emailVerified = await this.stateService.getEmailVerified();
|
||||||
|
if (!this.canAccessPremium || !this.emailVerified) {
|
||||||
|
this.type = SendType.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.send == null) {
|
||||||
|
if (this.editMode) {
|
||||||
|
const send = await this.loadSend();
|
||||||
|
this.send = await send.decrypt();
|
||||||
|
} else {
|
||||||
|
this.send = new SendView();
|
||||||
|
this.send.type = this.type == null ? SendType.File : this.type;
|
||||||
|
this.send.file = new SendFileView();
|
||||||
|
this.send.text = new SendTextView();
|
||||||
|
this.send.deletionDate = new Date();
|
||||||
|
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(): Promise<boolean> {
|
||||||
|
if (this.disableSend) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("sendDisabledWarning")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.send.name == null || this.send.name === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("nameRequired")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file: File = null;
|
||||||
|
if (this.send.type === SendType.File && !this.editMode) {
|
||||||
|
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||||
|
const files = fileEl.files;
|
||||||
|
if (files == null || files.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("selectFile")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file = files[0];
|
||||||
|
if (files[0].size > 524288000) {
|
||||||
|
// 500 MB
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("maxFileSize")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.password != null && this.password.trim() === "") {
|
||||||
|
this.password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formPromise = this.encryptSend(file).then(async (encSend) => {
|
||||||
|
const uploadPromise = this.sendService.saveWithServer(encSend);
|
||||||
|
await uploadPromise;
|
||||||
|
if (this.send.id == null) {
|
||||||
|
this.send.id = encSend[0].id;
|
||||||
|
}
|
||||||
|
if (this.send.accessId == null) {
|
||||||
|
this.send.accessId = encSend[0].accessId;
|
||||||
|
}
|
||||||
|
this.onSavedSend.emit(this.send);
|
||||||
|
if (this.copyLink && this.link != null) {
|
||||||
|
const copySuccess = await this.copyLinkToClipboard(this.link);
|
||||||
|
if (copySuccess ?? true) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.editMode ? "editedSend" : "createdSend")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t(this.editMode ? "editedSend" : "createdSend"),
|
||||||
|
null,
|
||||||
|
this.i18nService.t("ok"),
|
||||||
|
null,
|
||||||
|
"success",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
await this.copyLinkToClipboard(this.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await this.formPromise;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyLinkToClipboard(link: string): Promise<void | boolean> {
|
||||||
|
return Promise.resolve(this.platformUtilsService.copyToClipboard(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<boolean> {
|
||||||
|
if (this.deletePromise != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("deleteSendConfirmation"),
|
||||||
|
this.i18nService.t("deleteSend"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePromise = this.sendService.deleteWithServer(this.send.id);
|
||||||
|
await this.deletePromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedSend"));
|
||||||
|
await this.load();
|
||||||
|
this.onDeletedSend.emit(this.send);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeChanged() {
|
||||||
|
if (this.send.type === SendType.File && !this.alertShown) {
|
||||||
|
if (!this.canAccessPremium) {
|
||||||
|
this.alertShown = true;
|
||||||
|
this.messagingService.send("premiumRequired");
|
||||||
|
} else if (!this.emailVerified) {
|
||||||
|
this.alertShown = true;
|
||||||
|
this.messagingService.send("emailVerificationRequired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadSend(): Promise<Send> {
|
||||||
|
return this.sendService.get(this.sendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> {
|
||||||
|
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
|
||||||
|
|
||||||
|
// Parse dates
|
||||||
|
try {
|
||||||
|
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
|
||||||
|
} catch {
|
||||||
|
sendData[0].deletionDate = null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sendData[0].expirationDate =
|
||||||
|
this.expirationDate == null ? null : new Date(this.expirationDate);
|
||||||
|
} catch {
|
||||||
|
sendData[0].expirationDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendData;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected togglePasswordVisible() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById("password").focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
356
libs/angular/src/components/send/efflux-dates.component.ts
Normal file
356
libs/angular/src/components/send/efflux-dates.component.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import { DatePipe } from "@angular/common";
|
||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
// Different BrowserPath = different controls.
|
||||||
|
enum BrowserPath {
|
||||||
|
// Native datetime-locale.
|
||||||
|
// We are happy.
|
||||||
|
Default = "default",
|
||||||
|
|
||||||
|
// Native date and time inputs, but no datetime-locale.
|
||||||
|
// We use individual date and time inputs and create a datetime programatically on submit.
|
||||||
|
Firefox = "firefox",
|
||||||
|
|
||||||
|
// No native date, time, or datetime-locale inputs.
|
||||||
|
// We use a polyfill for dates and a dropdown for times.
|
||||||
|
Safari = "safari",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DateField {
|
||||||
|
DeletionDate = "deletion",
|
||||||
|
ExpriationDate = "expiration",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value = hours
|
||||||
|
enum DatePreset {
|
||||||
|
OneHour = 1,
|
||||||
|
OneDay = 24,
|
||||||
|
TwoDays = 48,
|
||||||
|
ThreeDays = 72,
|
||||||
|
SevenDays = 168,
|
||||||
|
ThirtyDays = 720,
|
||||||
|
Custom = 0,
|
||||||
|
Never = null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeOption is used for the dropdown implementation of custom times
|
||||||
|
// twelveHour = displayed time; twentyFourHour = time used in logic
|
||||||
|
interface TimeOption {
|
||||||
|
twelveHour: string;
|
||||||
|
twentyFourHour: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class EffluxDatesComponent implements OnInit {
|
||||||
|
@Input() readonly initialDeletionDate: Date;
|
||||||
|
@Input() readonly initialExpirationDate: Date;
|
||||||
|
@Input() readonly editMode: boolean;
|
||||||
|
@Input() readonly disabled: boolean;
|
||||||
|
|
||||||
|
@Output() datesChanged = new EventEmitter<{ deletionDate: string; expirationDate: string }>();
|
||||||
|
|
||||||
|
get browserPath(): BrowserPath {
|
||||||
|
if (this.platformUtilsService.isFirefox()) {
|
||||||
|
return BrowserPath.Firefox;
|
||||||
|
} else if (this.platformUtilsService.isSafari()) {
|
||||||
|
return BrowserPath.Safari;
|
||||||
|
}
|
||||||
|
return BrowserPath.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
datesForm = new FormGroup({
|
||||||
|
selectedDeletionDatePreset: new FormControl(),
|
||||||
|
selectedExpirationDatePreset: new FormControl(),
|
||||||
|
defaultDeletionDateTime: new FormControl(),
|
||||||
|
defaultExpirationDateTime: new FormControl(),
|
||||||
|
fallbackDeletionDate: new FormControl(),
|
||||||
|
fallbackDeletionTime: new FormControl(),
|
||||||
|
fallbackExpirationDate: new FormControl(),
|
||||||
|
fallbackExpirationTime: new FormControl(),
|
||||||
|
});
|
||||||
|
|
||||||
|
deletionDatePresets: any[] = [
|
||||||
|
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
|
||||||
|
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
|
||||||
|
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
|
||||||
|
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
|
||||||
|
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
|
||||||
|
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
|
||||||
|
{ name: this.i18nService.t("custom"), value: DatePreset.Custom },
|
||||||
|
];
|
||||||
|
|
||||||
|
expirationDatePresets: any[] = [
|
||||||
|
{ name: this.i18nService.t("never"), value: DatePreset.Never },
|
||||||
|
].concat([...this.deletionDatePresets]);
|
||||||
|
|
||||||
|
get selectedDeletionDatePreset(): FormControl {
|
||||||
|
return this.datesForm.get("selectedDeletionDatePreset") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedExpirationDatePreset(): FormControl {
|
||||||
|
return this.datesForm.get("selectedExpirationDatePreset") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultDeletionDateTime(): FormControl {
|
||||||
|
return this.datesForm.get("defaultDeletionDateTime") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultExpirationDateTime(): FormControl {
|
||||||
|
return this.datesForm.get("defaultExpirationDateTime") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fallbackDeletionDate(): FormControl {
|
||||||
|
return this.datesForm.get("fallbackDeletionDate") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fallbackDeletionTime(): FormControl {
|
||||||
|
return this.datesForm.get("fallbackDeletionTime") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fallbackExpirationDate(): FormControl {
|
||||||
|
return this.datesForm.get("fallbackExpirationDate") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fallbackExpirationTime(): FormControl {
|
||||||
|
return this.datesForm.get("fallbackExpirationTime") as FormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be able to call these at any time and compute a submitable value
|
||||||
|
get formattedDeletionDate(): string {
|
||||||
|
switch (this.selectedDeletionDatePreset.value as DatePreset) {
|
||||||
|
case DatePreset.Never:
|
||||||
|
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
|
||||||
|
return this.formattedDeletionDate;
|
||||||
|
case DatePreset.Custom:
|
||||||
|
switch (this.browserPath) {
|
||||||
|
case BrowserPath.Safari:
|
||||||
|
case BrowserPath.Firefox:
|
||||||
|
return this.fallbackDeletionDate.value + "T" + this.fallbackDeletionTime.value;
|
||||||
|
default:
|
||||||
|
return this.defaultDeletionDateTime.value;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const now = new Date();
|
||||||
|
const miliseconds = now.setTime(
|
||||||
|
now.getTime() + (this.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
return new Date(miliseconds).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedExpirationDate(): string {
|
||||||
|
switch (this.selectedExpirationDatePreset.value as DatePreset) {
|
||||||
|
case DatePreset.Never:
|
||||||
|
return null;
|
||||||
|
case DatePreset.Custom:
|
||||||
|
switch (this.browserPath) {
|
||||||
|
case BrowserPath.Safari:
|
||||||
|
case BrowserPath.Firefox:
|
||||||
|
if (
|
||||||
|
(!this.fallbackExpirationDate.value || !this.fallbackExpirationTime.value) &&
|
||||||
|
this.editMode
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.fallbackExpirationDate.value + "T" + this.fallbackExpirationTime.value;
|
||||||
|
default:
|
||||||
|
if (!this.defaultExpirationDateTime.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.defaultExpirationDateTime.value;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const now = new Date();
|
||||||
|
const miliseconds = now.setTime(
|
||||||
|
now.getTime() + (this.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
return new Date(miliseconds).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
get safariDeletionTimePresetOptions() {
|
||||||
|
return this.safariTimePresetOptions(DateField.DeletionDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
get safariExpirationTimePresetOptions() {
|
||||||
|
return this.safariTimePresetOptions(DateField.ExpriationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get nextWeek(): Date {
|
||||||
|
const nextWeek = new Date();
|
||||||
|
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||||
|
return nextWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected datePipe: DatePipe
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setInitialFormValues();
|
||||||
|
this.emitDates();
|
||||||
|
this.datesForm.valueChanges.subscribe(() => {
|
||||||
|
this.emitDates();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeletionDatePresetSelect(value: DatePreset) {
|
||||||
|
this.selectedDeletionDatePreset.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExpiration() {
|
||||||
|
switch (this.browserPath) {
|
||||||
|
case BrowserPath.Safari:
|
||||||
|
case BrowserPath.Firefox:
|
||||||
|
this.fallbackExpirationDate.setValue(null);
|
||||||
|
this.fallbackExpirationTime.setValue(null);
|
||||||
|
break;
|
||||||
|
case BrowserPath.Default:
|
||||||
|
this.defaultExpirationDateTime.setValue(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitDates() {
|
||||||
|
this.datesChanged.emit({
|
||||||
|
deletionDate: this.formattedDeletionDate,
|
||||||
|
expirationDate: this.formattedExpirationDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setInitialFormValues() {
|
||||||
|
if (this.editMode) {
|
||||||
|
this.selectedDeletionDatePreset.setValue(DatePreset.Custom);
|
||||||
|
this.selectedExpirationDatePreset.setValue(DatePreset.Custom);
|
||||||
|
switch (this.browserPath) {
|
||||||
|
case BrowserPath.Safari:
|
||||||
|
case BrowserPath.Firefox:
|
||||||
|
this.fallbackDeletionDate.setValue(this.initialDeletionDate.toISOString().slice(0, 10));
|
||||||
|
this.fallbackDeletionTime.setValue(this.initialDeletionDate.toTimeString().slice(0, 5));
|
||||||
|
if (this.initialExpirationDate != null) {
|
||||||
|
this.fallbackExpirationDate.setValue(
|
||||||
|
this.initialExpirationDate.toISOString().slice(0, 10)
|
||||||
|
);
|
||||||
|
this.fallbackExpirationTime.setValue(
|
||||||
|
this.initialExpirationDate.toTimeString().slice(0, 5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BrowserPath.Default:
|
||||||
|
if (this.initialExpirationDate) {
|
||||||
|
this.defaultExpirationDateTime.setValue(
|
||||||
|
this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.defaultDeletionDateTime.setValue(
|
||||||
|
this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm")
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
|
||||||
|
this.selectedExpirationDatePreset.setValue(DatePreset.Never);
|
||||||
|
|
||||||
|
switch (this.browserPath) {
|
||||||
|
case BrowserPath.Safari:
|
||||||
|
this.fallbackDeletionDate.setValue(this.nextWeek.toISOString().slice(0, 10));
|
||||||
|
this.fallbackDeletionTime.setValue(
|
||||||
|
this.safariTimePresetOptions(DateField.DeletionDate)[1].twentyFourHour
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected safariTimePresetOptions(field: DateField): TimeOption[] {
|
||||||
|
// init individual arrays for major sort groups
|
||||||
|
const noon: TimeOption[] = [];
|
||||||
|
const midnight: TimeOption[] = [];
|
||||||
|
const ams: TimeOption[] = [];
|
||||||
|
const pms: TimeOption[] = [];
|
||||||
|
|
||||||
|
// determine minute skip (5 min, 10 min, 15 min, etc.)
|
||||||
|
const minuteIncrementer = 15;
|
||||||
|
|
||||||
|
// loop through each hour on a 12 hour system
|
||||||
|
for (let h = 1; h <= 12; h++) {
|
||||||
|
// loop through each minute in the hour using the skip to incriment
|
||||||
|
for (let m = 0; m < 60; m += minuteIncrementer) {
|
||||||
|
// init the final strings that will be added to the lists
|
||||||
|
let hour = h.toString();
|
||||||
|
let minutes = m.toString();
|
||||||
|
|
||||||
|
// add prepending 0s to single digit hours/minutes
|
||||||
|
if (h < 10) {
|
||||||
|
hour = "0" + hour;
|
||||||
|
}
|
||||||
|
if (m < 10) {
|
||||||
|
minutes = "0" + minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build time strings and push to relevant sort groups
|
||||||
|
if (h === 12) {
|
||||||
|
const midnightOption: TimeOption = {
|
||||||
|
twelveHour: `${hour}:${minutes} AM`,
|
||||||
|
twentyFourHour: `00:${minutes}`,
|
||||||
|
};
|
||||||
|
midnight.push(midnightOption);
|
||||||
|
|
||||||
|
const noonOption: TimeOption = {
|
||||||
|
twelveHour: `${hour}:${minutes} PM`,
|
||||||
|
twentyFourHour: `${hour}:${minutes}`,
|
||||||
|
};
|
||||||
|
noon.push(noonOption);
|
||||||
|
} else {
|
||||||
|
const amOption: TimeOption = {
|
||||||
|
twelveHour: `${hour}:${minutes} AM`,
|
||||||
|
twentyFourHour: `${hour}:${minutes}`,
|
||||||
|
};
|
||||||
|
ams.push(amOption);
|
||||||
|
|
||||||
|
const pmOption: TimeOption = {
|
||||||
|
twelveHour: `${hour}:${minutes} PM`,
|
||||||
|
twentyFourHour: `${h + 12}:${minutes}`,
|
||||||
|
};
|
||||||
|
pms.push(pmOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bring all the arrays together in the right order
|
||||||
|
const validTimes = [...midnight, ...ams, ...noon, ...pms];
|
||||||
|
|
||||||
|
// determine if an unsupported value already exists on the send & add that to the top of the option list
|
||||||
|
// example: if the Send was created with a different client
|
||||||
|
if (field === DateField.ExpriationDate && this.initialExpirationDate != null && this.editMode) {
|
||||||
|
const previousValue: TimeOption = {
|
||||||
|
twelveHour: this.datePipe.transform(this.initialExpirationDate, "hh:mm a"),
|
||||||
|
twentyFourHour: this.datePipe.transform(this.initialExpirationDate, "HH:mm"),
|
||||||
|
};
|
||||||
|
return [previousValue, { twelveHour: null, twentyFourHour: null }, ...validTimes];
|
||||||
|
} else if (
|
||||||
|
field === DateField.DeletionDate &&
|
||||||
|
this.initialDeletionDate != null &&
|
||||||
|
this.editMode
|
||||||
|
) {
|
||||||
|
const previousValue: TimeOption = {
|
||||||
|
twelveHour: this.datePipe.transform(this.initialDeletionDate, "hh:mm a"),
|
||||||
|
twentyFourHour: this.datePipe.transform(this.initialDeletionDate, "HH:mm"),
|
||||||
|
};
|
||||||
|
return [previousValue, ...validTimes];
|
||||||
|
} else {
|
||||||
|
return [{ twelveHour: null, twentyFourHour: null }, ...validTimes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
libs/angular/src/components/send/send.component.ts
Normal file
210
libs/angular/src/components/send/send.component.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { Directive, NgZone, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
|
import { SendService } from "jslib-common/abstractions/send.service";
|
||||||
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
import { SendType } from "jslib-common/enums/sendType";
|
||||||
|
import { SendView } from "jslib-common/models/view/sendView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class SendComponent implements OnInit {
|
||||||
|
disableSend = false;
|
||||||
|
sendType = SendType;
|
||||||
|
loaded = false;
|
||||||
|
loading = true;
|
||||||
|
refreshing = false;
|
||||||
|
expired = false;
|
||||||
|
type: SendType = null;
|
||||||
|
sends: SendView[] = [];
|
||||||
|
filteredSends: SendView[] = [];
|
||||||
|
searchText: string;
|
||||||
|
selectedType: SendType;
|
||||||
|
selectedAll: boolean;
|
||||||
|
searchPlaceholder: string;
|
||||||
|
filter: (cipher: SendView) => boolean;
|
||||||
|
searchPending = false;
|
||||||
|
hasSearched = false; // search() function called - returns true if text qualifies for search
|
||||||
|
|
||||||
|
actionPromise: any;
|
||||||
|
onSuccessfulRemovePassword: () => Promise<any>;
|
||||||
|
onSuccessfulDelete: () => Promise<any>;
|
||||||
|
onSuccessfulLoad: () => Promise<any>;
|
||||||
|
|
||||||
|
private searchTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected sendService: SendService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected ngZone: NgZone,
|
||||||
|
protected searchService: SearchService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(filter: (send: SendView) => boolean = null) {
|
||||||
|
this.loading = true;
|
||||||
|
const sends = await this.sendService.getAllDecrypted();
|
||||||
|
this.sends = sends;
|
||||||
|
if (this.onSuccessfulLoad != null) {
|
||||||
|
await this.onSuccessfulLoad();
|
||||||
|
} else {
|
||||||
|
// Default action
|
||||||
|
this.selectAll();
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(filter: (send: SendView) => boolean = null) {
|
||||||
|
this.loaded = false;
|
||||||
|
this.sends = [];
|
||||||
|
await this.load(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
try {
|
||||||
|
this.refreshing = true;
|
||||||
|
await this.reload(this.filter);
|
||||||
|
} finally {
|
||||||
|
this.refreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyFilter(filter: (send: SendView) => boolean = null) {
|
||||||
|
this.filter = filter;
|
||||||
|
await this.search(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(timeout: number = null) {
|
||||||
|
this.searchPending = false;
|
||||||
|
if (this.searchTimeout != null) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
if (timeout == null) {
|
||||||
|
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
||||||
|
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||||
|
this.applyTextSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.searchPending = true;
|
||||||
|
this.searchTimeout = setTimeout(async () => {
|
||||||
|
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
||||||
|
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||||
|
this.applyTextSearch();
|
||||||
|
this.searchPending = false;
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePassword(s: SendView): Promise<boolean> {
|
||||||
|
if (this.actionPromise != null || s.password == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("removePasswordConfirmation"),
|
||||||
|
this.i18nService.t("removePassword"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.actionPromise = this.sendService.removePasswordWithServer(s.id);
|
||||||
|
await this.actionPromise;
|
||||||
|
if (this.onSuccessfulRemovePassword != null) {
|
||||||
|
this.onSuccessfulRemovePassword();
|
||||||
|
} else {
|
||||||
|
// Default actions
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("removedPassword"));
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(s: SendView): Promise<boolean> {
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("deleteSendConfirmation"),
|
||||||
|
this.i18nService.t("deleteSend"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.actionPromise = this.sendService.deleteWithServer(s.id);
|
||||||
|
await this.actionPromise;
|
||||||
|
|
||||||
|
if (this.onSuccessfulDelete != null) {
|
||||||
|
this.onSuccessfulDelete();
|
||||||
|
} else {
|
||||||
|
// Default actions
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedSend"));
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(s: SendView) {
|
||||||
|
const sendLinkBaseUrl = this.environmentService.getSendUrl();
|
||||||
|
const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key;
|
||||||
|
this.platformUtilsService.copyToClipboard(link);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("valueCopied", this.i18nService.t("sendLink"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTextChanged() {
|
||||||
|
this.search(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
this.clearSelections();
|
||||||
|
this.selectedAll = true;
|
||||||
|
this.applyFilter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectType(type: SendType) {
|
||||||
|
this.clearSelections();
|
||||||
|
this.selectedType = type;
|
||||||
|
this.applyFilter((s) => s.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelections() {
|
||||||
|
this.selectedAll = false;
|
||||||
|
this.selectedType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyTextSearch() {
|
||||||
|
if (this.searchText != null) {
|
||||||
|
this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
libs/angular/src/components/set-password.component.ts
Normal file
180
libs/angular/src/components/set-password.component.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { Directive } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
import { HashPurpose } from "jslib-common/enums/hashPurpose";
|
||||||
|
import { DEFAULT_KDF_ITERATIONS, DEFAULT_KDF_TYPE } from "jslib-common/enums/kdfType";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { EncString } from "jslib-common/models/domain/encString";
|
||||||
|
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||||
|
import { KeysRequest } from "jslib-common/models/request/keysRequest";
|
||||||
|
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
||||||
|
import { SetPasswordRequest } from "jslib-common/models/request/setPasswordRequest";
|
||||||
|
|
||||||
|
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||||
|
syncLoading = true;
|
||||||
|
showPassword = false;
|
||||||
|
hint = "";
|
||||||
|
identifier: string = null;
|
||||||
|
orgId: string;
|
||||||
|
resetPasswordAutoEnroll = false;
|
||||||
|
|
||||||
|
onSuccessfulChangePassword: () => Promise<any>;
|
||||||
|
successRoute = "vault";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
i18nService: I18nService,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
passwordGenerationService: PasswordGenerationService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
policyService: PolicyService,
|
||||||
|
protected router: Router,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
stateService: StateService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
i18nService,
|
||||||
|
cryptoService,
|
||||||
|
messagingService,
|
||||||
|
passwordGenerationService,
|
||||||
|
platformUtilsService,
|
||||||
|
policyService,
|
||||||
|
stateService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
this.syncLoading = false;
|
||||||
|
|
||||||
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
|
if (qParams.identifier != null) {
|
||||||
|
this.identifier = qParams.identifier;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatic Enrollment Detection
|
||||||
|
if (this.identifier != null) {
|
||||||
|
try {
|
||||||
|
const response = await this.apiService.getOrganizationAutoEnrollStatus(this.identifier);
|
||||||
|
this.orgId = response.id;
|
||||||
|
this.resetPasswordAutoEnroll = response.resetPasswordEnabled;
|
||||||
|
this.enforcedPolicyOptions =
|
||||||
|
await this.policyService.getMasterPasswordPoliciesForInvitedUsers(this.orgId);
|
||||||
|
} catch {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupSubmitActions() {
|
||||||
|
this.kdf = DEFAULT_KDF_TYPE;
|
||||||
|
this.kdfIterations = DEFAULT_KDF_ITERATIONS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSubmitActions(
|
||||||
|
masterPasswordHash: string,
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
encKey: [SymmetricCryptoKey, EncString]
|
||||||
|
) {
|
||||||
|
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||||
|
const request = new SetPasswordRequest(
|
||||||
|
masterPasswordHash,
|
||||||
|
encKey[1].encryptedString,
|
||||||
|
this.hint,
|
||||||
|
this.kdf,
|
||||||
|
this.kdfIterations,
|
||||||
|
this.identifier,
|
||||||
|
new KeysRequest(keys[0], keys[1].encryptedString)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
if (this.resetPasswordAutoEnroll) {
|
||||||
|
this.formPromise = this.apiService
|
||||||
|
.setPassword(request)
|
||||||
|
.then(async () => {
|
||||||
|
await this.onSetPasswordSuccess(key, encKey, keys);
|
||||||
|
return this.apiService.getOrganizationKeys(this.orgId);
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response == null) {
|
||||||
|
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||||
|
}
|
||||||
|
const userId = await this.stateService.getUserId();
|
||||||
|
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||||
|
|
||||||
|
// RSA Encrypt user's encKey.key with organization public key
|
||||||
|
const userEncKey = await this.cryptoService.getEncKey();
|
||||||
|
const encryptedKey = await this.cryptoService.rsaEncrypt(
|
||||||
|
userEncKey.key,
|
||||||
|
publicKey.buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
|
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||||
|
|
||||||
|
return this.apiService.putOrganizationUserResetPasswordEnrollment(
|
||||||
|
this.orgId,
|
||||||
|
userId,
|
||||||
|
resetRequest
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formPromise = this.apiService.setPassword(request).then(async () => {
|
||||||
|
await this.onSetPasswordSuccess(key, encKey, keys);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.formPromise;
|
||||||
|
|
||||||
|
if (this.onSuccessfulChangePassword != null) {
|
||||||
|
this.onSuccessfulChangePassword();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword(confirmField: boolean) {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onSetPasswordSuccess(
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
encKey: [SymmetricCryptoKey, EncString],
|
||||||
|
keys: [string, EncString]
|
||||||
|
) {
|
||||||
|
await this.stateService.setKdfType(this.kdf);
|
||||||
|
await this.stateService.setKdfIterations(this.kdfIterations);
|
||||||
|
await this.cryptoService.setKey(key);
|
||||||
|
await this.cryptoService.setEncKey(encKey[1].encryptedString);
|
||||||
|
await this.cryptoService.setEncPrivateKey(keys[1].encryptedString);
|
||||||
|
|
||||||
|
const localKeyHash = await this.cryptoService.hashPassword(
|
||||||
|
this.masterPassword,
|
||||||
|
key,
|
||||||
|
HashPurpose.LocalAuthorization
|
||||||
|
);
|
||||||
|
await this.cryptoService.setKeyHash(localKeyHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
libs/angular/src/components/set-pin.component.ts
Normal file
54
libs/angular/src/components/set-pin.component.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Directive, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
import { ModalRef } from "./modal/modal.ref";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class SetPinComponent implements OnInit {
|
||||||
|
pin = "";
|
||||||
|
showPin = false;
|
||||||
|
masterPassOnRestart = true;
|
||||||
|
showMasterPassOnRestart = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private modalRef: ModalRef,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private keyConnectorService: KeyConnectorService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.showMasterPassOnRestart = this.masterPassOnRestart =
|
||||||
|
!(await this.keyConnectorService.getUsesKeyConnector());
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVisibility() {
|
||||||
|
this.showPin = !this.showPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (Utils.isNullOrWhitespace(this.pin)) {
|
||||||
|
this.modalRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kdf = await this.stateService.getKdfType();
|
||||||
|
const kdfIterations = await this.stateService.getKdfIterations();
|
||||||
|
const email = await this.stateService.getEmail();
|
||||||
|
const pinKey = await this.cryptoService.makePinKey(this.pin, email, kdf, kdfIterations);
|
||||||
|
const key = await this.cryptoService.getKey();
|
||||||
|
const pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey);
|
||||||
|
if (this.masterPassOnRestart) {
|
||||||
|
const encPin = await this.cryptoService.encrypt(this.pin);
|
||||||
|
await this.stateService.setProtectedPin(encPin.encryptedString);
|
||||||
|
await this.stateService.setDecryptedPinProtected(pinProtectedKey);
|
||||||
|
} else {
|
||||||
|
await this.stateService.setEncryptedPinProtected(pinProtectedKey.encryptedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modalRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Directive, Input, OnInit } from "@angular/core";
|
||||||
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
ControlValueAccessor,
|
||||||
|
FormBuilder,
|
||||||
|
ValidationErrors,
|
||||||
|
Validator,
|
||||||
|
} from "@angular/forms";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
import { Policy } from "jslib-common/models/domain/policy";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class VaultTimeoutInputComponent implements ControlValueAccessor, Validator, OnInit {
|
||||||
|
get showCustom() {
|
||||||
|
return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
get exceedsMinimumTimout(): boolean {
|
||||||
|
return (
|
||||||
|
!this.showCustom || this.customTimeInMinutes() > VaultTimeoutInputComponent.MIN_CUSTOM_MINUTES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CUSTOM_VALUE = -100;
|
||||||
|
static MIN_CUSTOM_MINUTES = 0;
|
||||||
|
|
||||||
|
form = this.formBuilder.group({
|
||||||
|
vaultTimeout: [null],
|
||||||
|
custom: this.formBuilder.group({
|
||||||
|
hours: [null],
|
||||||
|
minutes: [null],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
@Input() vaultTimeouts: { name: string; value: number }[];
|
||||||
|
vaultTimeoutPolicy: Policy;
|
||||||
|
vaultTimeoutPolicyHours: number;
|
||||||
|
vaultTimeoutPolicyMinutes: number;
|
||||||
|
|
||||||
|
private onChange: (vaultTimeout: number) => void;
|
||||||
|
private validatorChange: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
private i18nService: I18nService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout)) {
|
||||||
|
const vaultTimeoutPolicy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout);
|
||||||
|
|
||||||
|
this.vaultTimeoutPolicy = vaultTimeoutPolicy[0];
|
||||||
|
this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60);
|
||||||
|
this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60;
|
||||||
|
|
||||||
|
this.vaultTimeouts = this.vaultTimeouts.filter(
|
||||||
|
(t) =>
|
||||||
|
t.value <= this.vaultTimeoutPolicy.data.minutes &&
|
||||||
|
(t.value > 0 || t.value === VaultTimeoutInputComponent.CUSTOM_VALUE) &&
|
||||||
|
t.value != null
|
||||||
|
);
|
||||||
|
this.validatorChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(async (value) => {
|
||||||
|
this.onChange(this.getVaultTimeout(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign the previous value to the custom fields
|
||||||
|
this.form.get("vaultTimeout").valueChanges.subscribe((value) => {
|
||||||
|
if (value !== VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = Math.max(this.form.value.vaultTimeout, 0);
|
||||||
|
this.form.patchValue({
|
||||||
|
custom: {
|
||||||
|
hours: Math.floor(current / 60),
|
||||||
|
minutes: current % 60,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.vaultTimeouts.push({
|
||||||
|
name: this.i18nService.t("custom"),
|
||||||
|
value: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getVaultTimeout(value: any) {
|
||||||
|
if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||||
|
return value.vaultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.custom.hours * 60 + value.custom.minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(value: number): void {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.vaultTimeouts.every((p) => p.value !== value)) {
|
||||||
|
this.form.setValue({
|
||||||
|
vaultTimeout: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||||
|
custom: {
|
||||||
|
hours: Math.floor(value / 60),
|
||||||
|
minutes: value % 60,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.patchValue({
|
||||||
|
vaultTimeout: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(onChange: any): void {
|
||||||
|
this.onChange = onChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(onTouched: any): void {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(control: AbstractControl): ValidationErrors {
|
||||||
|
if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) {
|
||||||
|
return { policyError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.exceedsMinimumTimout) {
|
||||||
|
return { minTimeoutError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnValidatorChange(fn: () => void): void {
|
||||||
|
this.validatorChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private customTimeInMinutes() {
|
||||||
|
return this.form.get("custom.hours")?.value * 60 + this.form.get("custom.minutes")?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
libs/angular/src/components/share.component.ts
Normal file
116
libs/angular/src/components/share.component.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class ShareComponent implements OnInit {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Input() organizationId: string;
|
||||||
|
@Output() onSharedCipher = new EventEmitter();
|
||||||
|
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
cipher: CipherView;
|
||||||
|
collections: CollectionView[] = [];
|
||||||
|
organizations: Organization[] = [];
|
||||||
|
|
||||||
|
protected writeableCollections: CollectionView[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected collectionService: CollectionService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected organizationService: OrganizationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const allCollections = await this.collectionService.getAllDecrypted();
|
||||||
|
this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly);
|
||||||
|
const orgs = await this.organizationService.getAll();
|
||||||
|
this.organizations = orgs
|
||||||
|
.sort(Utils.getSortFunction(this.i18nService, "name"))
|
||||||
|
.filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
|
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||||
|
this.cipher = await cipherDomain.decrypt();
|
||||||
|
if (this.organizationId == null && this.organizations.length > 0) {
|
||||||
|
this.organizationId = this.organizations[0].id;
|
||||||
|
}
|
||||||
|
this.filterCollections();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterCollections() {
|
||||||
|
this.writeableCollections.forEach((c) => ((c as any).checked = false));
|
||||||
|
if (this.organizationId == null || this.writeableCollections.length === 0) {
|
||||||
|
this.collections = [];
|
||||||
|
} else {
|
||||||
|
this.collections = this.writeableCollections.filter(
|
||||||
|
(c) => c.organizationId === this.organizationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(): Promise<boolean> {
|
||||||
|
const selectedCollectionIds = this.collections
|
||||||
|
.filter((c) => !!(c as any).checked)
|
||||||
|
.map((c) => c.id);
|
||||||
|
if (selectedCollectionIds.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("selectOneCollection")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||||
|
const cipherView = await cipherDomain.decrypt();
|
||||||
|
const orgName =
|
||||||
|
this.organizations.find((o) => o.id === this.organizationId)?.name ??
|
||||||
|
this.i18nService.t("organization");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.cipherService
|
||||||
|
.shareWithServer(cipherView, this.organizationId, selectedCollectionIds)
|
||||||
|
.then(async () => {
|
||||||
|
this.onSharedCipher.emit();
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("movedItemToOrg", cipherView.name, orgName)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await this.formPromise;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canSave() {
|
||||||
|
if (this.collections != null) {
|
||||||
|
for (let i = 0; i < this.collections.length; i++) {
|
||||||
|
if ((this.collections[i] as any).checked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
260
libs/angular/src/components/sso.component.ts
Normal file
260
libs/angular/src/components/sso.component.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { Directive } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||||
|
import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||||
|
import { SsoPreValidateResponse } from "jslib-common/models/response/ssoPreValidateResponse";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class SsoComponent {
|
||||||
|
identifier: string;
|
||||||
|
loggingIn = false;
|
||||||
|
|
||||||
|
formPromise: Promise<AuthResult>;
|
||||||
|
initiateSsoFormPromise: Promise<SsoPreValidateResponse>;
|
||||||
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginChangePasswordNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||||
|
|
||||||
|
protected twoFactorRoute = "2fa";
|
||||||
|
protected successRoute = "lock";
|
||||||
|
protected changePasswordRoute = "set-password";
|
||||||
|
protected forcePasswordResetRoute = "update-temp-password";
|
||||||
|
protected clientId: string;
|
||||||
|
protected redirectUri: string;
|
||||||
|
protected state: string;
|
||||||
|
protected codeChallenge: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected router: Router,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
|
protected logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
|
if (qParams.code != null && qParams.state != null) {
|
||||||
|
const codeVerifier = await this.stateService.getSsoCodeVerifier();
|
||||||
|
const state = await this.stateService.getSsoState();
|
||||||
|
await this.stateService.setSsoCodeVerifier(null);
|
||||||
|
await this.stateService.setSsoState(null);
|
||||||
|
if (
|
||||||
|
qParams.code != null &&
|
||||||
|
codeVerifier != null &&
|
||||||
|
state != null &&
|
||||||
|
this.checkState(state, qParams.state)
|
||||||
|
) {
|
||||||
|
await this.logIn(
|
||||||
|
qParams.code,
|
||||||
|
codeVerifier,
|
||||||
|
this.getOrgIdentifierFromState(qParams.state)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
qParams.clientId != null &&
|
||||||
|
qParams.redirectUri != null &&
|
||||||
|
qParams.state != null &&
|
||||||
|
qParams.codeChallenge != null
|
||||||
|
) {
|
||||||
|
this.redirectUri = qParams.redirectUri;
|
||||||
|
this.state = qParams.state;
|
||||||
|
this.codeChallenge = qParams.codeChallenge;
|
||||||
|
this.clientId = qParams.clientId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(returnUri?: string, includeUserIdentifier?: boolean) {
|
||||||
|
if (this.identifier == null || this.identifier === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("ssoValidationFailed"),
|
||||||
|
this.i18nService.t("ssoIdentifierRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initiateSsoFormPromise = this.apiService.preValidateSso(this.identifier);
|
||||||
|
const response = await this.initiateSsoFormPromise;
|
||||||
|
|
||||||
|
const authorizeUrl = await this.buildAuthorizeUrl(
|
||||||
|
returnUri,
|
||||||
|
includeUserIdentifier,
|
||||||
|
response.token
|
||||||
|
);
|
||||||
|
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async buildAuthorizeUrl(
|
||||||
|
returnUri?: string,
|
||||||
|
includeUserIdentifier?: boolean,
|
||||||
|
token?: string
|
||||||
|
): Promise<string> {
|
||||||
|
let codeChallenge = this.codeChallenge;
|
||||||
|
let state = this.state;
|
||||||
|
|
||||||
|
const passwordOptions: any = {
|
||||||
|
type: "password",
|
||||||
|
length: 64,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
numbers: true,
|
||||||
|
special: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (codeChallenge == null) {
|
||||||
|
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||||
|
codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||||
|
await this.stateService.setSsoCodeVerifier(codeVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == null) {
|
||||||
|
state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
if (returnUri) {
|
||||||
|
state += `_returnUri='${returnUri}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Organization Identifier to state
|
||||||
|
state += `_identifier=${this.identifier}`;
|
||||||
|
|
||||||
|
// Save state (regardless of new or existing)
|
||||||
|
await this.stateService.setSsoState(state);
|
||||||
|
|
||||||
|
let authorizeUrl =
|
||||||
|
this.environmentService.getIdentityUrl() +
|
||||||
|
"/connect/authorize?" +
|
||||||
|
"client_id=" +
|
||||||
|
this.clientId +
|
||||||
|
"&redirect_uri=" +
|
||||||
|
encodeURIComponent(this.redirectUri) +
|
||||||
|
"&" +
|
||||||
|
"response_type=code&scope=api offline_access&" +
|
||||||
|
"state=" +
|
||||||
|
state +
|
||||||
|
"&code_challenge=" +
|
||||||
|
codeChallenge +
|
||||||
|
"&" +
|
||||||
|
"code_challenge_method=S256&response_mode=query&" +
|
||||||
|
"domain_hint=" +
|
||||||
|
encodeURIComponent(this.identifier) +
|
||||||
|
"&ssoToken=" +
|
||||||
|
encodeURIComponent(token);
|
||||||
|
|
||||||
|
if (includeUserIdentifier) {
|
||||||
|
const userIdentifier = await this.apiService.getSsoUserIdentifier();
|
||||||
|
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
||||||
|
this.loggingIn = true;
|
||||||
|
try {
|
||||||
|
const credentials = new SsoLogInCredentials(
|
||||||
|
code,
|
||||||
|
codeVerifier,
|
||||||
|
this.redirectUri,
|
||||||
|
orgIdFromState
|
||||||
|
);
|
||||||
|
this.formPromise = this.authService.logIn(credentials);
|
||||||
|
const response = await this.formPromise;
|
||||||
|
if (response.requiresTwoFactor) {
|
||||||
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.twoFactorRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: orgIdFromState,
|
||||||
|
sso: "true",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (response.resetMasterPassword) {
|
||||||
|
if (this.onSuccessfulLoginChangePasswordNavigate != null) {
|
||||||
|
this.onSuccessfulLoginChangePasswordNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.changePasswordRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: orgIdFromState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (response.forcePasswordReset) {
|
||||||
|
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||||
|
this.onSuccessfulLoginForceResetNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.forcePasswordResetRoute]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
|
if (this.onSuccessfulLogin != null) {
|
||||||
|
this.onSuccessfulLogin();
|
||||||
|
}
|
||||||
|
if (this.onSuccessfulLoginNavigate != null) {
|
||||||
|
this.onSuccessfulLoginNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
|
||||||
|
// TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here
|
||||||
|
if (e.message === "Key Connector error") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("ssoKeyConnectorError")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loggingIn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrgIdentifierFromState(state: string): string {
|
||||||
|
if (state === null || state === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateSplit = state.split("_identifier=");
|
||||||
|
return stateSplit.length > 1 ? stateSplit[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkState(state: string, checkState: string): boolean {
|
||||||
|
if (state === null || state === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (checkState === null || checkState === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateSplit = state.split("_identifier=");
|
||||||
|
const checkStateSplit = checkState.split("_identifier=");
|
||||||
|
return stateSplit[0] === checkStateSplit[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
95
libs/angular/src/components/toastr.component.ts
Normal file
95
libs/angular/src/components/toastr.component.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
|
||||||
|
import {
|
||||||
|
DefaultNoComponentGlobalConfig,
|
||||||
|
GlobalConfig,
|
||||||
|
Toast as BaseToast,
|
||||||
|
ToastPackage,
|
||||||
|
ToastrService,
|
||||||
|
TOAST_CONFIG,
|
||||||
|
} from "ngx-toastr";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "[toast-component2]",
|
||||||
|
template: `
|
||||||
|
<button
|
||||||
|
*ngIf="options.closeButton"
|
||||||
|
(click)="remove()"
|
||||||
|
type="button"
|
||||||
|
class="toast-close-button"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<div class="icon">
|
||||||
|
<i></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
|
||||||
|
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="message && options.enableHtml"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-live="polite"
|
||||||
|
[class]="options.messageClass"
|
||||||
|
[innerHTML]="message"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
*ngIf="message && !options.enableHtml"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-live="polite"
|
||||||
|
[class]="options.messageClass"
|
||||||
|
[attr.aria-label]="message"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="options.progressBar">
|
||||||
|
<div class="toast-progress" [style.width]="width + '%'"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger("flyInOut", [
|
||||||
|
state("inactive", style({ opacity: 0 })),
|
||||||
|
state("active", style({ opacity: 1 })),
|
||||||
|
state("removed", style({ opacity: 0 })),
|
||||||
|
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
|
||||||
|
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
preserveWhitespaces: false,
|
||||||
|
})
|
||||||
|
export class BitwardenToast extends BaseToast {
|
||||||
|
constructor(protected toastrService: ToastrService, public toastPackage: ToastPackage) {
|
||||||
|
super(toastrService, toastPackage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BitwardenToastGlobalConfig: GlobalConfig = {
|
||||||
|
...DefaultNoComponentGlobalConfig,
|
||||||
|
toastComponent: BitwardenToast,
|
||||||
|
};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule],
|
||||||
|
declarations: [BitwardenToast],
|
||||||
|
exports: [BitwardenToast],
|
||||||
|
})
|
||||||
|
export class BitwardenToastModule {
|
||||||
|
static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> {
|
||||||
|
return {
|
||||||
|
ngModule: BitwardenToastModule,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: TOAST_CONFIG,
|
||||||
|
useValue: {
|
||||||
|
default: BitwardenToastGlobalConfig,
|
||||||
|
config: config,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
36
libs/angular/src/components/two-factor-options.component.ts
Normal file
36
libs/angular/src/components/two-factor-options.component.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
|
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class TwoFactorOptionsComponent implements OnInit {
|
||||||
|
@Output() onProviderSelected = new EventEmitter<TwoFactorProviderType>();
|
||||||
|
@Output() onRecoverSelected = new EventEmitter();
|
||||||
|
|
||||||
|
providers: any[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
|
protected router: Router,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected win: Window
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.providers = this.twoFactorService.getSupportedProviders(this.win);
|
||||||
|
}
|
||||||
|
|
||||||
|
choose(p: any) {
|
||||||
|
this.onProviderSelected.emit(p.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
recover() {
|
||||||
|
this.platformUtilsService.launchUri("https://bitwarden.com/help/lost-two-step-device/");
|
||||||
|
this.onRecoverSelected.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
284
libs/angular/src/components/two-factor.component.ts
Normal file
284
libs/angular/src/components/two-factor.component.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import * as DuoWebSDK from "duo_web_sdk";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
|
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||||
|
import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
|
||||||
|
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||||
|
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
|
||||||
|
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||||
|
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class TwoFactorComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||||
|
token = "";
|
||||||
|
remember = false;
|
||||||
|
webAuthnReady = false;
|
||||||
|
webAuthnNewTab = false;
|
||||||
|
providers = TwoFactorProviders;
|
||||||
|
providerType = TwoFactorProviderType;
|
||||||
|
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||||
|
webAuthnSupported = false;
|
||||||
|
webAuthn: WebAuthnIFrame = null;
|
||||||
|
title = "";
|
||||||
|
twoFactorEmail: string = null;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
emailPromise: Promise<any>;
|
||||||
|
identifier: string = null;
|
||||||
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
|
|
||||||
|
get webAuthnAllow(): string {
|
||||||
|
return `publickey-credentials-get ${this.environmentService.getWebVaultUrl()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loginRoute = "login";
|
||||||
|
protected successRoute = "vault";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected router: Router,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected win: Window,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
|
protected appIdService: AppIdService
|
||||||
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (!this.authing || this.twoFactorService.getProviders() == null) {
|
||||||
|
this.router.navigate([this.loginRoute]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.route.queryParams.pipe(first()).subscribe((qParams) => {
|
||||||
|
if (qParams.identifier != null) {
|
||||||
|
this.identifier = qParams.identifier;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.needsLock) {
|
||||||
|
this.successRoute = "lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.win != null && this.webAuthnSupported) {
|
||||||
|
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
this.webAuthn = new WebAuthnIFrame(
|
||||||
|
this.win,
|
||||||
|
webVaultUrl,
|
||||||
|
this.webAuthnNewTab,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.i18nService,
|
||||||
|
(token: string) => {
|
||||||
|
this.token = token;
|
||||||
|
this.submit();
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error);
|
||||||
|
},
|
||||||
|
(info: string) => {
|
||||||
|
if (info === "ready") {
|
||||||
|
this.webAuthnReady = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported);
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupWebAuthn();
|
||||||
|
this.webAuthn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.selectedProviderType == null) {
|
||||||
|
this.title = this.i18nService.t("loginUnavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupWebAuthn();
|
||||||
|
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||||
|
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
||||||
|
switch (this.selectedProviderType) {
|
||||||
|
case TwoFactorProviderType.WebAuthn:
|
||||||
|
if (!this.webAuthnNewTab) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.authWebAuthn();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TwoFactorProviderType.Duo:
|
||||||
|
case TwoFactorProviderType.OrganizationDuo:
|
||||||
|
setTimeout(() => {
|
||||||
|
DuoWebSDK.init({
|
||||||
|
iframe: undefined,
|
||||||
|
host: providerData.Host,
|
||||||
|
sig_request: providerData.Signature,
|
||||||
|
submit_callback: async (f: HTMLFormElement) => {
|
||||||
|
const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement;
|
||||||
|
if (sig != null) {
|
||||||
|
this.token = sig.value;
|
||||||
|
await this.submit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
break;
|
||||||
|
case TwoFactorProviderType.Email:
|
||||||
|
this.twoFactorEmail = providerData.Email;
|
||||||
|
if (this.twoFactorService.getProviders().size > 1) {
|
||||||
|
await this.sendEmail(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
await this.setupCaptcha();
|
||||||
|
|
||||||
|
if (this.token == null || this.token === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("verificationCodeRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn) {
|
||||||
|
if (this.webAuthn != null) {
|
||||||
|
this.webAuthn.stop();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
this.selectedProviderType === TwoFactorProviderType.Email ||
|
||||||
|
this.selectedProviderType === TwoFactorProviderType.Authenticator
|
||||||
|
) {
|
||||||
|
this.token = this.token.replace(" ", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.doSubmit();
|
||||||
|
} catch {
|
||||||
|
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) {
|
||||||
|
this.webAuthn.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doSubmit() {
|
||||||
|
this.formPromise = this.authService.logInTwoFactor(
|
||||||
|
new TokenRequestTwoFactor(this.selectedProviderType, this.token, this.remember),
|
||||||
|
this.captchaToken
|
||||||
|
);
|
||||||
|
const response: AuthResult = await this.formPromise;
|
||||||
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
|
if (this.handleCaptchaRequired(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.onSuccessfulLogin != null) {
|
||||||
|
this.onSuccessfulLogin();
|
||||||
|
}
|
||||||
|
if (response.resetMasterPassword) {
|
||||||
|
this.successRoute = "set-password";
|
||||||
|
}
|
||||||
|
if (response.forcePasswordReset) {
|
||||||
|
this.successRoute = "update-temp-password";
|
||||||
|
}
|
||||||
|
if (this.onSuccessfulLoginNavigate != null) {
|
||||||
|
this.onSuccessfulLoginNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.successRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: this.identifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmail(doToast: boolean) {
|
||||||
|
if (this.selectedProviderType !== TwoFactorProviderType.Email) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.emailPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new TwoFactorEmailRequest();
|
||||||
|
request.email = this.authService.email;
|
||||||
|
request.masterPasswordHash = this.authService.masterPasswordHash;
|
||||||
|
request.deviceIdentifier = await this.appIdService.getAppId();
|
||||||
|
this.emailPromise = this.apiService.postTwoFactorEmail(request);
|
||||||
|
await this.emailPromise;
|
||||||
|
if (doToast) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emailPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
authWebAuthn() {
|
||||||
|
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
||||||
|
|
||||||
|
if (!this.webAuthnSupported || this.webAuthn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webAuthn.init(providerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupWebAuthn() {
|
||||||
|
if (this.webAuthn != null) {
|
||||||
|
this.webAuthn.stop();
|
||||||
|
this.webAuthn.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get authing(): boolean {
|
||||||
|
return (
|
||||||
|
this.authService.authingWithPassword() ||
|
||||||
|
this.authService.authingWithSso() ||
|
||||||
|
this.authService.authingWithApiKey()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get needsLock(): boolean {
|
||||||
|
return this.authService.authingWithSso() || this.authService.authingWithApiKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
libs/angular/src/components/update-password.component.ts
Normal file
126
libs/angular/src/components/update-password.component.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { Directive } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
||||||
|
import { VerificationType } from "jslib-common/enums/verificationType";
|
||||||
|
import { EncString } from "jslib-common/models/domain/encString";
|
||||||
|
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
|
||||||
|
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||||
|
import { PasswordRequest } from "jslib-common/models/request/passwordRequest";
|
||||||
|
import { Verification } from "jslib-common/types/verification";
|
||||||
|
|
||||||
|
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||||
|
hint: string;
|
||||||
|
key: string;
|
||||||
|
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
|
showPassword = false;
|
||||||
|
currentMasterPassword: string;
|
||||||
|
|
||||||
|
onSuccessfulChangePassword: () => Promise<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
passwordGenerationService: PasswordGenerationService,
|
||||||
|
policyService: PolicyService,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
stateService: StateService,
|
||||||
|
private userVerificationService: UserVerificationService,
|
||||||
|
private logService: LogService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
i18nService,
|
||||||
|
cryptoService,
|
||||||
|
messagingService,
|
||||||
|
passwordGenerationService,
|
||||||
|
platformUtilsService,
|
||||||
|
policyService,
|
||||||
|
stateService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword(confirmField: boolean) {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
await this.stateService.setOrganizationInvitation(null);
|
||||||
|
this.router.navigate(["/vault"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupSubmitActions(): Promise<boolean> {
|
||||||
|
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("masterPassRequired")
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret: Verification = {
|
||||||
|
type: VerificationType.MasterPassword,
|
||||||
|
secret: this.currentMasterPassword,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await this.userVerificationService.verifyUser(secret);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.kdf = await this.stateService.getKdfType();
|
||||||
|
this.kdfIterations = await this.stateService.getKdfIterations();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSubmitActions(
|
||||||
|
masterPasswordHash: string,
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
encKey: [SymmetricCryptoKey, EncString]
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Create Request
|
||||||
|
const request = new PasswordRequest();
|
||||||
|
request.masterPasswordHash = await this.cryptoService.hashPassword(
|
||||||
|
this.currentMasterPassword,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
request.newMasterPasswordHash = masterPasswordHash;
|
||||||
|
request.key = encKey[1].encryptedString;
|
||||||
|
|
||||||
|
// Update user's password
|
||||||
|
this.apiService.postPassword(request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
this.i18nService.t("masterPasswordChanged"),
|
||||||
|
this.i18nService.t("logBackIn")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.onSuccessfulChangePassword != null) {
|
||||||
|
this.onSuccessfulChangePassword();
|
||||||
|
} else {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
libs/angular/src/components/update-temp-password.component.ts
Normal file
132
libs/angular/src/components/update-temp-password.component.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Directive } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
import { EncString } from "jslib-common/models/domain/encString";
|
||||||
|
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
|
||||||
|
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||||
|
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
|
||||||
|
|
||||||
|
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||||
|
hint: string;
|
||||||
|
key: string;
|
||||||
|
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
|
showPassword = false;
|
||||||
|
|
||||||
|
onSuccessfulChangePassword: () => Promise<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
passwordGenerationService: PasswordGenerationService,
|
||||||
|
policyService: PolicyService,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
stateService: StateService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private logService: LogService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
i18nService,
|
||||||
|
cryptoService,
|
||||||
|
messagingService,
|
||||||
|
passwordGenerationService,
|
||||||
|
platformUtilsService,
|
||||||
|
policyService,
|
||||||
|
stateService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePassword(confirmField: boolean) {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupSubmitActions(): Promise<boolean> {
|
||||||
|
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
||||||
|
this.email = await this.stateService.getEmail();
|
||||||
|
this.kdf = await this.stateService.getKdfType();
|
||||||
|
this.kdfIterations = await this.stateService.getKdfIterations();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
// Validation
|
||||||
|
if (!(await this.strongPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.setupSubmitActions())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create new key and hash new password
|
||||||
|
const newKey = await this.cryptoService.makeKey(
|
||||||
|
this.masterPassword,
|
||||||
|
this.email.trim().toLowerCase(),
|
||||||
|
this.kdf,
|
||||||
|
this.kdfIterations
|
||||||
|
);
|
||||||
|
const newPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
|
||||||
|
|
||||||
|
// Grab user's current enc key
|
||||||
|
const userEncKey = await this.cryptoService.getEncKey();
|
||||||
|
|
||||||
|
// Create new encKey for the User
|
||||||
|
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
|
||||||
|
|
||||||
|
await this.performSubmitActions(newPasswordHash, newKey, newEncKey);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSubmitActions(
|
||||||
|
masterPasswordHash: string,
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
encKey: [SymmetricCryptoKey, EncString]
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Create request
|
||||||
|
const request = new UpdateTempPasswordRequest();
|
||||||
|
request.key = encKey[1].encryptedString;
|
||||||
|
request.newMasterPasswordHash = masterPasswordHash;
|
||||||
|
request.masterPasswordHint = this.hint;
|
||||||
|
|
||||||
|
// Update user's password
|
||||||
|
this.formPromise = this.apiService.putUpdateTempPassword(request);
|
||||||
|
await this.formPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("updatedMasterPassword")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.onSuccessfulChangePassword != null) {
|
||||||
|
this.onSuccessfulChangePassword();
|
||||||
|
} else {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
libs/angular/src/components/user-verification.component.html
Normal file
46
libs/angular/src/components/user-verification.component.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<ng-container *ngIf="!usesKeyConnector">
|
||||||
|
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="masterPassword"
|
||||||
|
type="password"
|
||||||
|
name="MasterPasswordHash"
|
||||||
|
class="form-control"
|
||||||
|
[formControl]="secret"
|
||||||
|
required
|
||||||
|
appAutofocus
|
||||||
|
appInputVerbatim
|
||||||
|
/>
|
||||||
|
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="usesKeyConnector">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
(click)="requestOTP()"
|
||||||
|
[disabled]="disableRequestOTP"
|
||||||
|
>
|
||||||
|
{{ "sendCode" | i18n }}
|
||||||
|
</button>
|
||||||
|
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
|
||||||
|
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||||
|
{{ "codeSent" | i18n }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verificationCode">{{ "verificationCode" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="verificationCode"
|
||||||
|
type="input"
|
||||||
|
name="verificationCode"
|
||||||
|
class="form-control"
|
||||||
|
[formControl]="secret"
|
||||||
|
required
|
||||||
|
appAutofocus
|
||||||
|
appInputVerbatim
|
||||||
|
/>
|
||||||
|
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
96
libs/angular/src/components/user-verification.component.ts
Normal file
96
libs/angular/src/components/user-verification.component.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||||
|
|
||||||
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
||||||
|
import { VerificationType } from "jslib-common/enums/verificationType";
|
||||||
|
import { Verification } from "jslib-common/types/verification";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for general-purpose user verification throughout the app.
|
||||||
|
* Collects the user's master password, or if they are using Key Connector, prompts for an OTP via email.
|
||||||
|
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
|
||||||
|
* Use UserVerificationService to verify the user's input.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: "app-user-verification",
|
||||||
|
templateUrl: "user-verification.component.html",
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
multi: true,
|
||||||
|
useExisting: UserVerificationComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
animations: [
|
||||||
|
trigger("sent", [
|
||||||
|
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class UserVerificationComponent implements ControlValueAccessor, OnInit {
|
||||||
|
usesKeyConnector = false;
|
||||||
|
disableRequestOTP = false;
|
||||||
|
sentCode = false;
|
||||||
|
|
||||||
|
secret = new FormControl("");
|
||||||
|
|
||||||
|
private onChange: (value: Verification) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private keyConnectorService: KeyConnectorService,
|
||||||
|
private userVerificationService: UserVerificationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
|
||||||
|
this.processChanges(this.secret.value);
|
||||||
|
|
||||||
|
this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestOTP() {
|
||||||
|
if (this.usesKeyConnector) {
|
||||||
|
this.disableRequestOTP = true;
|
||||||
|
try {
|
||||||
|
await this.userVerificationService.requestOTP();
|
||||||
|
this.sentCode = true;
|
||||||
|
} finally {
|
||||||
|
this.disableRequestOTP = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(obj: any): void {
|
||||||
|
this.secret.setValue(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.onChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.disableRequestOTP = isDisabled;
|
||||||
|
if (isDisabled) {
|
||||||
|
this.secret.disable();
|
||||||
|
} else {
|
||||||
|
this.secret.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processChanges(secret: string) {
|
||||||
|
if (this.onChange == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onChange({
|
||||||
|
type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword,
|
||||||
|
secret: secret,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
39
libs/angular/src/components/view-custom-fields.component.ts
Normal file
39
libs/angular/src/components/view-custom-fields.component.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Directive, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { FieldType } from "jslib-common/enums/fieldType";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { FieldView } from "jslib-common/models/view/fieldView";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class ViewCustomFieldsComponent {
|
||||||
|
@Input() cipher: CipherView;
|
||||||
|
@Input() promptPassword: () => Promise<boolean>;
|
||||||
|
@Input() copy: (value: string, typeI18nKey: string, aType: string) => void;
|
||||||
|
|
||||||
|
fieldType = FieldType;
|
||||||
|
|
||||||
|
constructor(private eventService: EventService) {}
|
||||||
|
|
||||||
|
async toggleFieldValue(field: FieldView) {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const f = field as any;
|
||||||
|
f.showValue = !f.showValue;
|
||||||
|
f.showCount = false;
|
||||||
|
if (f.showValue) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipher.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleFieldCount(field: FieldView) {
|
||||||
|
if (!field.showValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.showCount = !field.showCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
454
libs/angular/src/components/view.component.ts
Normal file
454
libs/angular/src/components/view.component.ts
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Directive,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
NgZone,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
|
import { AuditService } from "jslib-common/abstractions/audit.service";
|
||||||
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
|
import { TotpService } from "jslib-common/abstractions/totp.service";
|
||||||
|
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
|
||||||
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { FieldType } from "jslib-common/enums/fieldType";
|
||||||
|
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||||
|
import { AttachmentView } from "jslib-common/models/view/attachmentView";
|
||||||
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
import { LoginUriView } from "jslib-common/models/view/loginUriView";
|
||||||
|
|
||||||
|
const BroadcasterSubscriptionId = "ViewComponent";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class ViewComponent implements OnDestroy, OnInit {
|
||||||
|
@Input() cipherId: string;
|
||||||
|
@Output() onEditCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onCloneCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onShareCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||||
|
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||||
|
|
||||||
|
cipher: CipherView;
|
||||||
|
showPassword: boolean;
|
||||||
|
showPasswordCount: boolean;
|
||||||
|
showCardNumber: boolean;
|
||||||
|
showCardCode: boolean;
|
||||||
|
canAccessPremium: boolean;
|
||||||
|
totpCode: string;
|
||||||
|
totpCodeFormatted: string;
|
||||||
|
totpDash: number;
|
||||||
|
totpSec: number;
|
||||||
|
totpLow: boolean;
|
||||||
|
fieldType = FieldType;
|
||||||
|
checkPasswordPromise: Promise<number>;
|
||||||
|
|
||||||
|
private totpInterval: any;
|
||||||
|
private previousCipherId: string;
|
||||||
|
private passwordReprompted = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
|
protected totpService: TotpService,
|
||||||
|
protected tokenService: TokenService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected auditService: AuditService,
|
||||||
|
protected win: Window,
|
||||||
|
protected broadcasterService: BroadcasterService,
|
||||||
|
protected ngZone: NgZone,
|
||||||
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
|
protected eventService: EventService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||||
|
this.ngZone.run(async () => {
|
||||||
|
switch (message.command) {
|
||||||
|
case "syncCompleted":
|
||||||
|
if (message.successfully) {
|
||||||
|
await this.load();
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
|
this.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.cleanUp();
|
||||||
|
|
||||||
|
const cipher = await this.cipherService.get(this.cipherId);
|
||||||
|
this.cipher = await cipher.decrypt();
|
||||||
|
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.cipher.type === CipherType.Login &&
|
||||||
|
this.cipher.login.totp &&
|
||||||
|
(cipher.organizationUseTotp || this.canAccessPremium)
|
||||||
|
) {
|
||||||
|
await this.totpUpdateCode();
|
||||||
|
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||||
|
await this.totpTick(interval);
|
||||||
|
|
||||||
|
this.totpInterval = setInterval(async () => {
|
||||||
|
await this.totpTick(interval);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.previousCipherId !== this.cipherId) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||||
|
}
|
||||||
|
this.previousCipherId = this.cipherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit() {
|
||||||
|
if (await this.promptPassword()) {
|
||||||
|
this.onEditCipher.emit(this.cipher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clone() {
|
||||||
|
if (await this.promptPassword()) {
|
||||||
|
this.onCloneCipher.emit(this.cipher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async share() {
|
||||||
|
if (await this.promptPassword()) {
|
||||||
|
this.onShareCipher.emit(this.cipher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<boolean> {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t(
|
||||||
|
this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation"
|
||||||
|
),
|
||||||
|
this.i18nService.t("deleteItem"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.deleteCipher();
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem")
|
||||||
|
);
|
||||||
|
this.onDeletedCipher.emit(this.cipher);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(): Promise<boolean> {
|
||||||
|
if (!this.cipher.isDeleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("restoreItemConfirmation"),
|
||||||
|
this.i18nService.t("restoreItem"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.restoreCipher();
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
||||||
|
this.onRestoredCipher.emit(this.cipher);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async togglePassword() {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
this.showPasswordCount = false;
|
||||||
|
if (this.showPassword) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async togglePasswordCount() {
|
||||||
|
if (!this.showPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showPasswordCount = !this.showPasswordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleCardNumber() {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showCardNumber = !this.showCardNumber;
|
||||||
|
if (this.showCardNumber) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleCardCode() {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showCardCode = !this.showCardCode;
|
||||||
|
if (this.showCardCode) {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPassword() {
|
||||||
|
if (
|
||||||
|
this.cipher.login == null ||
|
||||||
|
this.cipher.login.password == null ||
|
||||||
|
this.cipher.login.password === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
|
||||||
|
const matches = await this.checkPasswordPromise;
|
||||||
|
|
||||||
|
if (matches > 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"warning",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("passwordExposed", matches.toString())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(uri: LoginUriView, cipherId?: string) {
|
||||||
|
if (!uri.canLaunch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipherId) {
|
||||||
|
this.cipherService.updateLastLaunchedDate(cipherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.launchUri(uri.launchUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
async copy(value: string, typeI18nKey: string, aType: string) {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||||
|
!(await this.promptPassword())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||||
|
this.platformUtilsService.copyToClipboard(value, copyOptions);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeI18nKey === "password") {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
|
||||||
|
} else if (typeI18nKey === "securityCode") {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
|
||||||
|
} else if (aType === "H_Field") {
|
||||||
|
this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTextDataOnDrag(event: DragEvent, data: string) {
|
||||||
|
event.dataTransfer.setData("text", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadAttachment(attachment: AttachmentView) {
|
||||||
|
if (!(await this.promptPassword())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const a = attachment as any;
|
||||||
|
if (a.downloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cipher.organizationId == null && !this.canAccessPremium) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("premiumRequired"),
|
||||||
|
this.i18nService.t("premiumRequiredDesc")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
try {
|
||||||
|
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||||
|
this.cipher.id,
|
||||||
|
attachment.id
|
||||||
|
);
|
||||||
|
url = attachmentDownloadResponse.url;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||||
|
url = attachment.url;
|
||||||
|
} else if (e instanceof ErrorResponse) {
|
||||||
|
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.downloading = true;
|
||||||
|
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||||
|
if (response.status !== 200) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
a.downloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buf = await response.arrayBuffer();
|
||||||
|
const key =
|
||||||
|
attachment.key != null
|
||||||
|
? attachment.key
|
||||||
|
: await this.cryptoService.getOrgKey(this.cipher.organizationId);
|
||||||
|
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
|
||||||
|
this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName);
|
||||||
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
a.downloading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteCipher() {
|
||||||
|
return this.cipher.isDeleted
|
||||||
|
? this.cipherService.deleteWithServer(this.cipher.id)
|
||||||
|
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected restoreCipher() {
|
||||||
|
return this.cipherService.restoreWithServer(this.cipher.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async promptPassword() {
|
||||||
|
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanUp() {
|
||||||
|
this.totpCode = null;
|
||||||
|
this.cipher = null;
|
||||||
|
this.showPassword = false;
|
||||||
|
this.showCardNumber = false;
|
||||||
|
this.showCardCode = false;
|
||||||
|
this.passwordReprompted = false;
|
||||||
|
if (this.totpInterval) {
|
||||||
|
clearInterval(this.totpInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async totpUpdateCode() {
|
||||||
|
if (
|
||||||
|
this.cipher == null ||
|
||||||
|
this.cipher.type !== CipherType.Login ||
|
||||||
|
this.cipher.login.totp == null
|
||||||
|
) {
|
||||||
|
if (this.totpInterval) {
|
||||||
|
clearInterval(this.totpInterval);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
||||||
|
if (this.totpCode != null) {
|
||||||
|
if (this.totpCode.length > 4) {
|
||||||
|
const half = Math.floor(this.totpCode.length / 2);
|
||||||
|
this.totpCodeFormatted =
|
||||||
|
this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
|
||||||
|
} else {
|
||||||
|
this.totpCodeFormatted = this.totpCode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.totpCodeFormatted = null;
|
||||||
|
if (this.totpInterval) {
|
||||||
|
clearInterval(this.totpInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async totpTick(intervalSeconds: number) {
|
||||||
|
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||||
|
const mod = epoch % intervalSeconds;
|
||||||
|
|
||||||
|
this.totpSec = intervalSeconds - mod;
|
||||||
|
this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2");
|
||||||
|
this.totpLow = this.totpSec <= 7;
|
||||||
|
if (mod === 0) {
|
||||||
|
await this.totpUpdateCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
libs/angular/src/directives/a11y-invalid.directive.ts
Normal file
26
libs/angular/src/directives/a11y-invalid.directive.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Directive, ElementRef, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { NgControl } from "@angular/forms";
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appA11yInvalid]",
|
||||||
|
})
|
||||||
|
export class A11yInvalidDirective implements OnDestroy, OnInit {
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef<HTMLInputElement>, private formControlDirective: NgControl) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.sub = this.formControlDirective.control.statusChanges.subscribe((status) => {
|
||||||
|
if (status === "INVALID") {
|
||||||
|
this.el.nativeElement.setAttribute("aria-invalid", "true");
|
||||||
|
} else if (status === "VALID") {
|
||||||
|
this.el.nativeElement.setAttribute("aria-invalid", "false");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.sub?.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
libs/angular/src/directives/a11y-title.directive.ts
Normal file
23
libs/angular/src/directives/a11y-title.directive.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appA11yTitle]",
|
||||||
|
})
|
||||||
|
export class A11yTitleDirective {
|
||||||
|
@Input() set appA11yTitle(title: string) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private title: string;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef, private renderer: Renderer2) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.el.nativeElement.hasAttribute("title")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
|
||||||
|
}
|
||||||
|
if (!this.el.nativeElement.hasAttribute("aria-label")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
libs/angular/src/directives/api-action.directive.ts
Normal file
49
libs/angular/src/directives/api-action.directive.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
||||||
|
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||||
|
|
||||||
|
import { ValidationService } from "../services/validation.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides error handling, in particular for any error returned by the server in an api call.
|
||||||
|
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
|
||||||
|
* e.g. <form [appApiAction]="this.formPromise">
|
||||||
|
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: "[appApiAction]",
|
||||||
|
})
|
||||||
|
export class ApiActionDirective implements OnChanges {
|
||||||
|
@Input() appApiAction: Promise<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private el: ElementRef,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: any) {
|
||||||
|
if (this.appApiAction == null || this.appApiAction.then == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el.nativeElement.loading = true;
|
||||||
|
|
||||||
|
this.appApiAction.then(
|
||||||
|
(response: any) => {
|
||||||
|
this.el.nativeElement.loading = false;
|
||||||
|
},
|
||||||
|
(e: any) => {
|
||||||
|
this.el.nativeElement.loading = false;
|
||||||
|
|
||||||
|
if ((e as ErrorResponse).captchaRequired) {
|
||||||
|
this.logService.error("Captcha required error response: " + e.getSingleMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logService?.error(`Received API exception: ${e}`);
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
libs/angular/src/directives/autofocus.directive.ts
Normal file
27
libs/angular/src/directives/autofocus.directive.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
|
||||||
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appAutofocus]",
|
||||||
|
})
|
||||||
|
export class AutofocusDirective {
|
||||||
|
@Input() set appAutofocus(condition: boolean | string) {
|
||||||
|
this.autofocus = condition === "" || condition === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private autofocus: boolean;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef, private ngZone: NgZone) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!Utils.isMobileBrowser && this.autofocus) {
|
||||||
|
if (this.ngZone.isStable) {
|
||||||
|
this.el.nativeElement.focus();
|
||||||
|
} else {
|
||||||
|
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
libs/angular/src/directives/blur-click.directive.ts
Normal file
12
libs/angular/src/directives/blur-click.directive.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appBlurClick]",
|
||||||
|
})
|
||||||
|
export class BlurClickDirective {
|
||||||
|
constructor(private el: ElementRef) {}
|
||||||
|
|
||||||
|
@HostListener("click") onClick() {
|
||||||
|
this.el.nativeElement.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
libs/angular/src/directives/box-row.directive.ts
Normal file
59
libs/angular/src/directives/box-row.directive.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appBoxRow]",
|
||||||
|
})
|
||||||
|
export class BoxRowDirective implements OnInit {
|
||||||
|
el: HTMLElement = null;
|
||||||
|
formEls: Element[];
|
||||||
|
|
||||||
|
constructor(elRef: ElementRef) {
|
||||||
|
this.el = elRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.formEls = Array.from(
|
||||||
|
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')
|
||||||
|
);
|
||||||
|
this.formEls.forEach((formEl) => {
|
||||||
|
formEl.addEventListener(
|
||||||
|
"focus",
|
||||||
|
() => {
|
||||||
|
this.el.classList.add("active");
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
formEl.addEventListener(
|
||||||
|
"blur",
|
||||||
|
() => {
|
||||||
|
this.el.classList.remove("active");
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener("click", ["$event"]) onClick(event: Event) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
target !== this.el &&
|
||||||
|
!target.classList.contains("progress") &&
|
||||||
|
!target.classList.contains("progress-bar")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formEls.length > 0) {
|
||||||
|
const formEl = this.formEls[0] as HTMLElement;
|
||||||
|
if (formEl.tagName.toLowerCase() === "input") {
|
||||||
|
const inputEl = formEl as HTMLInputElement;
|
||||||
|
if (inputEl.type != null && inputEl.type.toLowerCase() === "checkbox") {
|
||||||
|
inputEl.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formEl.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
CdkFixedSizeVirtualScroll,
|
||||||
|
FixedSizeVirtualScrollStrategy,
|
||||||
|
VIRTUAL_SCROLL_STRATEGY,
|
||||||
|
} from "@angular/cdk/scrolling";
|
||||||
|
import { Directive, forwardRef } from "@angular/core";
|
||||||
|
|
||||||
|
// Custom virtual scroll strategy for cdk-virtual-scroll
|
||||||
|
// Uses a sample list item to set the itemSize for FixedSizeVirtualScrollStrategy
|
||||||
|
// The use case is the same as FixedSizeVirtualScrollStrategy, but it avoids locking in pixel sizes in the template.
|
||||||
|
export class CipherListVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
|
||||||
|
private checkItemSizeCallback: any;
|
||||||
|
private timeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
itemSize: number,
|
||||||
|
minBufferPx: number,
|
||||||
|
maxBufferPx: number,
|
||||||
|
checkItemSizeCallback: any
|
||||||
|
) {
|
||||||
|
super(itemSize, minBufferPx, maxBufferPx);
|
||||||
|
this.checkItemSizeCallback = checkItemSizeCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentRendered() {
|
||||||
|
if (this.timeout != null) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = setTimeout(this.checkItemSizeCallback, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _cipherListVirtualScrollStrategyFactory(cipherListDir: CipherListVirtualScroll) {
|
||||||
|
return cipherListDir._scrollStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "cdk-virtual-scroll-viewport[itemSize]",
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VIRTUAL_SCROLL_STRATEGY,
|
||||||
|
useFactory: _cipherListVirtualScrollStrategyFactory,
|
||||||
|
deps: [forwardRef(() => CipherListVirtualScroll)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CipherListVirtualScroll extends CdkFixedSizeVirtualScroll {
|
||||||
|
_scrollStrategy: CipherListVirtualScrollStrategy;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._scrollStrategy = new CipherListVirtualScrollStrategy(
|
||||||
|
this.itemSize,
|
||||||
|
this.minBufferPx,
|
||||||
|
this.maxBufferPx,
|
||||||
|
this.checkAndUpdateItemSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAndUpdateItemSize = () => {
|
||||||
|
const sampleItem = document.querySelector(
|
||||||
|
"cdk-virtual-scroll-viewport .virtual-scroll-item"
|
||||||
|
) as HTMLElement;
|
||||||
|
const newItemSize = sampleItem?.offsetHeight;
|
||||||
|
|
||||||
|
if (newItemSize != null && newItemSize !== this.itemSize) {
|
||||||
|
this.itemSize = newItemSize;
|
||||||
|
this._scrollStrategy.updateItemAndBufferSize(
|
||||||
|
this.itemSize,
|
||||||
|
this.minBufferPx,
|
||||||
|
this.maxBufferPx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
14
libs/angular/src/directives/fallback-src.directive.ts
Normal file
14
libs/angular/src/directives/fallback-src.directive.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appFallbackSrc]",
|
||||||
|
})
|
||||||
|
export class FallbackSrcDirective {
|
||||||
|
@Input("appFallbackSrc") appFallbackSrc: string;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef) {}
|
||||||
|
|
||||||
|
@HostListener("error") onError() {
|
||||||
|
this.el.nativeElement.src = this.appFallbackSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
libs/angular/src/directives/input-strip-spaces.directive.ts
Normal file
12
libs/angular/src/directives/input-strip-spaces.directive.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "input[appInputStripSpaces]",
|
||||||
|
})
|
||||||
|
export class InputStripSpacesDirective {
|
||||||
|
constructor(private el: ElementRef<HTMLInputElement>) {}
|
||||||
|
|
||||||
|
@HostListener("input") onInput() {
|
||||||
|
this.el.nativeElement.value = this.el.nativeElement.value.replace(/ /g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
32
libs/angular/src/directives/input-verbatim.directive.ts
Normal file
32
libs/angular/src/directives/input-verbatim.directive.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appInputVerbatim]",
|
||||||
|
})
|
||||||
|
export class InputVerbatimDirective {
|
||||||
|
@Input() set appInputVerbatim(condition: boolean | string) {
|
||||||
|
this.disableComplete = condition === "" || condition === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableComplete: boolean;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef, private renderer: Renderer2) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.disableComplete && !this.el.nativeElement.hasAttribute("autocomplete")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "autocomplete", "off");
|
||||||
|
}
|
||||||
|
if (!this.el.nativeElement.hasAttribute("autocapitalize")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "autocapitalize", "none");
|
||||||
|
}
|
||||||
|
if (!this.el.nativeElement.hasAttribute("autocorrect")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "autocorrect", "none");
|
||||||
|
}
|
||||||
|
if (!this.el.nativeElement.hasAttribute("spellcheck")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "spellcheck", "false");
|
||||||
|
}
|
||||||
|
if (!this.el.nativeElement.hasAttribute("inputmode")) {
|
||||||
|
this.renderer.setAttribute(this.el.nativeElement, "inputmode", "verbatim");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
libs/angular/src/directives/not-premium.directive.ts
Normal file
27
libs/angular/src/directives/not-premium.directive.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||||
|
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the element if the user has premium.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: "[appNotPremium]",
|
||||||
|
})
|
||||||
|
export class NotPremiumDirective implements OnInit {
|
||||||
|
constructor(
|
||||||
|
private templateRef: TemplateRef<any>,
|
||||||
|
private viewContainer: ViewContainerRef,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const premium = await this.stateService.getCanAccessPremium();
|
||||||
|
|
||||||
|
if (premium) {
|
||||||
|
this.viewContainer.clear();
|
||||||
|
} else {
|
||||||
|
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
libs/angular/src/directives/premium.directive.ts
Normal file
27
libs/angular/src/directives/premium.directive.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||||
|
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only shows the element if the user has premium.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: "[appPremium]",
|
||||||
|
})
|
||||||
|
export class PremiumDirective implements OnInit {
|
||||||
|
constructor(
|
||||||
|
private templateRef: TemplateRef<any>,
|
||||||
|
private viewContainer: ViewContainerRef,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const premium = await this.stateService.getCanAccessPremium();
|
||||||
|
|
||||||
|
if (premium) {
|
||||||
|
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||||
|
} else {
|
||||||
|
this.viewContainer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
libs/angular/src/directives/select-copy.directive.ts
Normal file
37
libs/angular/src/directives/select-copy.directive.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appSelectCopy]",
|
||||||
|
})
|
||||||
|
export class SelectCopyDirective {
|
||||||
|
constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) {}
|
||||||
|
|
||||||
|
@HostListener("copy") onCopy() {
|
||||||
|
if (window == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let copyText = "";
|
||||||
|
const selection = window.getSelection();
|
||||||
|
for (let i = 0; i < selection.rangeCount; i++) {
|
||||||
|
const range = selection.getRangeAt(i);
|
||||||
|
const text = range.toString();
|
||||||
|
|
||||||
|
// The selection should only contain one line of text. In some cases however, the
|
||||||
|
// selection contains newlines and space characters from the indentation of following
|
||||||
|
// sibling nodes. To avoid copying passwords containing trailing newlines and spaces
|
||||||
|
// that aren't part of the password, the selection has to be trimmed.
|
||||||
|
let stringEndPos = text.length;
|
||||||
|
const newLinePos = text.search(/(?:\r\n|\r|\n)/);
|
||||||
|
if (newLinePos > -1) {
|
||||||
|
const otherPart = text.substr(newLinePos).trim();
|
||||||
|
if (otherPart === "") {
|
||||||
|
stringEndPos = newLinePos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copyText += text.substring(0, stringEndPos);
|
||||||
|
}
|
||||||
|
this.platformUtilsService.copyToClipboard(copyText, { window: window });
|
||||||
|
}
|
||||||
|
}
|
||||||
10
libs/angular/src/directives/stop-click.directive.ts
Normal file
10
libs/angular/src/directives/stop-click.directive.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Directive, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appStopClick]",
|
||||||
|
})
|
||||||
|
export class StopClickDirective {
|
||||||
|
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||||
|
$event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
libs/angular/src/directives/stop-prop.directive.ts
Normal file
10
libs/angular/src/directives/stop-prop.directive.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Directive, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appStopProp]",
|
||||||
|
})
|
||||||
|
export class StopPropDirective {
|
||||||
|
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
libs/angular/src/directives/true-false-value.directive.ts
Normal file
49
libs/angular/src/directives/true-false-value.directive.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Directive, ElementRef, forwardRef, HostListener, Input, Renderer2 } from "@angular/core";
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||||
|
|
||||||
|
// ref: https://juristr.com/blog/2018/02/ng-true-value-directive/
|
||||||
|
@Directive({
|
||||||
|
selector: "input[type=checkbox][appTrueFalseValue]",
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => TrueFalseValueDirective),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class TrueFalseValueDirective implements ControlValueAccessor {
|
||||||
|
@Input() trueValue = true;
|
||||||
|
@Input() falseValue = false;
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
|
||||||
|
|
||||||
|
@HostListener("change", ["$event"])
|
||||||
|
onHostChange(ev: any) {
|
||||||
|
this.propagateChange(ev.target.checked ? this.trueValue : this.falseValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(obj: any): void {
|
||||||
|
if (obj === this.trueValue) {
|
||||||
|
this.renderer.setProperty(this.elementRef.nativeElement, "checked", true);
|
||||||
|
} else {
|
||||||
|
this.renderer.setProperty(this.elementRef.nativeElement, "checked", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
/* nothing */
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
/* nothing */
|
||||||
|
}
|
||||||
|
|
||||||
|
private propagateChange = (_: any) => {
|
||||||
|
/* nothing */
|
||||||
|
};
|
||||||
|
}
|
||||||
42
libs/angular/src/guards/auth.guard.ts
Normal file
42
libs/angular/src/guards/auth.guard.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private keyConnectorService: KeyConnectorService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
|
||||||
|
const authStatus = await this.authService.getAuthStatus();
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
|
this.messagingService.send("authBlocked", { url: routerState.url });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
|
if (routerState != null) {
|
||||||
|
this.messagingService.send("lockedUrl", { url: routerState.url });
|
||||||
|
}
|
||||||
|
return this.router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!routerState.url.includes("remove-password") &&
|
||||||
|
(await this.keyConnectorService.getConvertAccountRequired())
|
||||||
|
) {
|
||||||
|
return this.router.createUrlTree(["/remove-password"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
libs/angular/src/guards/lock.guard.ts
Normal file
25
libs/angular/src/guards/lock.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { CanActivate, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LockGuard implements CanActivate {
|
||||||
|
protected homepage = "vault";
|
||||||
|
protected loginpage = "login";
|
||||||
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
|
async canActivate() {
|
||||||
|
const authStatus = await this.authService.getAuthStatus();
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUrl =
|
||||||
|
authStatus === AuthenticationStatus.LoggedOut ? this.loginpage : this.homepage;
|
||||||
|
|
||||||
|
return this.router.createUrlTree([redirectUrl]);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
libs/angular/src/guards/unauth.guard.ts
Normal file
25
libs/angular/src/guards/unauth.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { CanActivate, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UnauthGuard implements CanActivate {
|
||||||
|
protected homepage = "vault";
|
||||||
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
|
async canActivate() {
|
||||||
|
const authStatus = await this.authService.getAuthStatus();
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
|
return this.router.createUrlTree(["lock"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.router.createUrlTree([this.homepage]);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
libs/angular/src/images/cards/amex-dark.png
Normal file
BIN
libs/angular/src/images/cards/amex-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 773 B |
BIN
libs/angular/src/images/cards/amex-light.png
Normal file
BIN
libs/angular/src/images/cards/amex-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 773 B |
BIN
libs/angular/src/images/cards/diners_club-dark.png
Normal file
BIN
libs/angular/src/images/cards/diners_club-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 783 B |
BIN
libs/angular/src/images/cards/diners_club-light.png
Normal file
BIN
libs/angular/src/images/cards/diners_club-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 713 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user