1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Chad Scharf
9326a9a4a7 version bump v2.18.1 2021-01-25 17:40:41 -05:00
Addison Beck
6b3b1a901d Lunr search bug (#810)
* changed hrtime library

* updated jslib
2021-01-25 16:57:43 -05:00
Kyle Spearrin
41d0880bd5 New translations messages.json (Polish) (#809) 2021-01-25 16:53:07 -05:00
Kyle Spearrin
3e66a7162c New Crowdin updates (#807)
* New translations messages.json (Romanian)

* New translations messages.json (Polish)

* New translations messages.json (Malayalam)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Swedish)

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Afrikaans)

* New translations messages.json (Spanish)

* New translations messages.json (English, India)
2021-01-25 16:52:58 -05:00
Thomas Rittson
c93e39566f Add branded 404 page to replace Github Pages 404 (#798) 2021-01-25 16:52:35 -05:00
Oscar Hinton
6af405b89d Update emergency access user-access link (#797)
The help link for user-access incorrectly linked to the wrong page. Changed to the correct link.
2021-01-25 16:52:13 -05:00
vachan-maker
f9a2ec138e Update learn more link for Emergency Access (#796) 2021-01-25 16:52:04 -05:00
Matt Gibson
ddab1a3cee Fix context copy buttons work only with TOTP present (#794) 2021-01-25 16:51:55 -05:00
Oscar Hinton
f24836b0a5 Fix emergency access confirm not working with two-factor enabled (#792) 2021-01-25 16:51:45 -05:00
213 changed files with 7374 additions and 22659 deletions

View File

@@ -5,6 +5,9 @@ on:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
release:
types:
- published
jobs:
cloc:
@@ -38,40 +41,27 @@ jobs:
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Login to Azure
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Log into docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
echo "${{ secrets.DOCKER_DELEGATION_KEY }}" > ~/.docker/trust/private/$DOCKER_DELEGATION_KEY_ID.key
echo "${{ secrets.DOCKER_REPO_WEB_KEY }}" > ~/.docker/trust/private/$DOCKER_WEB_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
DOCKER_DELEGATION_KEY_ID: "5702b22123e058cbd96a7a43000cb981ae98ef3f2f4aa34138ab3dc1d011e446"
DOCKER_WEB_KEY_ID: "0f88641697187f42a31b584897cd4edfe80360a5209122d9e7f71af17a6422e4"
- name: Checkout repo
uses: actions/checkout@v2
@@ -81,44 +71,60 @@ jobs:
- name: Build
run: |
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run dist:selfhost
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
- name: Tag rc branch
if: github.ref == 'refs/heads/rc'
run: docker tag bitwarden/web bitwarden/web:rc
chmod +x ./build.sh
./build.sh
- name: Tag dev
if: github.ref == 'refs/heads/master'
run: docker tag bitwarden/web bitwarden/web:dev
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: ./build.sh tag dev
- name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
- name: Tag beta
if: github.event_name == 'release'
run: ./build.sh tag beta
- name: Tag version
if: github.event_name == 'release'
run: ./build.sh tag $($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: List docker images
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker images
- name: Push rc images
if: github.ref == 'refs/heads/rc'
run: docker push bitwarden/web:rc
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push dev images
if: github.ref == 'refs/heads/master'
run: docker push bitwarden/web:dev
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: ./build.sh push dev
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
- name: Push beta images
if: github.event_name == 'release'
run: ./build.sh push beta
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Push latest images
if: github.event_name == 'release'
run: ./build.sh push latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Push version images
if: github.event_name == 'release'
run: ./build.sh push $($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
- name: Log out of docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker logout
windows:

View File

@@ -1,157 +0,0 @@
name: Release
on:
workflow_dispatch:
inputs:
release_tag_name_input:
description: "Release Tag Name <X.X.X>"
required: true
jobs:
setup:
runs-on: ubuntu-latest
outputs:
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
release_version: ${{ steps.create_tags.outputs.package_version }}
tag_version: ${{ steps.create_tags.outputs.tag_version }}
steps:
- name: Branch check
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
echo "==================================="
echo "[!] Can only release from rc branch"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
- name: Create Release Vars
id: create_tags
run: |
case "${RELEASE_TAG_NAME_INPUT:0:1}" in
v)
echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
;;
[0-9])
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
;;
*)
exit 1
;;
esac
env:
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
- name: Create Draft Release
id: create_release
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # 1.1.4 - Repo Archived
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.RELEASE_TAG_NAME }}
release_name: Version ${{ env.RELEASE_NAME }}
draft: true
prerelease: false
ubuntu:
runs-on: ubuntu-latest
needs: setup
env:
RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Build
run: |
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run dist:selfhost
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
- name: Tag version
run: docker tag bitwarden/web bitwarden/web:$RELEASE_VERSION
- name: List Docker images
run: docker images
- name: Push latest images
run: docker push bitwarden/web:latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push version images
run: docker push bitwarden/web:$RELEASE_VERSION
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Log out of Docker
run: docker logout

1
.gitignore vendored
View File

@@ -11,4 +11,3 @@ dist/
*.zip
build/
!dev-server.shared.pem
config/development.json

View File

@@ -5,8 +5,8 @@
The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).
</p>
<p align="center">
<a href="https://github.com/bitwarden/web/actions?query=branch:master" target="_blank">
<img src="https://github.com/bitwarden/web/actions/workflows/build.yml/badge.svg?branch=master" alt="Github Workflow build on master" />
<a href="https://ci.appveyor.com/project/bitwarden/web/branch/master" target="_blank">
<img src="https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true" alt="appveyor build" />
</a>
<a href="https://crowdin.com/project/bitwarden-web" target="_blank">
<img src="https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg" alt="Crowdin" />
@@ -34,36 +34,52 @@ npm install
npm run build:watch
```
You can now access the web vault in your browser at `https://localhost:8080`.
You can now access the web vault in your browser at `https://localhost:8080`. You can adjust your API endpoint settings in `src/app/services/services.module.ts` by altering the `apiService.setUrls` call. For example:
If you want to point the development web vault to the production APIs, you can run using:
```typescript
await apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://mylocalapi' : null,
identity: isDev ? 'http://mylocalidentity' : null,
events: isDev ? 'http://mylocalevents' : null,
});
```
If you want to point the development web vault to the production APIs, you can set:
```typescript
await apiService.setUrls({
base: null,
api: 'https://api.bitwarden.com',
identity: 'https://identity.bitwarden.com',
events: 'https://events.bitwarden.com',
});
```
And note to run the app with:
```
npm install
ENV=production npm run build:watch
npm run build:prod:watch
```
You can also manually adjusting your API endpoint settings by adding `config/development.js` overriding any of the values in `config/base.json`. For example:
## Common Issues:
```typescript
{
"proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url",
"proxyPortal": "http://your-portal-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"]
}
```
To pick up the overrides in the newly created `config/development.js` file, run the app with:
### CORS
If you run the frontend and receive a notification after attempting to login that says:
```
npm run build:dev:watch
An error has occurred.
NetworkError when attempting to fetch resource.
```
And in the console:
```
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.bitwarden.com/accounts/prelogin. (Reason: CORS header Access-Control-Allow-Origin missing).
```
This means that you are having a CORS header issue. This can be mitigated by using a CORS header changing extension in your browser such as [this one.](https://mybrowseraddon.com/access-control-allow-origin.html)
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Code contributions are welcome! Please commit any pull requests against the `master` branch.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.

83
appveyor.yml Normal file
View File

@@ -0,0 +1,83 @@
image:
- Visual Studio 2017
- Ubuntu1804
branches:
except:
- l10n_master
- gh-pages
services:
- docker
stack: node 10
init:
- ps: |
if($isWindows) {
Install-Product node 10
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version
$env:PUSH_DOCKER = "false"
$env:PROD_DEPLOY = "false"
$env:TAG_NAME = ""
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
$env:TAG_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
echo "This is a production deployment for ${env:TAG_NAME}."
}
if("${env:DOCKER_USERNAME}" -ne "" -and "${env:DOCKER_PASSWORD}" -ne "") {
$env:PUSH_DOCKER = "true"
}
if($isWindows) {
choco install cloc --no-progress
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
}
before_build:
- node --version
- npm --version
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
fi
- cmd: set "GIT_PATH=C:\Program Files\Git\mingw64\libexec\git-core"
- cmd: set "PATH=%GIT_PATH%;%PATH%"
build_script:
- sh: chmod +x ./build.sh
- ps: |
if($isLinux) {
./build.sh
./build.sh tag dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh tag beta
./build.sh tag $env:TAG_NAME
}
docker images
if($env:PUSH_DOCKER -eq "true") {
./build.sh push dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh push beta
./build.sh push latest
./build.sh push $env:TAG_NAME
}
}
}
- cmd: npm install
- cmd: npm run build:prod
after_build:
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
docker logout
fi

33
build.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo ""
if [ $# -gt 1 -a "$1" == "push" ]
then
TAG=$2
echo "# Pushing Web ($TAG)"
echo ""
docker push bitwarden/web:$TAG
elif [ $# -gt 1 -a "$1" == "tag" ]
then
TAG=$2
echo "Tagging Web as '$TAG'"
docker tag bitwarden/web bitwarden/web:$TAG
else
echo "# Building Web"
echo ""
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run sub:update
npm run dist:selfhost
echo ""
echo "Building docker image"
docker --version
docker build -t bitwarden/web $DIR/.
fi

View File

@@ -1,29 +0,0 @@
function load(envName) {
const envOverrides = {
'production': () => require('./config/production.json'),
'qa': () => require('./config/qa.json'),
'development': () => require('./config/development.json'),
};
const baseConfig = require('./config/base.json');
const overrideConfig = envOverrides.hasOwnProperty(envName) ? envOverrides[envName]() : {};
return {
...baseConfig,
...overrideConfig
};
}
function log(configObj) {
const repeatNum = 50
console.log(`${"=".repeat(repeatNum)}\nenvConfig`)
Object.entries(configObj).map(([key, value]) => {
console.log(` ${key}: ${value}`)
})
console.log(`${"=".repeat(repeatNum)}`)
}
module.exports = {
load,
log
};

View File

@@ -1,8 +0,0 @@
{
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840",
"proxyPortal": "http://localhost:52313",
"allowedHosts": []
}

View File

@@ -1,7 +0,0 @@
{
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com",
"proxyNotifications": "https://notifications.bitwarden.com",
"proxyPortal": "https://portal.bitwarden.com"
}

View File

@@ -1,7 +0,0 @@
{
"proxyApi": "https://api.qa.bitwarden.com",
"proxyIdentity": "https://identity.qa.bitwarden.com",
"proxyEvents": "https://events.qa.bitwarden.com",
"proxyNotifications": "https://notifications.qa.bitwarden.com",
"proxyPortal": "https://portal.qa.bitwarden.com"
}

2
jslib

Submodule jslib updated: f6d91e2d92...9ddec9baf8

1927
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden-web",
"version": "2.20.4",
"version": "2.18.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
@@ -13,12 +13,8 @@
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build": "gulp prebuild && webpack",
"build:watch": "gulp prebuild && webpack-dev-server",
"build:dev": "gulp prebuild && cross-env ENV=development webpack",
"build:dev:watch": "gulp prebuild && cross-env ENV=development webpack-dev-server",
"build:qa": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack",
"build:qa:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-dev-server",
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
@@ -28,23 +24,23 @@
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
"deploy": "npm run dist && gh-pages -d build",
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "tslint 'src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' --fix"
"lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix"
},
"devDependencies": {
"@angular/compiler-cli": "^9.1.12",
"@ngtools/webpack": "^9.1.12",
"@types/jquery": "^3.5.5",
"@types/jquery": "^3.5.1",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-forge": "^0.9.7",
"@types/papaparse": "^5.2.0",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "^4.4.0",
"angular2-template-loader": "^0.6.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^5.1.1",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
@@ -77,26 +73,26 @@
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@microsoft/signalr": "3.1.13",
"@microsoft/signalr-protocol-msgpack": "3.1.13",
"@microsoft/signalr": "3.1.0",
"@microsoft/signalr-protocol-msgpack": "3.1.0",
"angular2-toaster": "8.0.0",
"big-integer": "1.6.48",
"angulartics2": "9.1.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"braintree-web-drop-in": "1.13.0",
"browser-hrtime": "^1.1.8",
"core-js": "2.6.2",
"date-input-polyfill": "^2.14.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
"font-awesome": "4.7.0",
"jquery": "3.6.0",
"jquery": "3.4.1",
"lunr": "2.3.3",
"ngx-infinite-scroll": "7.0.1",
"node-forge": "0.10.0",
"papaparse": "5.2.0",
"node-forge": "0.7.6",
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "6.6.2",
"sweetalert2": "10.15.4",
"sweetalert2": "9.8.1",
"tslib": "^2.0.1",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",

35
src/404.css Normal file
View File

@@ -0,0 +1,35 @@
html, body, .row {
height: 100%;
-webkit-font-smoothing: antialiased;
}
h2 {
font-size: 25px;
margin-bottom: 12.5px;
font-weight: 500;
line-height: 1.1;
}
.brand {
font-size: 23px;
line-height: 25px;
color: #fff;
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.banner {
background-color: #175DDC;
height: 56px;
}
.content {
padding-top: 20px;
padding-bottom: 20px;
padding-left: 15px;
padding-right: 15px;
}
.footer {
padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6;
}

View File

@@ -5,11 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
<link href="/404/styles.css" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<link rel="stylesheet" crossorigin="anonymous"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0= sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic"
rel="stylesheet" type="text/css">
<link href="/404.css" rel="stylesheet" type="text/css">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,119 +0,0 @@
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: url(../fonts/Open_Sans-italic-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url(../fonts/Open_Sans-italic-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: url(../fonts/Open_Sans-italic-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: url(../fonts/Open_Sans-italic-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: url(../fonts/Open_Sans-italic-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url(../fonts/Open_Sans-normal-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url(../fonts/Open_Sans-normal-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url(../fonts/Open_Sans-normal-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url(../fonts/Open_Sans-normal-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: url(../fonts/Open_Sans-normal-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}
body {
font-family: 'Open Sans';
}
html, body, .row {
height: 100%;
-webkit-font-smoothing: antialiased;
}
h2 {
font-size: 25px;
margin-bottom: 12.5px;
font-weight: 500;
line-height: 1.1;
}
.brand {
font-size: 23px;
line-height: 25px;
color: #fff;
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.banner {
background-color: #175DDC;
height: 56px;
}
.content {
padding-top: 20px;
padding-bottom: 20px;
padding-left: 15px;
padding-right: 15px;
}
.footer {
padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6;
}

View File

@@ -36,7 +36,7 @@ export class AcceptEmergencyComponent implements OnInit {
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async qParams => {
this.route.queryParams.subscribe(async (qParams) => {
if (fired) {
return;
}

View File

@@ -37,7 +37,7 @@ export class AcceptOrganizationComponent implements OnInit {
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async qParams => {
this.route.queryParams.subscribe(async (qParams) => {
if (fired) {
return;
}

View File

@@ -33,6 +33,13 @@ export class LockComponent extends BaseLockComponent {
async ngOnInit() {
await super.ngOnInit();
const authed = await this.userService.isAuthenticated();
if (!authed) {
this.router.navigate(['/']);
} else if (await this.cryptoService.hasKey()) {
this.router.navigate(['vault']);
}
this.onSuccessfulSubmit = () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {

View File

@@ -34,7 +34,7 @@ export class LoginComponent extends BaseLoginComponent {
}
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -17,7 +18,8 @@ export class RecoverDeleteComponent {
formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService,
private toasterService: ToasterService, private i18nService: I18nService) {
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService) {
}
async submit() {
@@ -26,6 +28,7 @@ export class RecoverDeleteComponent {
request.email = this.email.trim().toLowerCase();
this.formPromise = this.apiService.postAccountRecoverDelete(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Started Delete Recovery' });
this.toasterService.popAsync('success', null, this.i18nService.t('deleteRecoverEmailSent'));
this.router.navigate(['/']);
} catch { }

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
@@ -21,8 +22,9 @@ export class RecoverTwoFactorComponent {
formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService,
private toasterService: ToasterService, private i18nService: I18nService,
private cryptoService: CryptoService, private authService: AuthService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private cryptoService: CryptoService,
private authService: AuthService) { }
async submit() {
try {
@@ -33,6 +35,7 @@ export class RecoverTwoFactorComponent {
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Recovered 2FA' });
this.toasterService.popAsync('success', null, this.i18nService.t('twoStepRecoverDisabled'));
this.router.navigate(['/']);
} catch { }

View File

@@ -62,7 +62,7 @@ export class RegisterComponent extends BaseRegisterComponent {
}
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(qParams => {
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
@@ -96,8 +96,8 @@ export class RegisterComponent extends BaseRegisterComponent {
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.email, invite.organizationUserId);
if (policies.data != null) {
const policiesData = policies.data.map(p => new PolicyData(p));
this.policies = policiesData.map(p => new Policy(p));
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
}
} catch { }
}

View File

@@ -36,7 +36,7 @@ export class SsoComponent extends BaseSsoComponent {
async ngOnInit() {
super.ngOnInit();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
} else {

View File

@@ -33,10 +33,16 @@
required appAutofocus appInputVerbatim autocomplete="new-password">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
<div id="web-authn-frame" class="mb-3">
<iframe id="webauthn_iframe"></iframe>
</div>
<ng-container *ngIf="selectedProviderType === providerType.U2f">
<p class="text-center" *ngIf="!u2fReady">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
<ng-container *ngIf="u2fReady">
<p class="text-center">{{'insertU2f' | i18n}}</p>
<img src="../../images/u2fkey.jpg" alt="" class="rounded img-fluid mb-3">
</ng-container>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo">
@@ -45,7 +51,7 @@
</div>
</ng-container>
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn" aria-hidden="true"></i>
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
<div class="form-check" *ngIf="selectedProviderType != null">
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
[(ngModel)]="remember">
@@ -59,7 +65,7 @@
<div class="d-flex mb-3">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.WebAuthn">
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
@@ -78,3 +84,4 @@
</div>
</form>
<ng-template #twoFactorOptions></ng-template>
<iframe id="u2f_iframe" hidden></iframe>

View File

@@ -26,7 +26,7 @@ export class VerifyEmailTokenComponent implements OnInit {
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async qParams => {
this.route.queryParams.subscribe(async (qParams) => {
if (fired) {
return;
}

View File

@@ -8,6 +8,7 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -26,13 +27,13 @@ export class VerifyRecoverDeleteComponent implements OnInit {
private token: string;
constructor(private router: Router, private apiService: ApiService,
private toasterService: ToasterService, private i18nService: I18nService,
private route: ActivatedRoute) {
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute) {
}
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async qParams => {
this.route.queryParams.subscribe(async (qParams) => {
if (fired) {
return;
}
@@ -52,6 +53,7 @@ export class VerifyRecoverDeleteComponent implements OnInit {
const request = new VerifyDeleteRecoverRequest(this.userId, this.token);
this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Recovered Delete' });
this.toasterService.popAsync('success', this.i18nService.t('accountDeleted'),
this.i18nService.t('accountDeletedDesc'));
this.router.navigate(['/']);

View File

@@ -87,10 +87,9 @@ import { VaultComponent } from './vault/vault.component';
import { OrganizationGuardService } from './services/organization-guard.service';
import { OrganizationTypeGuardService } from './services/organization-type-guard.service';
import { UnauthGuardService } from './services/unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { LockGuardService } from 'jslib/angular/services/lock-guard.service';
import { UnauthGuardService } from 'jslib/angular/services/unauth-guard.service';
import { Permissions } from 'jslib/enums/permissions';
@@ -123,11 +122,7 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'passwordHint' },
},
{
path: 'lock',
component: LockComponent,
canActivate: [LockGuardService],
},
{ path: 'lock', component: LockComponent },
{ path: 'verify-email', component: VerifyEmailTokenComponent },
{
path: 'accept-organization',
@@ -158,11 +153,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
{
/*{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},
},*/
],
},
{

View File

@@ -5,8 +5,11 @@ import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import {
Component,
@@ -67,12 +70,12 @@ export class AppComponent implements OnDestroy, OnInit {
private idleTimer: number = null;
private isIdle = false;
constructor(
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
private broadcasterService: BroadcasterService, private userService: UserService,
private tokenService: TokenService, private folderService: FolderService,
private settingsService: SettingsService, private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService, private cipherService: CipherService,
private authService: AuthService, private router: Router,
private authService: AuthService, private router: Router, private analytics: Angulartics2,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
@@ -136,18 +139,15 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate(['settings/premium']);
}
break;
case 'emailVerificationRequired':
const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('emailVerificationRequiredDesc'),
this.i18nService.t('emailVerificationRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri('https://bitwarden.com/help/article/create-bitwarden-account/');
}
break;
case 'showToast':
this.showToast(message);
break;
case 'analyticsEventTrack':
this.analytics.eventTrack.next({
action: message.action,
properties: { label: message.label },
});
break;
case 'setFullWidth':
this.setFullWidth();
break;
@@ -157,7 +157,7 @@ export class AppComponent implements OnDestroy, OnInit {
});
});
this.router.events.subscribe(event => {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll('.modal'));
for (const modal of modals) {
@@ -198,6 +198,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.searchService.clearIndex();
this.authService.logOut(async () => {
this.analytics.eventTrack.next({ action: 'Logged Out' });
if (expired) {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));

View File

@@ -1,6 +1,8 @@
import 'core-js';
import { ToasterModule } from 'angular2-toaster';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { AppRoutingModule } from './app-routing.module';
@@ -112,11 +114,10 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { EmergencyAccessAddEditComponent } from './settings/emergency-access-add-edit.component';
import { EmergencyAccessAttachmentsComponent } from './settings/emergency-access-attachments.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
import { EmergencyAccessConfirmComponent } from './settings/emergency-access-confirm.component';
import { EmergencyAccessTakeoverComponent } from './settings/emergency-access-takeover.component';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
import { EmergencyAddEditComponent } from './settings/emergency-add-edit.component';
import { LinkSsoComponent } from './settings/link-sso.component';
import { OptionsComponent } from './settings/options.component';
@@ -133,8 +134,8 @@ import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
import { TwoFactorRecoveryComponent } from './settings/two-factor-recovery.component';
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
import { TwoFactorWebAuthnComponent } from './settings/two-factor-webauthn.component';
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
import { UpdateKeyComponent } from './settings/update-key.component';
import { UpdateLicenseComponent } from './settings/update-license.component';
@@ -165,7 +166,6 @@ import { CiphersComponent } from './vault/ciphers.component';
import { CollectionsComponent } from './vault/collections.component';
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
import { GroupingsComponent } from './vault/groupings.component';
import { SendInfoComponent } from './vault/send-info.component';
import { ShareComponent } from './vault/share.component';
import { VaultComponent } from './vault/vault.component';
@@ -190,8 +190,8 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
import {
DatePipe,
registerLocaleData,
DatePipe,
} from '@angular/common';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
@@ -254,6 +254,11 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
}),
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
@@ -299,7 +304,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
@@ -376,7 +380,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SendInfoComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
@@ -392,8 +395,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
@@ -421,7 +424,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
DeleteAccountComponent,
DeleteOrganizationComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAddEditComponent,
@@ -447,7 +449,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorEmailComponent,
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorWebAuthnComponent,
TwoFactorU2fComponent,
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
],

View File

@@ -15,8 +15,8 @@ export class FooterComponent implements OnInit {
constructor(private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = await this.platformUtilsService.getApplicationVersion();
this.version = this.platformUtilsService.getApplicationVersion();
}
}

View File

@@ -16,9 +16,9 @@ export class FrontendLayoutComponent implements OnInit, OnDestroy {
constructor(private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = await this.platformUtilsService.getApplicationVersion();
this.version = this.platformUtilsService.getApplicationVersion();
document.body.classList.add('layout_frontend');
}

View File

@@ -8,9 +8,9 @@
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/sends">{{'send' | i18n}}</a>
</li>
<!--<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/sends">Send</a>
</li>-->
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
</li>

View File

@@ -42,7 +42,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
}
document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async params => {
this.route.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -14,7 +15,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { EncString } from 'jslib/models/domain/encString';
import { CipherString } from 'jslib/models/domain/cipherString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CollectionRequest } from 'jslib/models/request/collectionRequest';
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
@@ -45,8 +46,9 @@ export class CollectionAddEditComponent implements OnInit {
private orgKey: SymmetricCryptoKey;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService, private userService: UserService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private userService: UserService) { }
async ngOnInit() {
const organization = await this.userService.getOrganization(this.organizationId);
@@ -54,7 +56,7 @@ export class CollectionAddEditComponent implements OnInit {
this.editMode = this.loading = this.collectionId != null;
if (this.accessGroups) {
const groupsResponse = await this.apiService.getGroups(this.organizationId);
this.groups = groupsResponse.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groupsResponse.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
}
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
@@ -63,11 +65,11 @@ export class CollectionAddEditComponent implements OnInit {
this.title = this.i18nService.t('editCollection');
try {
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
this.name = await this.cryptoService.decryptToUtf8(new EncString(collection.name), this.orgKey);
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
this.externalId = collection.externalId;
if (collection.groups != null && this.groups.length > 0) {
collection.groups.forEach(s => {
const group = this.groups.filter(g => !g.accessAll && g.id === s.id);
collection.groups.forEach((s) => {
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
(group[0] as any).readOnly = s.readOnly;
@@ -80,7 +82,7 @@ export class CollectionAddEditComponent implements OnInit {
this.title = this.i18nService.t('addCollection');
}
this.groups.forEach(g => {
this.groups.forEach((g) => {
if (g.accessAll) {
(g as any).checked = true;
}
@@ -101,7 +103,7 @@ export class CollectionAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.groups.forEach(g => this.check(g, select));
this.groups.forEach((g) => this.check(g, select));
}
async submit() {
@@ -112,8 +114,8 @@ export class CollectionAddEditComponent implements OnInit {
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
request.externalId = this.externalId;
request.groups = this.groups.filter(g => (g as any).checked && !g.accessAll)
.map(g => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
try {
if (this.editMode) {
@@ -122,6 +124,7 @@ export class CollectionAddEditComponent implements OnInit {
this.formPromise = this.apiService.postCollection(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Collection' : 'Created Collection' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedCollectionId' : 'createdCollectionId', this.name));
this.onSavedCollection.emit();
@@ -143,6 +146,7 @@ export class CollectionAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteCollection(this.organizationId, this.collectionId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', this.name));
this.onDeletedCollection.emit();
} catch { }

View File

@@ -8,6 +8,7 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -51,15 +52,15 @@ export class CollectionsComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private searchService: SearchService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private userService: UserService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
@@ -76,7 +77,7 @@ export class CollectionsComponent implements OnInit {
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data.filter(c => c.organizationId === this.organizationId).map(r =>
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
this.resetPaging();
@@ -140,6 +141,7 @@ export class CollectionsComponent implements OnInit {
try {
await this.apiService.deleteCollection(this.organizationId, collection.id);
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', collection.name));
this.removeCollection(collection);
} catch { }

View File

@@ -50,7 +50,7 @@ export class EntityEventsComponent implements OnInit {
async load() {
if (this.showUser) {
const response = await this.apiService.getOrganizationUsers(this.organizationId);
response.data.forEach(u => {
response.data.forEach((u) => {
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
@@ -94,7 +94,7 @@ export class EntityEventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map(r => {
const events = response.data.map((r) => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const user = this.showUser && userId != null && this.orgUsersUserIdMap.has(userId) ?

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -41,7 +42,7 @@ export class EntityUsersComponent implements OnInit {
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService) { }
async ngOnInit() {
await this.loadUsers();
@@ -50,7 +51,7 @@ export class EntityUsersComponent implements OnInit {
get users() {
if (this.showSelected) {
return this.allUsers.filter(u => (u as any).checked);
return this.allUsers.filter((u) => (u as any).checked);
} else {
return this.allUsers;
}
@@ -58,12 +59,12 @@ export class EntityUsersComponent implements OnInit {
async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
if (this.entity === 'group') {
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach(s => {
const user = users.data.filter(u => u.id === s);
response.forEach((s) => {
const user = users.data.filter((u) => u.id === s);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
}
@@ -72,8 +73,8 @@ export class EntityUsersComponent implements OnInit {
} else if (this.entity === 'collection') {
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach(s => {
const user = users.data.filter(u => !u.accessAll && u.id === s.id);
response.forEach((s) => {
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
(user[0] as any).readOnly = s.readOnly;
@@ -83,7 +84,7 @@ export class EntityUsersComponent implements OnInit {
}
}
this.allUsers.forEach(u => {
this.allUsers.forEach((u) => {
if (this.entity === 'collection' && u.accessAll) {
(u as any).checked = true;
}
@@ -120,14 +121,17 @@ export class EntityUsersComponent implements OnInit {
async submit() {
try {
if (this.entity === 'group') {
const selections = this.users.filter(u => (u as any).checked).map(u => u.id);
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
} else {
const selections = this.users.filter(u => (u as any).checked && !u.accessAll)
.map(u => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
}
await this.formPromise;
this.analytics.eventTrack.next({
action: this.entity === 'group' ? 'Edited Group Users' : 'Edited Collection Users',
});
this.toasterService.popAsync('success', null, this.i18nService.t('updatedUsers'));
this.onEditedUsers.emit();
} catch { }

View File

@@ -39,7 +39,7 @@ export class EventsComponent implements OnInit {
private router: Router) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.useEvents) {
@@ -55,7 +55,7 @@ export class EventsComponent implements OnInit {
async load() {
const response = await this.apiService.getOrganizationUsers(this.organizationId);
response.data.forEach(u => {
response.data.forEach((u) => {
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
@@ -92,7 +92,7 @@ export class EventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map(r => {
const events = response.data.map((r) => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const user = userId != null && this.orgUsersUserIdMap.has(userId) ?

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -41,8 +42,8 @@ export class GroupAddEditComponent implements OnInit {
deletePromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.editMode = this.loading = this.groupId != null;
@@ -57,8 +58,8 @@ export class GroupAddEditComponent implements OnInit {
this.name = group.name;
this.externalId = group.externalId;
if (group.collections != null && this.collections != null) {
group.collections.forEach(s => {
const collection = this.collections.filter(c => c.id === s.id);
group.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id);
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
@@ -76,7 +77,7 @@ export class GroupAddEditComponent implements OnInit {
async loadCollections() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map(r =>
const collections = response.data.map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
}
@@ -89,7 +90,7 @@ export class GroupAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.collections.forEach(c => this.check(c, select));
this.collections.forEach((c) => this.check(c, select));
}
async submit() {
@@ -98,8 +99,8 @@ export class GroupAddEditComponent implements OnInit {
request.externalId = this.externalId;
request.accessAll = this.access === 'all';
if (!request.accessAll) {
request.collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
request.collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -109,6 +110,7 @@ export class GroupAddEditComponent implements OnInit {
this.formPromise = this.apiService.postGroup(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Group' : 'Created Group' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedGroupId' : 'createdGroupId', this.name));
this.onSavedGroup.emit();
@@ -130,6 +132,7 @@ export class GroupAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteGroup(this.organizationId, this.groupId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Group' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', this.name));
this.onDeletedGroup.emit();
} catch { }

View File

@@ -11,6 +11,7 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -48,12 +49,12 @@ export class GroupsComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private userService: UserService, private router: Router,
private searchService: SearchService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.useGroups) {
@@ -61,7 +62,7 @@ export class GroupsComponent implements OnInit {
return;
}
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
@@ -135,6 +136,7 @@ export class GroupsComponent implements OnInit {
try {
await this.apiService.deleteGroup(this.organizationId, group.id);
this.analytics.eventTrack.next({ action: 'Deleted Group' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', group.name));
this.removeGroup(group);
} catch { }

View File

@@ -21,7 +21,7 @@ export class ManageComponent implements OnInit {
constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.accessPolicies = this.organization.usePolicies;
this.accessEvents = this.organization.useEvents;

View File

@@ -11,8 +11,8 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
@@ -70,13 +70,13 @@ export class PeopleComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
private cryptoService: CryptoService, private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService,
private validationService: ValidationService) { }
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService,
private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.canManageUsers) {
@@ -87,10 +87,10 @@ export class PeopleComponent implements OnInit {
this.accessGroups = organization.useGroups;
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter(u => u.id === qParams.viewEvents);
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
this.events(user[0]);
}
@@ -107,7 +107,7 @@ export class PeopleComponent implements OnInit {
this.statusMap.clear();
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
this.allUsers.sort(Utils.getSortFunction(this.i18nService, 'email'));
this.allUsers.forEach(u => {
this.allUsers.forEach((u) => {
if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]);
} else {
@@ -231,6 +231,7 @@ export class PeopleComponent implements OnInit {
try {
await this.apiService.deleteOrganizationUser(this.organizationId, user.id);
this.analytics.eventTrack.next({ action: 'Deleted User' });
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.name || user.email));
this.removeUser(user);
} catch { }
@@ -242,6 +243,7 @@ export class PeopleComponent implements OnInit {
}
this.actionPromise = this.apiService.postOrganizationUserReinvite(this.organizationId, user.id);
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Reinvited User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', user.name || user.email));
this.actionPromise = null;
}
@@ -256,20 +258,6 @@ export class PeopleComponent implements OnInit {
}
}
const confirmUser = async (publicKey: Uint8Array) => {
try {
this.actionPromise = this.doConfirmation(user, publicKey);
await this.actionPromise;
updateUser(this);
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
} catch (e) {
this.validationService.showError(e);
throw e;
} finally {
this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
}
@@ -289,14 +277,9 @@ export class PeopleComponent implements OnInit {
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.onConfirmedUser.subscribe(async (publicKey: Uint8Array) => {
try {
await confirmUser(publicKey);
this.modal.close();
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
}
childComponent.onConfirmedUser.subscribe(() => {
this.modal.close();
updateUser(this);
});
this.modal.onClosed.subscribe(() => {
@@ -305,19 +288,12 @@ export class PeopleComponent implements OnInit {
return;
}
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
await confirmUser(publicKey);
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
}
this.actionPromise = this.doConfirmation(user);
await this.actionPromise;
updateUser(this);
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
this.actionPromise = null;
}
async events(user: OrganizationUserUserDetailsResponse) {
@@ -358,8 +334,15 @@ export class PeopleComponent implements OnInit {
return !searching && this.users && this.users.length > this.pageSize;
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) {
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;

View File

@@ -12,8 +12,8 @@ import {
import { PolicyType } from 'jslib/enums/policyType';
import { EnvironmentService } from 'jslib/abstractions';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -51,7 +51,7 @@ export class PoliciesComponent implements OnInit {
private router: Router, private environmentService: EnvironmentService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.usePolicies) {
@@ -102,25 +102,11 @@ export class PoliciesComponent implements OnInit {
enabled: false,
display: true,
},
{
name: this.i18nService.t('disableSend'),
description: this.i18nService.t('disableSendPolicyDesc'),
type: PolicyType.DisableSend,
enabled: false,
display: true,
},
{
name: this.i18nService.t('sendOptions'),
description: this.i18nService.t('sendOptionsPolicyDesc'),
type: PolicyType.SendOptions,
enabled: false,
display: true,
},
];
await this.load();
// Handle policies component launch from Event message
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.policyId != null) {
const policyIdFromEvents: string = qParams.policyId;
for (const orgPolicy of this.orgPolicies) {
@@ -154,10 +140,10 @@ export class PoliciesComponent implements OnInit {
async load() {
const response = await this.apiService.getPolicies(this.organizationId);
this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : [];
this.orgPolicies.forEach(op => {
this.orgPolicies.forEach((op) => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.policies.forEach(p => {
this.policies.forEach((p) => {
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
});
this.loading = false;

View File

@@ -32,12 +32,6 @@
<app-callout type="warning" *ngIf="type === policyType.PersonalOwnership">
{{'personalOwnershipExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.DisableSend">
{{'disableSendExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
{{'sendOptionsExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
@@ -150,14 +144,6 @@
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.SendOptions">
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail" [(ngModel)]="sendDisableHideEmail"
name="SendDisableHideEmail">
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -37,6 +38,7 @@ export class PolicyEditComponent implements OnInit {
defaultTypes: any[];
// Master password
masterPassMinComplexity?: number = null;
masterPassMinLength?: number;
masterPassRequireUpper?: number;
@@ -45,6 +47,7 @@ export class PolicyEditComponent implements OnInit {
masterPassRequireSpecial?: number;
// Password generator
passGenDefaultType?: string;
passGenMinLength?: number;
passGenUseUpper?: boolean;
@@ -57,13 +60,10 @@ export class PolicyEditComponent implements OnInit {
passGenCapitalize?: boolean;
passGenIncludeNumber?: boolean;
// Send options
sendDisableHideEmail?: boolean;
private policy: PolicyResponse;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) {
private analytics: Angulartics2, private toasterService: ToasterService) {
this.passwordScores = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('weak') + ' (0)', value: 0 },
@@ -113,9 +113,6 @@ export class PolicyEditComponent implements OnInit {
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
break;
case PolicyType.SendOptions:
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
break;
default:
break;
}
@@ -162,17 +159,13 @@ export class PolicyEditComponent implements OnInit {
requireSpecial: this.masterPassRequireSpecial,
};
break;
case PolicyType.SendOptions:
request.data = {
disableHideEmail: this.sendDisableHideEmail,
};
break;
default:
break;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Edited Policy' });
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
this.onSavedPolicy.emit();
} catch { }

View File

@@ -178,15 +178,6 @@
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageResetPassword"
id="manageResetPassword" [(ngModel)]="permissions.manageResetPassword">
<label class="form-check-label font-weight-normal" for="manageResetPassword">
{{'manageResetPassword' | i18n}}
</label>
</div>
</div>
</div>
</div>
</div>
@@ -264,9 +255,8 @@
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -53,8 +54,8 @@ export class UserAddEditComponent implements OnInit {
}
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.editMode = this.loading = this.organizationUserId != null;
@@ -71,8 +72,8 @@ export class UserAddEditComponent implements OnInit {
this.permissions = user.permissions;
}
if (user.collections != null && this.collections != null) {
user.collections.forEach(s => {
const collection = this.collections.filter(c => c.id === s.id);
user.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id);
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
@@ -90,7 +91,7 @@ export class UserAddEditComponent implements OnInit {
async loadCollections() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map(r =>
const collections = response.data.map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
}
@@ -103,7 +104,7 @@ export class UserAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.collections.forEach(c => this.check(c, select));
this.collections.forEach((c) => this.check(c, select));
}
setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) {
@@ -137,17 +138,14 @@ export class UserAddEditComponent implements OnInit {
p.manageUsers = clearPermissions ?
false :
this.permissions.manageUsers;
p.manageResetPassword = clearPermissions ?
false :
this.permissions.manageResetPassword;
return p;
}
async submit() {
let collections: SelectionReadOnlyRequest[] = null;
if (this.access !== 'all') {
collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -169,6 +167,7 @@ export class UserAddEditComponent implements OnInit {
this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited User' : 'Invited User' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSavedUser.emit();
@@ -190,6 +189,7 @@ export class UserAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteOrganizationUser(this.organizationId, this.organizationUserId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted User' });
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
this.onDeletedUser.emit();
} catch { }

View File

@@ -1,6 +1,6 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{'confirmUser' | i18n}}

View File

@@ -6,12 +6,18 @@ import {
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { Utils } from 'jslib/misc/utils';
@Component({
@@ -28,11 +34,13 @@ export class UserConfirmComponent implements OnInit {
dontAskAgain = false;
loading = true;
fingerprint: string;
formPromise: Promise<any>;
private publicKey: Uint8Array = null;
constructor(private apiService: ApiService, private cryptoService: CryptoService,
private storageService: StorageService) { }
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private storageService: StorageService) { }
async ngOnInit() {
try {
@@ -57,6 +65,20 @@ export class UserConfirmComponent implements OnInit {
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
}
this.onConfirmedUser.emit(this.publicKey);
try {
this.formPromise = this.doConfirmation();
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.name));
this.onConfirmedUser.emit();
} catch { }
}
private async doConfirmation() {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, this.publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(this.organizationId, this.organizationUserId, request);
}
}

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -31,11 +32,11 @@ export class UserGroupsComponent implements OnInit {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService) { }
async ngOnInit() {
const groupsResponse = await this.apiService.getGroups(this.organizationId);
const groups = groupsResponse.data.map(r => r);
const groups = groupsResponse.data.map((r) => r);
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groups;
@@ -43,8 +44,8 @@ export class UserGroupsComponent implements OnInit {
const userGroups = await this.apiService.getOrganizationUserGroups(
this.organizationId, this.organizationUserId);
if (userGroups != null && this.groups != null) {
userGroups.forEach(ug => {
const group = this.groups.filter(g => g.id === ug);
userGroups.forEach((ug) => {
const group = this.groups.filter((g) => g.id === ug);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
}
@@ -63,17 +64,18 @@ export class UserGroupsComponent implements OnInit {
}
selectAll(select: boolean) {
this.groups.forEach(g => this.check(g, select));
this.groups.forEach((g) => this.check(g, select));
}
async submit() {
const request = new OrganizationUserUpdateGroupsRequest();
request.groupIds = this.groups.filter(g => (g as any).checked).map(g => g.id);
request.groupIds = this.groups.filter((g) => (g as any).checked).map((g) => g.id);
try {
this.formPromise = this.apiService.putOrganizationUserGroups(this.organizationId, this.organizationUserId,
request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Edited User Groups' });
this.toasterService.popAsync('success', null, this.i18nService.t('editedGroupsForUser', this.name));
this.onSavedUser.emit();
} catch { }

View File

@@ -7,6 +7,7 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -45,12 +46,13 @@ export class AccountComponent {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
@@ -71,6 +73,7 @@ export class AccountComponent {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Settings' });
this.toasterService.popAsync('success', null, this.i18nService.t('organizationUpdated'));
} catch { }
}
@@ -78,6 +81,7 @@ export class AccountComponent {
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Tax Info' });
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
}

View File

@@ -12,6 +12,7 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -38,8 +39,8 @@ export class AdjustSeatsComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private router: Router,
private activatedRoute: ActivatedRoute) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private router: Router, private activatedRoute: ActivatedRoute) { }
async submit() {
try {
@@ -62,6 +63,7 @@ export class AdjustSeatsComponent {
};
this.formPromise = action();
await this.formPromise;
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
this.onAdjusted.emit(this.seatAdjustment);
if (paymentFailed) {
this.toasterService.popAsync({

View File

@@ -28,6 +28,7 @@ export class ChangePlanComponent {
async submit() {
try {
this.platformUtilsService.eventTrack('Changed Plan');
this.onChanged.emit();
} catch { }
}

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -20,8 +21,8 @@ export class DeleteOrganizationComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private cryptoService: CryptoService,
private router: Router) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -35,6 +36,7 @@ export class DeleteOrganizationComponent {
try {
this.formPromise = this.apiService.deleteOrganization(this.organizationId, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deleted Organization' });
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
this.i18nService.t('organizationDeletedDesc'));
this.router.navigate(['/']);

View File

@@ -32,6 +32,7 @@ export class DownloadLicenseComponent {
const license = await this.formPromise;
const licenseString = JSON.stringify(license, null, 2);
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
this.platformUtilsService.eventTrack('Downloaded License');
this.onDownloaded.emit();
} catch { }
}

View File

@@ -5,6 +5,7 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -17,13 +18,14 @@ import { UserBillingComponent } from '../../settings/user-billing.component';
templateUrl: '../../settings/user-billing.component.html',
})
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
constructor(apiService: ApiService, i18nService: I18nService, toasterService: ToasterService,
constructor(apiService: ApiService, i18nService: I18nService,
analytics: Angulartics2, toasterService: ToasterService,
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService) {
super(apiService, i18nService, toasterService, platformUtilsService);
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;

View File

@@ -5,6 +5,7 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
@@ -37,13 +38,14 @@ export class OrganizationSubscriptionComponent implements OnInit {
reinstatePromise: Promise<any>;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private toasterService: ToasterService,
private messagingService: MessagingService, private route: ActivatedRoute) {
private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;
@@ -73,6 +75,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
try {
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
await this.reinstatePromise;
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
this.load();
} catch { }
@@ -92,6 +95,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
try {
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
await this.cancelPromise;
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
this.load();
} catch { }

View File

@@ -16,7 +16,7 @@ export class SettingsComponent {
private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.route.parent.params.subscribe(async (params) => {
this.selfHosted = await this.platformUtilsService.isSelfHost();
const organization = await this.userService.getOrganization(params.organizationId);
this.access2fa = organization.use2fa;

View File

@@ -26,7 +26,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await super.ngOnInit();
});

View File

@@ -23,7 +23,7 @@ export class ExportComponent extends BaseExportComponent {
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
});
}

View File

@@ -13,8 +13,8 @@ import {
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
} from '../../tools/exposed-passwords-report.component';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
import { Cipher } from 'jslib/models/domain/cipher';
@Component({
selector: 'app-exposed-passwords-report',
@@ -30,7 +30,7 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
super.ngOnInit();

View File

@@ -5,11 +5,10 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ImportService } from 'jslib/abstractions/import.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { ImportComponent as BaseImportComponent } from '../../tools/import.component';
@@ -18,32 +17,17 @@ import { ImportComponent as BaseImportComponent } from '../../tools/import.compo
templateUrl: '../../tools/import.component.html',
})
export class ImportComponent extends BaseImportComponent {
organizationName: string;
constructor(i18nService: I18nService, toasterService: ToasterService,
importService: ImportService, router: Router, private route: ActivatedRoute,
platformUtilsService: PlatformUtilsService,
private userService: UserService) {
super(i18nService, toasterService, importService, router, platformUtilsService);
constructor(i18nService: I18nService, analytics: Angulartics2,
toasterService: ToasterService, importService: ImportService,
router: Router, private route: ActivatedRoute) {
super(i18nService, analytics, toasterService, importService, router);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
this.successNavigate = ['organizations', this.organizationId, 'vault'];
super.ngOnInit();
});
const organization = await this.userService.getOrganization(this.organizationId);
this.organizationName = organization.name;
}
async submit() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('importWarning', this.organizationName),
this.i18nService.t('warning'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
super.submit();
}
}

View File

@@ -26,7 +26,7 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});

View File

@@ -30,7 +30,7 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();

View File

@@ -19,7 +19,7 @@ export class ToolsComponent {
private messagingService: MessagingService) { }
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp

View File

@@ -26,7 +26,7 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});

View File

@@ -31,7 +31,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();

View File

@@ -20,13 +20,12 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/at
templateUrl: '../../vault/attachments.component.html',
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = false;
organization: Organization;
constructor(cipherService: CipherService, i18nService: I18nService,
cryptoService: CryptoService, userService: UserService,
platformUtilsService: PlatformUtilsService, apiService: ApiService) {
super(cipherService, i18nService, cryptoService, userService, platformUtilsService, apiService);
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
}
protected async reupload(attachment: AttachmentView) {

View File

@@ -5,6 +5,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
@@ -32,22 +33,22 @@ export class CiphersComponent extends BaseCiphersComponent {
protected allCiphers: CipherView[] = [];
constructor(searchService: SearchService, toasterService: ToasterService, i18nService: I18nService,
constructor(searchService: SearchService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
private apiService: ApiService, eventService: EventService, totpService: TotpService, userService: UserService) {
super(searchService, toasterService, i18nService, platformUtilsService, cipherService,
eventService, totpService, userService);
super(searchService, analytics, toasterService, i18nService, platformUtilsService,
cipherService, eventService, totpService, userService);
}
async load(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.canManageAllCollections) {
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
} else {
this.allCiphers = (await this.cipherService.getAllDecrypted()).filter(c => c.organizationId === this.organization.id);
if (!this.organization.canManageAllCollections) {
await super.load(filter, this.deleted);
return;
}
await this.searchService.indexCiphers(this.organization.id, this.allCiphers);
await this.applyFilter(filter);
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
this.applyFilter(filter);
this.loaded = true;
}
@@ -61,8 +62,28 @@ export class CiphersComponent extends BaseCiphersComponent {
}
async search(timeout: number = null) {
await super.search(timeout, this.allCiphers);
if (!this.organization.canManageAllCollections) {
return super.search(timeout);
}
this.searchPending = false;
let filteredCiphers = this.allCiphers;
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers.filter((c) => {
if (c.isDeleted !== this.deleted) {
return false;
}
return this.filter == null || this.filter(c);
});
} else {
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
}
await this.resetPaging();
}
events(c: CipherView) {
this.onEventsClicked.emit(c);
}

View File

@@ -36,7 +36,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
const collections = await this.apiService.getCollections(this.organization.id);
if (collections != null && collections.data != null && collections.data.length) {
const collectionDomains = collections.data.map(r =>
const collectionDomains = collections.data.map((r) =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collectionDomains);
} else {

View File

@@ -29,9 +29,6 @@
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="fa-warning">
{{trashCleanupWarning}}
</app-callout>
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
(onCollectionsClicked)="editCipherCollections($event)" (onEventsClicked)="viewEvents($event)"

View File

@@ -15,7 +15,6 @@ import {
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -53,7 +52,6 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionId: string = null;
type: CipherType = null;
deleted: boolean = false;
trashCleanupWarning: string = null;
modal: ModalComponent = null;
@@ -61,20 +59,15 @@ export class VaultComponent implements OnInit, OnDestroy {
private router: Router, private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService, private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService) { }
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
ngOnInit() {
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost() ? 'trashCleanupWarningSelfHosted' : 'trashCleanupWarning'
);
const queryParams = this.route.parent.params.subscribe(async params => {
const queryParams = this.route.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.groupingsComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (!this.organization.canManageAllCollections) {
await this.syncService.fullSync(false);
@@ -117,7 +110,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (qParams.viewEvents != null) {
const cipher = this.ciphersComponent.ciphers.filter(c => c.id === qParams.viewEvents);
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
if (cipher.length > 0) {
this.viewEvents(cipher[0]);
}
@@ -242,7 +235,7 @@ export class VaultComponent implements OnInit, OnDestroy {
if (this.organization.canManageAllCollections) {
childComponent.collectionIds = cipher.collectionIds;
childComponent.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
childComponent.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
childComponent.organization = this.organization;
childComponent.cipherId = cipher.id;
@@ -261,7 +254,7 @@ export class VaultComponent implements OnInit, OnDestroy {
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
if (this.collectionId != null) {
component.collectionIds = [this.collectionId];
@@ -304,7 +297,7 @@ export class VaultComponent implements OnInit, OnDestroy {
component.cloneMode = true;
component.organizationId = this.organization.id;
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
// in the add-edit componenet

View File

@@ -19,5 +19,4 @@ if (process.env.ENV === 'production') {
// Other polyfills
require('whatwg-fetch');
require('webcrypto-shim');
require('date-input-polyfill');
/* tslint:enable */

View File

@@ -1,20 +1,7 @@
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-center mt-5">
<div class="col-12">
<p class="lead text-center mb-4">Bitwarden Send</p>
</div>
<div class="col-12 text-center" *ngIf="creatorIdentifier != null">
<p>{{'sendCreatorIdentifier' | i18n: creatorIdentifier }}</p>
</div>
<div class="col-8" *ngIf="hideEmail">
<app-callout type="warning" title="{{'warning' | i18n}}">
{{'viewSendHiddenEmailWarning' | i18n }}
<a href="https://bitwarden.com/help/article/receive-send/" target="_blank">{{'learnMore' | i18n}}</a>.
</app-callout>
</div>
</div>
<div class="row justify-content-center">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">Bitwarden Send</p>
<div class="card d-block">
<div class="card-body" *ngIf="loading" class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
@@ -67,26 +54,12 @@
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<p>{{send.file.fileName}}</p>
<button class="btn btn-primary btn-block" type="button" (click)="download()" *ngIf="!downloading">
<button class="btn btn-primary btn-block" type="button" (click)="download()">
<i class="fa fa-download" aria-hidden="true"></i>
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
<button class="btn btn-primary btn-block" type="button" *ngIf="downloading" disabled="true">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</ng-container>
<p *ngIf="expirationDate" class="text-center text-muted">Expires:
{{expirationDate | date: 'medium'}}</p>
</div>
</div>
</div>
<div class="col-12 text-center mt-5 text-muted">
<p class="mb-0">{{'sendAccessTaglineProductDesc' | i18n}}<br>
{{'sendAccessTaglineLearnMore' | i18n}} <a
href="https://www.bitwarden.com/products/send?source=web-vault" target="_blank">Bitwarden Send</a>
{{'sendAccessTaglineOr' | i18n}} <a
href="https://vault.bitwarden.com/#/register" target="_blank">{{'sendAccessTaglineSignUp' | i18n}}</a>
{{'sendAccessTaglineTryToday' | i18n}}
</p>
</div>
</div>
</form>

View File

@@ -13,8 +13,8 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { Utils } from 'jslib/misc/utils';
import { SendAccess } from 'jslib/models/domain/sendAccess';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { SendAccess } from 'jslib/models/domain/sendAccess';
import { SendAccessView } from 'jslib/models/view/sendAccessView';
@@ -39,12 +39,10 @@ export class AccessComponent implements OnInit {
showText = false;
unavailable = false;
error = false;
hideEmail = false;
private id: string;
private key: string;
private decKey: SymmetricCryptoKey;
private accessRequest: SendAccessRequest;
constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
@@ -58,22 +56,8 @@ export class AccessComponent implements OnInit {
return this.showText ? this.send.text.text : this.send.text.maskedText;
}
get expirationDate() {
if (this.send == null || this.send.expirationDate == null) {
return null;
}
return this.send.expirationDate;
}
get creatorIdentifier() {
if (this.send == null || this.send.creatorIdentifier == null) {
return null;
}
return this.send.creatorIdentifier;
}
ngOnInit() {
this.route.params.subscribe(async params => {
this.route.params.subscribe(async (params) => {
this.id = params.sendId;
this.key = params.key;
if (this.key == null || this.id == null) {
@@ -92,16 +76,8 @@ export class AccessComponent implements OnInit {
return;
}
const downloadData = await this.apiService.getSendFileDownloadData(this.send, this.accessRequest);
if (Utils.isNullOrWhitespace(downloadData.url)) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('missingSendFile'));
return;
}
this.downloading = true;
const response = await fetch(new Request(downloadData.url, { cache: 'no-store' }));
const response = await fetch(new Request(this.send.file.url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
this.downloading = false;
@@ -132,19 +108,18 @@ export class AccessComponent implements OnInit {
async load() {
this.unavailable = false;
this.error = false;
this.hideEmail = false;
const keyArray = Utils.fromUrlB64ToArray(this.key);
this.accessRequest = new SendAccessRequest();
const accessRequest = new SendAccessRequest();
if (this.password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
accessRequest.password = Utils.fromBufferToB64(passwordHash);
}
try {
let sendResponse: SendAccessResponse = null;
if (this.loading) {
sendResponse = await this.apiService.postSendAccess(this.id, this.accessRequest);
sendResponse = await this.apiService.postSendAccess(this.id, accessRequest);
} else {
this.formPromise = this.apiService.postSendAccess(this.id, this.accessRequest);
this.formPromise = this.apiService.postSendAccess(this.id, accessRequest);
sendResponse = await this.formPromise;
}
this.passwordRequired = false;
@@ -164,6 +139,5 @@ export class AccessComponent implements OnInit {
}
}
this.loading = false;
this.hideEmail = this.creatorIdentifier == null && !this.passwordRequired && !this.loading && !this.unavailable;
}
}

View File

@@ -9,48 +9,32 @@
</button>
</div>
<div class="modal-body" *ngIf="send">
<app-callout *ngIf="disableSend">
<span>{{'sendDisabledWarning' | i18n}}</span>
</app-callout>
<app-callout *ngIf="!disableSend && disableHideEmail">
<span>{{'sendOptionsPolicyInEffect' | i18n}}</span>
<ul class="mb-0">
<li>{{'sendDisableHideEmailInEffect' | i18n}}</li>
</ul>
</app-callout>
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label for="type">{{'whatTypeOfSend' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="send.type" class="form-control" appAutofocus
(change)='typeChanged()'>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required
[readOnly]="disableSend">
<small class="form-text text-muted">{{'sendNameDesc' | i18n}}</small>
</div>
</div>
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label>{{'whatTypeOfSend' | i18n}}</label>
<div class="form-check" *ngFor="let o of typeOptions">
<input class="form-check-input" type="radio" [(ngModel)]="send.type" name="Type_{{o.value}}"
id="type_{{o.value}}" [value]="o.value" (change)="typeChanged(o)"
[checked]="send.type === o.value">
<label class="form-check-label" for="type_{{o.value}}">
{{o.name}}
</label>
</div>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{'sendTypeText' | i18n}}</label>
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text" class="form-control"
[readOnly]="disableSend"></textarea>
<small class="form-text text-muted">{{'sendTextDesc' | i18n}}</small>
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text"
class="form-control"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
id="text-hidden" name="Text.Hidden" [disabled]="disableSend">
id="text-hidden" name="Text.Hidden">
<label class="form-check-label" for="text-hidden">{{'textHiddenByDefault' | i18n}}</label>
</div>
</div>
@@ -64,178 +48,96 @@
</div>
<div *ngIf="!editMode">
<label for="file">{{'file' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required
[disabled]="disableSend">
<small class="form-text text-muted">{{'sendFileDesc' | i18n}} {{'maxFileSize' |
i18n}}</small>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
</div>
</div>
</ng-container>
<h3 class="mt-5">{{'share' | i18n}}</h3>
<div class="form-group" *ngIf="link">
<label for="link">{{'sendLinkLabel' | i18n}}</label>
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
<h3 class="mt-5">{{'options' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<div *ngIf="!editMode">
<select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect"
class="form-control" required>
<option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
<input id="deletionDateCustom" class="form-control mt-1" type="datetime-local"
name="DeletionDate" [(ngModel)]="deletionDate" required *ngIf="deletionDateSelect === 0"
placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div *ngIf="editMode">
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
[(ngModel)]="deletionDate" required placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div class="form-text text-muted small">{{'deletionDateDesc' | i18n}}</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto" *ngIf="editMode">
{{'clear' | i18n}}
</a>
</div>
<div *ngIf="!editMode">
<select id="expirationDate" name="ExpirationDateSelect" [(ngModel)]="expirationDateSelect"
class="form-control" required>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<input id="expirationDateCustom" class="form-control mt-1" type="datetime-local"
name="ExpirationDate" [(ngModel)]="expirationDate" required
*ngIf="expirationDateSelect === 0" placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div *ngIf="editMode">
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
[(ngModel)]="expirationDate" placeholder="MM/DD/YYYY HH:MM AM/PM">
</div>
<div class="form-text text-muted small">{{'expirationDateDesc' | i18n}}</div>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount" min="1">
<div class="form-text text-muted small">{{'maxAccessCountDesc' | i18n}}</div>
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" class="form-control" type="number" name="AccessCount" readonly
[(ngModel)]="send.accessCount">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
<input id="password" class="form-control" type="password" name="Password"
[(ngModel)]="password">
<div class="form-text text-muted small">{{'sendPasswordDesc' | i18n}}</div>
</div>
</div>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"></textarea>
<div class="form-text text-muted small">{{'sendNotesDesc' | i18n}}</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="copyLink" id="copy-link"
name="CopyLink">
<label class="form-check-label" for="copy-link">{{'copySendLinkOnSave' | i18n}}</label>
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
name="Disabled">
<label class="form-check-label" for="disabled">{{'disableThisSend' | i18n}}</label>
</div>
</div>
<div id="options-header" class="section-header d-flex flex-row align-items-center mt-5"
(click)="toggleOptions()">
<h3 class="mb-0 mr-2">{{'options' | i18n}}</h3>
<a class="mb-1" href="#" appStopClick role="button">
<i class="fa" aria-hidden="true"
[ngClass]="{'fa-chevron-down': !showOptions, 'fa-chevron-up': showOptions}"></i>
</a>
</div>
<div id="options" [hidden]="!showOptions">
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<ng-template #deletionDateCustom>
<ng-container *ngIf="isDateTimeLocalSupported">
<input id="deletionDateCustom" class="form-control mt-1" type="datetime-local"
name="DeletionDate" [(ngModel)]="deletionDate" required
placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
</ng-container>
<div *ngIf="!isDateTimeLocalSupported" class="d-flex justify-content-around">
<input id="deletionDateCustomFallback" class="form-control mt-1" type="date"
name="DeletionDateFallback" [(ngModel)]="deletionDateFallback" required
placeholder="MM/DD/YYYY" [readOnly]="disableSend" data-date-format="mm/dd/yyyy">
<select *ngIf="isSafari" id="deletionTimeCustomFallback" class="form-control mt-1 ml-1" [required]="!editMode"
[(ngModel)]="safariDeletionTime" name="SafariDeletionTime">
<option *ngFor="let o of safariDeletionTimeOptions" [value]="o.military">{{o.standard}}</option>
</select>
<input *ngIf="!isSafari" id="deletionTimeCustomFallback" class="form-control mt-1 ml-1" type="time"
name="DeletionTimeDate" [(ngModel)]="deletionTimeFallback" required
placeholder="HH:MM AM/PM" [readOnly]="disableSend">
</div>
</ng-template>
<div *ngIf="!editMode">
<select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect"
class="form-control" required>
<option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<ng-container *ngIf="deletionDateSelect === 0">
<ng-container *ngTemplateOutlet="deletionDateCustom">
</ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="deletionDateCustom">
</ng-container>
</div>
<div class="form-text text-muted small">{{'deletionDateDesc' | i18n}}</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto"
*ngIf="editMode && !disableSend">
{{'clear' | i18n}}
</a>
</div>
<ng-template #expirationDateCustom>
<ng-container *ngIf="isDateTimeLocalSupported">
<input id="expirationDateCustom" class="form-control mt-1" type="datetime-local"
name="ExpirationDate" [(ngModel)]="expirationDate" placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
</ng-container>
<div class="d-flex justify-content-around" *ngIf="!isDateTimeLocalSupported">
<input id="expirationDateCustomFallback" class="form-control mt-1" type="date"
name="ExpirationDateFallback" [(ngModel)]="expirationDateFallback" [required]="!editMode"
placeholder="MM/DD/YYYY" [readOnly]="disableSend" data-date-format="mm/dd/yyyy" (change)="expirationDateFallbackChanged()">
<select *ngIf="isSafari" id="expirationTimeCustomFallback" class="form-control mt-1 ml-1" [required]="!editMode"
[(ngModel)]="safariExpirationTime" name="SafariExpirationTime">
<option *ngFor="let o of safariExpirationTimeOptions" [ngValue]="o.military">{{o.standard}}</option>
</select>
<input *ngIf="!isSafari" id="expirationTimeCustomFallback" class="form-control mt-1 ml-1" type="time"
name="ExpirationTimeFallback" [(ngModel)]="expirationTimeFallback" [required]="!editMode"
placeholder="HH:MM AM/PM" [readOnly]="disableSend">
</div>
</ng-template>
<div *ngIf="!editMode">
<select id="expirationDate" name="ExpirationDateSelect"
[(ngModel)]="expirationDateSelect" class="form-control" required>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<ng-container *ngIf="expirationDateSelect === 0">
<ng-container *ngTemplateOutlet="expirationDateCustom">
</ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="expirationDateCustom">
</ng-container>
</div>
<div class="form-text text-muted small">{{'expirationDateDesc' | i18n}}</div>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount" min="1" [readOnly]="disableSend">
<div class="form-text text-muted small">{{'maxAccessCountDesc' | i18n}}</div>
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" class="form-control" type="text" name="AccessCount" readonly
[(ngModel)]="send.accessCount">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
<div class="input-group">
<input id="password" class="form-control text-monospace"
type="{{showPassword ? 'text' : 'password'}}" name="Password" [(ngModel)]="password"
[readOnly]="disableSend">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePasswordVisible()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-text text-muted small">{{'sendPasswordDesc' | i18n}}</div>
</div>
</div>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"
[readOnly]="disableSend"></textarea>
<div class="form-text text-muted small">{{'sendNotesDesc' | i18n}}</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.hideEmail" id="hideEmail"
name="HideEmail" [disabled]="(disableHideEmail && !send.hideEmail) || disableSend">
<label class="form-check-label" for="hideEmail">
{{'hideEmail' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
name="Disabled" [disabled]="disableSend">
<label class="form-check-label" for="disabled">{{'disableThisSend' | i18n}}</label>
</div>
</div>
<h3 class="mt-5" *ngIf="link">{{'share' | i18n}}</h3>
<div class="form-group" *ngIf="link">
<label for="link">{{'sendLink' | i18n}}</label>
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary disabled" disabled=true *ngIf="disableSend">
<span>{{'save' | i18n}}</span>
</button>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="!disableSend">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>

View File

@@ -1,33 +1,236 @@
import { DatePipe } from '@angular/common';
import {
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component } from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SendService } from 'jslib/abstractions/send.service';
import { UserService } from 'jslib/abstractions/user.service';
import { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/send/add-edit.component';
import { SendFileView } from 'jslib/models/view/sendFileView';
import { SendTextView } from 'jslib/models/view/sendTextView';
import { SendView } from 'jslib/models/view/sendView';
import { Send } from 'jslib/models/domain/send';
@Component({
selector: 'app-send-add-edit',
templateUrl: 'add-edit.component.html',
})
export class AddEditComponent extends BaseAddEditComponent {
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, datePipe: DatePipe,
sendService: SendService, userService: UserService,
messagingService: MessagingService, policyService: PolicyService) {
super(i18nService, platformUtilsService, environmentService, datePipe, sendService, userService,
messagingService, policyService);
export class AddEditComponent {
@Input() sendId: string;
@Input() type: SendType;
@Output() onSavedSend = new EventEmitter<SendView>();
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
editMode: boolean = false;
send: SendView;
link: string;
title: string;
deletionDate: string;
expirationDate: string;
hasPassword: boolean;
password: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
sendType = SendType;
typeOptions: any[];
deletionDateOptions: any[];
expirationDateOptions: any[];
deletionDateSelect = 168;
expirationDateSelect: number = null;
canAccessPremium = true;
premiumRequiredAlertShown = false;
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService, private datePipe: DatePipe,
private sendService: SendService, private userService: UserService,
private messagingService: MessagingService) {
this.typeOptions = [
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
];
this.deletionDateOptions = this.expirationDateOptions = [
{ name: i18nService.t('oneHour'), value: 1 },
{ name: i18nService.t('oneDay'), value: 24 },
{ name: i18nService.t('days', '2'), value: 48 },
{ name: i18nService.t('days', '3'), value: 72 },
{ name: i18nService.t('days', '7'), value: 168 },
{ name: i18nService.t('days', '30'), value: 720 },
{ name: i18nService.t('custom'), value: 0 },
];
this.expirationDateOptions = [
{ name: i18nService.t('never'), value: null },
].concat([...this.deletionDateOptions]);
}
copyLinkToClipboard(link: string) {
// Copy function on web depends on the modal being open or not. Since this event occurs during a transition
// of the modal closing we need to add a small delay to make sure state of the DOM is consistent.
window.setTimeout(() => super.copyLinkToClipboard(link), 500);
async ngOnInit() {
await this.load();
}
async load() {
this.editMode = this.sendId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editSend');
} else {
this.title = this.i18nService.t('createSend');
}
this.canAccessPremium = await this.userService.canAccessPremium();
if (!this.canAccessPremium) {
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() !== '';
// Parse dates
this.deletionDate = this.dateToString(this.send.deletionDate);
this.expirationDate = this.dateToString(this.send.expirationDate);
if (this.editMode) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
}
}
async submit(): Promise<boolean> {
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 (file.size > 104857600) { // 100 MB
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('maxFileSize'));
return;
}
}
if (!this.editMode) {
const now = new Date();
if (this.deletionDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.deletionDateSelect);
this.deletionDate = this.dateToString(d);
}
if (this.expirationDateSelect != null && this.expirationDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.expirationDateSelect);
this.expirationDate = this.dateToString(d);
}
}
const encSend = await this.encryptSend(file);
try {
this.formPromise = this.sendService.saveWithServer(encSend);
await this.formPromise;
this.send.id = encSend[0].id;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
this.onSavedSend.emit(this.send);
return true;
} catch { }
return false;
}
clearExpiration() {
this.expirationDate = null;
}
async delete(): Promise<void> {
if (this.deletePromise != null) {
return;
}
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;
}
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);
} catch { }
}
typeChanged() {
if (!this.canAccessPremium && this.send.type === SendType.File && !this.premiumRequiredAlertShown) {
this.premiumRequiredAlertShown = true;
this.messagingService.send('premiumRequired');
}
}
protected async loadSend(): Promise<Send> {
return this.sendService.get(this.sendId);
}
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
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 dateToString(d: Date) {
return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm');
}
}

View File

@@ -1,12 +1,4 @@
<div class="container page-content">
<div class="row card border-warning mb-4" *ngIf="disableSend">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{'sendDisabled' | i18n}}
</div>
<div class="card-body">
<span>{{'sendDisabledWarning' | i18n}}</span>
</div>
</div>
<div class="row">
<div class="col-3 groupings">
<div class="card vault-filters">
@@ -43,7 +35,7 @@
<div class="col-9">
<div class="page-header d-flex">
<h1>
{{'send' | i18n}}
Send
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
@@ -53,8 +45,7 @@
</small>
</h1>
<div class="ml-auto d-flex">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()"
[disabled]="disableSend">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'createSend' | i18n}}
</button>
</div>
@@ -71,11 +62,9 @@
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{s.name}}</a>
<ng-container *ngIf="s.disabled">
<i class="fa fa-warning" appStopProp title="{{'disabled' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'disabled' | i18n}}</span>
</ng-container>
<span appStopClick class="badge badge-secondary" *ngIf="s.disabled">
{{'disabled' | i18n}}
</span>
<ng-container *ngIf="s.password">
<i class="fa fa-key" appStopProp title="{{'password' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'password' | i18n}}</span>
@@ -86,8 +75,7 @@
<span class="sr-only">{{'maxAccessCountReached' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o" appStopProp title="{{'expired' | i18n}}"
aria-hidden="true"></i>
<i class="fa fa-clock-o" appStopProp title="{{'expired' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'expired' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
@@ -111,7 +99,7 @@
{{'copySendLink' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="removePassword(s)"
*ngIf="s.password && !disableSend">
*ngIf="s.password">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'removePassword' | i18n}}
</a>
@@ -132,7 +120,7 @@
</ng-container>
<ng-container *ngIf="loaded">
<p>{{'noSendsInList' | i18n}}</p>
<button (click)="addSend()" class="btn btn-outline-primary" [disabled]="disableSend">
<button (click)="addSend()" class="btn btn-outline-primary">
<i class="fa fa-plus fa-fw"></i>{{'createSend' | i18n}}</button>
</ng-container>
</div>

View File

@@ -1,75 +1,105 @@
import {
Component,
ComponentFactoryResolver,
NgZone,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { SendView } from 'jslib/models/view/sendView';
import { SendType } from 'jslib/enums/sendType';
import { SendComponent as BaseSendComponent } from 'jslib/angular/components/send/send.component';
import { SendView } from 'jslib/models/view/sendView';
import { AddEditComponent } from './add-edit.component';
import { ModalComponent } from '../modal.component';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { SendService } from 'jslib/abstractions/send.service';
import { UserService } from 'jslib/abstractions/user.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
const BroadcasterSubscriptionId = 'SendComponent';
@Component({
selector: 'app-send',
templateUrl: 'send.component.html',
})
export class SendComponent extends BaseSendComponent {
export class SendComponent implements OnInit {
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
modal: ModalComponent = null;
sendType = SendType;
loaded = false;
loading = true;
refreshing = false;
expired: boolean = false;
type: SendType = null;
sends: SendView[] = [];
filteredSends: SendView[] = [];
searchText: string;
selectedType: SendType;
selectedAll: boolean;
searchPlaceholder: string;
filter: (cipher: SendView) => boolean;
searchPending = false;
constructor(sendService: SendService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
ngZone: NgZone, searchService: SearchService, policyService: PolicyService, userService: UserService,
private componentFactoryResolver: ComponentFactoryResolver, private broadcasterService: BroadcasterService) {
super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService,
policyService, userService);
}
modal: ModalComponent = null;
actionPromise: any;
private searchTimeout: any;
constructor(private apiService: ApiService, private sendService: SendService,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
async ngOnInit() {
await super.ngOnInit();
await this.load();
// Broadcaster subscription - load if sync completes in the background
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncCompleted':
if (message.successfully) {
await this.load();
}
break;
}
});
});
}
async load(filter: (send: SendView) => boolean = null) {
this.loading = true;
const sends = await this.sendService.getAllDecrypted();
this.sends = sends;
this.selectAll();
this.loading = false;
this.loaded = true;
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
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.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.searchPending = false;
}, timeout);
}
addSend() {
if (this.disableSend) {
return;
}
const component = this.editSend(null);
component.type = this.type;
}
@@ -100,4 +130,78 @@ export class SendComponent extends BaseSendComponent {
return childComponent;
}
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;
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
await this.load();
} catch { }
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;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
} catch { }
this.actionPromise = null;
return true;
}
copy(s: SendView) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
const link = webVaultUrl + '/#/send/' + 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;
}
}

View File

@@ -155,13 +155,6 @@ export class EventService {
break;
case EventType.OrganizationUser_UnlinkedSso:
msg = this.i18nService.t('unlinkedSsoUser', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_ResetPassword_Enroll:
msg = this.i18nService.t('eventEnrollPasswordReset', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_ResetPassword_Withdraw:
msg = this.i18nService.t('eventWithdrawPasswordReset', this.formatOrgUserId(ev));
break;
// Org
case EventType.Organization_Updated:
msg = this.i18nService.t('editedOrgSettings');

View File

@@ -16,7 +16,7 @@ export class RouterService {
constructor(private router: Router, private activatedRoute: ActivatedRoute,
private titleService: Title, i18nService: I18nService) {
this.currentUrl = this.router.url;
router.events.subscribe(event => {
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.previousUrl = this.currentUrl;
this.currentUrl = event.url;

View File

@@ -16,13 +16,14 @@ import { EventService } from './event.service';
import { OrganizationGuardService } from './organization-guard.service';
import { OrganizationTypeGuardService } from './organization-type-guard.service';
import { RouterService } from './router.service';
import { UnauthGuardService } from './unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { LockGuardService } from 'jslib/angular/services/lock-guard.service';
import { UnauthGuardService } from 'jslib/angular/services/unauth-guard.service';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { Analytics } from 'jslib/misc/analytics';
import { ApiService } from 'jslib/services/api.service';
import { AppIdService } from 'jslib/services/appId.service';
import { AuditService } from 'jslib/services/audit.service';
@@ -36,7 +37,6 @@ import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
import { EventService as EventLoggingService } from 'jslib/services/event.service';
import { ExportService } from 'jslib/services/export.service';
import { FileUploadService } from 'jslib/services/fileUpload.service';
import { FolderService } from 'jslib/services/folder.service';
import { ImportService } from 'jslib/services/import.service';
import { NotificationsService } from 'jslib/services/notifications.service';
@@ -64,7 +64,6 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstractions/environment.service';
import { EventService as EventLoggingServiceAbstraction } from 'jslib/abstractions/event.service';
import { ExportService as ExportServiceAbstraction } from 'jslib/abstractions/export.service';
import { FileUploadService as FileUploadServiceAbstraction } from 'jslib/abstractions/fileUpload.service';
import { FolderService as FolderServiceAbstraction } from 'jslib/abstractions/folder.service';
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/import.service';
@@ -107,15 +106,14 @@ const apiService = new ApiService(tokenService, platformUtilsService,
const userService = new UserService(tokenService, storageService);
const settingsService = new SettingsService(userService, storageService);
export let searchService: SearchService = null;
const fileUploadService = new FileUploadService(consoleLogService, apiService);
const cipherService = new CipherService(cryptoService, userService, settingsService,
apiService, fileUploadService, storageService, i18nService, () => searchService);
apiService, storageService, i18nService, () => searchService);
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService);
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService, consoleLogService, i18nService);
searchService = new SearchService(cipherService, consoleLogService);
const policyService = new PolicyService(userService, storageService);
const sendService = new SendService(cryptoService, userService, apiService, fileUploadService, storageService,
const sendService = new SendService(cryptoService, userService, apiService, storageService,
i18nService, cryptoFunctionService);
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
@@ -130,33 +128,42 @@ const authService = new AuthService(cryptoService, apiService,
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
consoleLogService);
const exportService = new ExportService(folderService, cipherService, apiService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService,
platformUtilsService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
const notificationsService = new NotificationsService(userService, syncService, appIdService,
apiService, vaultTimeoutService, async () => messagingService.send('logout', { expired: true }), consoleLogService);
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
const eventLoggingService = new EventLoggingService(storageService, apiService, userService, cipherService);
const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
export function initFactory(): Function {
return async () => {
await (storageService as HtmlStorageService).init();
const isDev = platformUtilsService.isDev();
if (isDev || platformUtilsService.isSelfHost()) {
if (!isDev && platformUtilsService.isSelfHost()) {
environmentService.baseUrl = window.location.origin;
} else {
environmentService.notificationsUrl = 'https://notifications.bitwarden.com';
environmentService.enterpriseUrl = 'https://portal.bitwarden.com';
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
}
apiService.setUrls({
base: window.location.origin,
api: null,
identity: null,
events: null,
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,
identity: isDev ? 'http://localhost:33656' : null,
events: isDev ? 'http://localhost:46273' : null,
// Uncomment these (and comment out the above) if you want to target production
// servers for local development.
// base: null,
// api: 'https://api.bitwarden.com',
// identity: 'https://identity.bitwarden.com',
// events: 'https://events.bitwarden.com',
});
setTimeout(() => notificationsService.init(environmentService), 3000);
@@ -191,7 +198,6 @@ export function initFactory(): Function {
UnauthGuardService,
RouterService,
EventService,
LockGuardService,
{ provide: AuditServiceAbstraction, useValue: auditService },
{ provide: AuthServiceAbstraction, useValue: authService },
{ provide: CipherServiceAbstraction, useValue: cipherService },
@@ -205,7 +211,6 @@ export function initFactory(): Function {
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
{ provide: ApiServiceAbstraction, useValue: apiService },
{ provide: FileUploadServiceAbstraction, useValue: fileUploadService },
{ provide: SyncServiceAbstraction, useValue: syncService },
{ provide: UserServiceAbstraction, useValue: userService },
{ provide: MessagingServiceAbstraction, useValue: messagingService },

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import {
CanActivate,
Router,
} from '@angular/router';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
@Injectable()
export class UnauthGuardService implements CanActivate {
constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService,
private router: Router) { }
async canActivate() {
const isAuthed = await this.userService.isAuthenticated();
if (isAuthed) {
const locked = await this.vaultTimeoutService.isLocked();
if (locked) {
this.router.navigate(['lock']);
} else {
this.router.navigate(['vault']);
}
return false;
}
return true;
}
}

View File

@@ -8,6 +8,9 @@ import {
ViewChild,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -46,6 +49,7 @@ export class AddCreditComponent implements OnInit {
private email: string;
constructor(private userService: UserService, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService) {
if (platformUtilsService.isDev()) {
this.ppButtonFormAction = WebConstants.paypal.buttonActionSandbox;
@@ -104,6 +108,9 @@ export class AddCreditComponent implements OnInit {
return;
}
try {
this.analytics.eventTrack.next({
action: 'Added Credit',
});
this.onAdded.emit();
} catch { }
}

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -35,12 +36,12 @@ export class AdjustPaymentComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService) { }
async submit() {
try {
const request = new PaymentRequest();
this.formPromise = this.paymentComponent.createPaymentToken().then(result => {
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
@@ -58,6 +59,9 @@ export class AdjustPaymentComponent {
}
});
await this.formPromise;
this.analytics.eventTrack.next({
action: this.currentType == null ? 'Added Payment Method' : 'Changed Payment Method',
});
this.toasterService.popAsync('success', null, this.i18nService.t('updatedPaymentMethod'));
this.onAdjusted.emit();
} catch { }

View File

@@ -12,6 +12,7 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -40,8 +41,8 @@ export class AdjustStorageComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private router: Router,
private activatedRoute: ActivatedRoute) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private router: Router, private activatedRoute: ActivatedRoute) { }
async submit() {
try {
@@ -70,6 +71,7 @@ export class AdjustStorageComponent {
};
this.formPromise = action();
await this.formPromise;
this.analytics.eventTrack.next({ action: this.add ? 'Added Storage' : 'Removed Storage' });
this.onAdjusted.emit(this.storageAdjustment);
if (paymentFailed) {
this.toasterService.popAsync({

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -29,8 +30,8 @@ export class ApiKeyComponent {
clientId: string;
clientSecret: string;
constructor(private i18nService: I18nService, private toasterService: ToasterService,
private cryptoService: CryptoService) { }
constructor(private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -46,6 +47,7 @@ export class ApiKeyComponent {
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = `${this.keyType}.${this.entityId}`;
this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` });
} catch { }
}
}

View File

@@ -3,6 +3,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -26,8 +27,9 @@ export class ChangeEmailComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private cryptoService: CryptoService,
private messagingService: MessagingService, private userService: UserService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService) { }
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
@@ -61,6 +63,7 @@ export class ChangeEmailComponent {
this.formPromise = this.apiService.postEmail(request);
await this.formPromise;
this.reset();
this.analytics.eventTrack.next({ action: 'Changed Email' });
this.toasterService.popAsync('success', this.i18nService.t('emailChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');

View File

@@ -25,7 +25,7 @@
<div class="col-6">
<div class="form-group mb-0">
<label for="kdfIterations">{{'kdfIterations' | i18n}}</label>
<a class="ml-auto" href="https://bitwarden.com/help/article/what-encryption-is-used/#pbkdf2" target="_blank" rel="noopener"
<a class="ml-auto" href="https://en.wikipedia.org/wiki/PBKDF2" target="_blank" rel="noopener"
appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>

View File

@@ -4,6 +4,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -27,8 +28,9 @@ export class ChangeKdfComponent implements OnInit {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private cryptoService: CryptoService,
private messagingService: MessagingService, private userService: UserService) {
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService) {
this.kdfOptions = [
{ name: 'PBKDF2 SHA-256', value: KdfType.PBKDF2_SHA256 },
];
@@ -58,6 +60,7 @@ export class ChangeKdfComponent implements OnInit {
try {
this.formPromise = this.apiService.postAccountKdf(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Changed KDF' });
this.toasterService.popAsync('success', this.i18nService.t('encKeySettingsChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');

View File

@@ -51,7 +51,7 @@
<label class="form-check-label" for="rotateEncKey">
{{'rotateAccountEncKey' | i18n}}
</label>
<a href="https://bitwarden.com/help/article/account-encryption-key/#rotate-your-encryption-key"
<a href="https://help.bitwarden.com/article/change-your-master-password/#rotating-your-accounts-encryption-key"
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>

View File

@@ -19,13 +19,12 @@ import {
import { EmergencyAccessStatusType } from 'jslib/enums/emergencyAccessStatusType';
import { Utils } from 'jslib/misc/utils';
import { EncString } from 'jslib/models/domain/encString';
import { CipherString } from 'jslib/models/domain/cipherString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
import { EmergencyAccessUpdateRequest } from 'jslib/models/request/emergencyAccessUpdateRequest';
import { FolderWithIdRequest } from 'jslib/models/request/folderWithIdRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
import { PasswordRequest } from 'jslib/models/request/passwordRequest';
import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
@@ -42,7 +41,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
private folderService: FolderService, private cipherService: CipherService,
private syncService: SyncService, private apiService: ApiService) {
private syncService: SyncService, private apiService: ApiService ) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService);
}
@@ -108,7 +107,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
newEncKey: [SymmetricCryptoKey, EncString]) {
newEncKey: [SymmetricCryptoKey, CipherString]) {
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
request.newMasterPasswordHash = newMasterPasswordHash;
@@ -136,7 +135,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
let encPrivateKey: CipherString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
}
@@ -167,8 +166,6 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
await this.apiService.postAccountKey(request);
await this.updateEmergencyAccesses(encKey[0]);
await this.updateAllResetPasswordKeys(encKey[0]);
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
@@ -195,25 +192,4 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
await this.apiService.putEmergencyAccess(details.id, updateRequest);
}
}
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey) {
const orgs = await this.userService.getAllOrganizations();
for (const org of orgs) {
// If not already enrolled, skip
if (!org.isResetPasswordEnrolled) {
continue;
}
// Re-enroll - encrpyt user's encKey.key with organization key
const orgSymKey = await this.cryptoService.getOrgKey(org.id);
const encryptedKey = await this.cryptoService.encrypt(encKey.key, orgSymKey);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = encryptedKey.encryptedString;
await this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);
}
}
}

View File

@@ -20,7 +20,7 @@ export class CreateOrganizationComponent implements OnInit {
constructor(private route: ActivatedRoute) { }
ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.plan === 'families') {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.product = ProductType.Families;

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -18,8 +19,8 @@ export class DeauthorizeSessionsComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private cryptoService: CryptoService,
private messagingService: MessagingService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -33,6 +34,7 @@ export class DeauthorizeSessionsComponent {
try {
this.formPromise = this.apiService.postSecurityStamp(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deauthorized Sessions' });
this.toasterService.popAsync('success', this.i18nService.t('sessionsDeauthorized'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');

View File

@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -18,8 +19,8 @@ export class DeleteAccountComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private cryptoService: CryptoService,
private messagingService: MessagingService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -33,6 +34,7 @@ export class DeleteAccountComponent {
try {
this.formPromise = this.apiService.deleteAccount(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deleted Account' });
this.toasterService.popAsync('success', this.i18nService.t('accountDeleted'),
this.i18nService.t('accountDeletedDesc'));
this.messagingService.send('logout');

View File

@@ -4,6 +4,7 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -21,16 +22,16 @@ export class DomainRulesComponent implements OnInit {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService) { }
async ngOnInit() {
const response = await this.apiService.getSettingsDomains();
this.loading = false;
if (response.equivalentDomains != null) {
this.custom = response.equivalentDomains.map(d => d.join(', '));
this.custom = response.equivalentDomains.map((d) => d.join(', '));
}
if (response.globalEquivalentDomains != null) {
this.global = response.globalEquivalentDomains.map(d => {
this.global = response.globalEquivalentDomains.map((d) => {
return {
domains: d.domains.join(', '),
excluded: d.excluded,
@@ -59,13 +60,13 @@ export class DomainRulesComponent implements OnInit {
async submit() {
const request = new UpdateDomainsRequest();
request.excludedGlobalEquivalentDomains = this.global.filter(d => d.excluded)
.map(d => d.key);
request.excludedGlobalEquivalentDomains = this.global.filter((d) => d.excluded)
.map((d) => d.key);
if (request.excludedGlobalEquivalentDomains.length === 0) {
request.excludedGlobalEquivalentDomains = null;
}
request.equivalentDomains = this.custom.filter(d => d != null && d.trim() !== '')
.map(d => d.split(',').map(d2 => d2.trim()));
request.equivalentDomains = this.custom.filter((d) => d != null && d.trim() !== '')
.map((d) => d.split(',').map((d2) => d2.trim()));
if (request.equivalentDomains.length === 0) {
request.equivalentDomains = null;
}
@@ -73,6 +74,7 @@ export class DomainRulesComponent implements OnInit {
try {
this.formPromise = this.apiService.putSettingsDomains(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Saved Equivalent Domains' });
this.toasterService.popAsync('success', null, this.i18nService.t('domainsUpdated'));
} catch { }
}

View File

@@ -55,11 +55,9 @@
</div>
</div>
<div class="modal-footer">
<button #submitBtn type="submit" class="btn btn-primary"
[disabled]="loading || submitBtn.loading || readOnly" [appApiAction]="formPromise">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"
*ngIf="loading || submitBtn.loading"></i>
<span *ngIf="!loading && !submitBtn.loading">{{'save' | i18n}}</span>
<button type="submit" class="btn btn-primary" [disabled]="loading || readOnly">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="loading"></i>
<span *ngIf="!loading">{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>

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