mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
93 Commits
v2.10.1
...
AC-1743/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38480c6c16 | ||
|
|
9fa399a046 | ||
|
|
04fac215b5 | ||
|
|
a133718eb7 | ||
|
|
37bdd75c67 | ||
|
|
a9f1d32ce0 | ||
|
|
6e76f23653 | ||
|
|
39ed9359fe | ||
|
|
d1cb92b5e7 | ||
|
|
eacdb6b8a8 | ||
|
|
95f613d61a | ||
|
|
c259962279 | ||
|
|
9126d4ae59 | ||
|
|
f3b01afd0b | ||
|
|
82b5f9bd04 | ||
|
|
187d1e17b1 | ||
|
|
e1300f585a | ||
|
|
11f5e2993a | ||
|
|
ff87907b4e | ||
|
|
1163f34317 | ||
|
|
7a2e9ecec6 | ||
|
|
bab928c07c | ||
|
|
1546cc2012 | ||
|
|
36ab2953b5 | ||
|
|
9a89e95918 | ||
|
|
013c8a5293 | ||
|
|
a5f779c231 | ||
|
|
dd7df6504b | ||
|
|
f064de83e8 | ||
|
|
e27bdbc561 | ||
|
|
93407d061b | ||
|
|
8848edbb40 | ||
|
|
33da38a3f3 | ||
|
|
26e4930a9e | ||
|
|
4dba787df2 | ||
|
|
2e2b5d988c | ||
|
|
d0c126cf3e | ||
|
|
8700332d13 | ||
|
|
0e4f9c0de5 | ||
|
|
c76033b8ad | ||
|
|
50cfc50ed7 | ||
|
|
739ab3d2dd | ||
|
|
1ac402ca06 | ||
|
|
a3c8629f6d | ||
|
|
161c5f82be | ||
|
|
0476c97f5f | ||
|
|
41967d6dc1 | ||
|
|
40908e4b88 | ||
|
|
9e7a11a27c | ||
|
|
a491e2691d | ||
|
|
2c65003cf9 | ||
|
|
0d78b33ae1 | ||
|
|
a3939a31a9 | ||
|
|
3540a2741e | ||
|
|
0ddf81f644 | ||
|
|
83d527a83e | ||
|
|
63b7b9124f | ||
|
|
5d85df2105 | ||
|
|
dbc3c8795d | ||
|
|
f09f7a0e51 | ||
|
|
e3afe9fb69 | ||
|
|
1a6c51b3aa | ||
|
|
107d7afb26 | ||
|
|
0959e58dc9 | ||
|
|
911f9c0dfc | ||
|
|
7cf518edb5 | ||
|
|
4234b3b1cf | ||
|
|
0f5cdd53df | ||
|
|
50bd7ca2b3 | ||
|
|
8531a64568 | ||
|
|
c1cec89995 | ||
|
|
3d214dbedc | ||
|
|
a528480e07 | ||
|
|
af5c863f1f | ||
|
|
365bda7e21 | ||
|
|
f1b533f7b6 | ||
|
|
5bf9b128d4 | ||
|
|
0a8c4d30bb | ||
|
|
1b3f277c1f | ||
|
|
8039f93434 | ||
|
|
d05e8ab7af | ||
|
|
fb3d082b88 | ||
|
|
8541a4252b | ||
|
|
e0d36a7407 | ||
|
|
73b031b884 | ||
|
|
167c5e0108 | ||
|
|
f67f113fe1 | ||
|
|
073126949b | ||
|
|
45d0192f82 | ||
|
|
2d02d54b56 | ||
|
|
8f4da6d490 | ||
|
|
7753749b62 | ||
|
|
c5cc8eab0a |
1
.depcheckrc
Normal file
1
.depcheckrc
Normal file
@@ -0,0 +1 @@
|
||||
ignores: ["*-loader", "webpack-cli", "@types/jest"]
|
||||
@@ -1,9 +1,10 @@
|
||||
dist
|
||||
build
|
||||
build-cli
|
||||
jslib
|
||||
webpack.cli.js
|
||||
webpack.main.js
|
||||
webpack.renderer.js
|
||||
|
||||
**/node_modules
|
||||
|
||||
**/jest.config.js
|
||||
|
||||
103
.eslintrc.json
103
.eslintrc.json
@@ -4,29 +4,92 @@
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["./jslib/shared/eslintrc.json"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.js"],
|
||||
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.eslint.json"],
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
"plugin:rxjs/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"]
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{ "accessibility": "no-public" }
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
|
||||
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"no-console": "error",
|
||||
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"pattern": "jslib-*/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/jslib/**/*",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "@/src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||
"rxjs-angular/prefer-takeuntil": "error",
|
||||
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"message": "Calling `svgIcon` directly is not allowed",
|
||||
"selector": "CallExpression[callee.name='svgIcon']"
|
||||
},
|
||||
{
|
||||
"message": "Accessing FormGroup using `get` is not allowed, use `.value` instead",
|
||||
"selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']"
|
||||
}
|
||||
],
|
||||
"curly": ["error", "all"],
|
||||
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
|
||||
"no-restricted-imports": ["error", { "patterns": ["src/**/*"] }]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"parser": "@angular-eslint/template-parser",
|
||||
"plugins": ["@angular-eslint/template"],
|
||||
"rules": {
|
||||
"@angular-eslint/template/button-has-type": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
15
.github/CODEOWNERS
vendored
Normal file
15
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Please sort into logical groups with comment headers. Sort groups in order of specificity.
|
||||
# For example, default owners should always be the first group.
|
||||
# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.
|
||||
#
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default file owners.
|
||||
* @bitwarden/team-admin-console-dev
|
||||
|
||||
# DevOps for Actions and other workflow changes.
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
.github/secrets @bitwarden/dept-devops
|
||||
|
||||
# Multiple Owners
|
||||
**/package.json
|
||||
31
.github/renovate.json
vendored
Normal file
31
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"github>bitwarden/renovate-config:pin-actions",
|
||||
":combinePatchMinorReleases",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":prConcurrentLimit10",
|
||||
":rebaseStalePrs",
|
||||
":separateMajorReleases",
|
||||
"group:monorepos",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"enabledManagers": ["github-actions", "npm"],
|
||||
"commitMessagePrefix": "[deps]:",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "npm minor",
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["package.json"],
|
||||
"description": "Admin Console owns general dependencies",
|
||||
"reviewers": ["team:team-admin-console-dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
197
.github/workflows/build.yml
vendored
197
.github/workflows/build.yml
vendored
@@ -2,21 +2,16 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
paths-ignore:
|
||||
- '.github/workflows/**'
|
||||
pull_request: {}
|
||||
workflow_dispatch: {}
|
||||
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up CLOC
|
||||
run: |
|
||||
@@ -29,38 +24,38 @@ jobs:
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
package_version: ${{ steps.retrieve-version.outputs.package_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Get Package Version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
PKG_VERSION=$(jq -r .version package.json)
|
||||
echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
linux-cli:
|
||||
name: Build Linux CLI
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_PKG_FETCH_NODE_VERSION: 16.13.0
|
||||
_PKG_FETCH_VERSION: 3.2
|
||||
_PKG_FETCH_NODE_VERSION: 20.11.0
|
||||
_PKG_FETCH_VERSION: 3.4
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -77,7 +72,7 @@ jobs:
|
||||
|
||||
- name: Keytar
|
||||
run: |
|
||||
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
||||
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
|
||||
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
|
||||
|
||||
keytarTarGz="$keytarTar.gz"
|
||||
@@ -94,13 +89,16 @@ jobs:
|
||||
run: npm run dist:cli:lin
|
||||
|
||||
- name: Zip
|
||||
run: zip -j ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip ./dist-cli/linux/bwdc ./keytar/linux/build/Release/keytar.node
|
||||
run: zip -j dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip dist-cli/linux/bwdc keytar/linux/build/Release/keytar.node
|
||||
|
||||
- name: Create checksums
|
||||
run: sha256sum ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
|
||||
run: |
|
||||
shasum -a 256 dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | \
|
||||
cut -d " " -f 1 > dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
|
||||
|
||||
- name: Version Test
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
|
||||
eval $(dbus-launch --sh-syntax)
|
||||
|
||||
@@ -121,14 +119,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Linux Zip to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux checksum to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
@@ -137,22 +135,22 @@ jobs:
|
||||
|
||||
macos-cli:
|
||||
name: Build Mac CLI
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_PKG_FETCH_NODE_VERSION: 16.13.0
|
||||
_PKG_FETCH_VERSION: 3.2
|
||||
_PKG_FETCH_NODE_VERSION: 20.11.0
|
||||
_PKG_FETCH_VERSION: 3.4
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -169,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Keytar
|
||||
run: |
|
||||
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
||||
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
|
||||
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
|
||||
|
||||
keytarTarGz="$keytarTar.gz"
|
||||
@@ -186,10 +184,12 @@ jobs:
|
||||
run: npm run dist:cli:mac
|
||||
|
||||
- name: Zip
|
||||
run: zip -j ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip ./dist-cli/macos/bwdc ./keytar/macos/build/Release/keytar.node
|
||||
run: zip -j dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip dist-cli/macos/bwdc keytar/macos/build/Release/keytar.node
|
||||
|
||||
- name: Create checksums
|
||||
run: sha256sum ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
|
||||
run: |
|
||||
shasum -a 256 dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | \
|
||||
cut -d " " -f 1 > dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
|
||||
|
||||
- name: Version Test
|
||||
run: |
|
||||
@@ -207,31 +207,30 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Mac Zip to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Mac checksum to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
windows-cli:
|
||||
name: Build Windows CLI
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_WIN_PKG_FETCH_VERSION: 16.13.0
|
||||
_WIN_PKG_VERSION: 3.2
|
||||
_WIN_PKG_FETCH_VERSION: 20.11.0
|
||||
_WIN_PKG_VERSION: 3.4
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Setup Windows builder
|
||||
run: |
|
||||
@@ -239,11 +238,11 @@ jobs:
|
||||
choco install reshack --no-progress
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -264,7 +263,7 @@ jobs:
|
||||
- name: Keytar
|
||||
shell: pwsh
|
||||
run: |
|
||||
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
|
||||
$keytarVersion = (Get-Content -Raw -Path ./package.json | ConvertFrom-Json).dependencies.keytar
|
||||
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
|
||||
$keytarTarGz = "${keytarTar}.gz"
|
||||
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
|
||||
@@ -333,15 +332,16 @@ jobs:
|
||||
|
||||
- name: Zip
|
||||
shell: cmd
|
||||
run: 7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
|
||||
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
|
||||
|
||||
- name: Version Test
|
||||
shell: pwsh
|
||||
run: |
|
||||
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
|
||||
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
|
||||
echo "version: $env:_PACKAGE_VERSION"
|
||||
Expand-Archive -Path "dist-cli\bwdc-windows-${{ env._PACKAGE_VERSION }}.zip" -DestinationPath "test\windows"
|
||||
$testVersion = Invoke-Expression '& .\test\windows\bwdc.exe -v'
|
||||
echo "version: ${env:_PACKAGE_VERSION}"
|
||||
echo "testVersion: $testVersion"
|
||||
if($testVersion -ne $env:_PACKAGE_VERSION) {
|
||||
if ($testVersion -ne ${env:_PACKAGE_VERSION}) {
|
||||
Throw "Version test failed."
|
||||
}
|
||||
|
||||
@@ -351,14 +351,14 @@ jobs:
|
||||
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
||||
|
||||
- name: Upload Windows Zip to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows checksum to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
@@ -367,50 +367,39 @@ jobs:
|
||||
|
||||
windows-gui:
|
||||
name: Build Windows GUI
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
needs: setup
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||
with:
|
||||
dotnet-version: "3.1.x"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
dotnet --version
|
||||
|
||||
- name: Install AST
|
||||
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
|
||||
run: dotnet tool install --global AzureSignTool --version 4.0.1
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
# - name: Run linter
|
||||
# run: npm run lint
|
||||
|
||||
- name: Build & Sign
|
||||
run: npm run dist:win
|
||||
env:
|
||||
@@ -422,28 +411,28 @@ jobs:
|
||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||
|
||||
- name: Upload Portable Executable to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Installer Executable to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Installer Executable Blockmap to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: latest.yml
|
||||
path: ./dist/latest.yml
|
||||
@@ -452,29 +441,28 @@ jobs:
|
||||
|
||||
linux-gui:
|
||||
name: Build Linux GUI
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -491,14 +479,14 @@ jobs:
|
||||
run: npm run dist:lin
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: latest-linux.yml
|
||||
path: ./dist/latest-linux.yml
|
||||
@@ -507,29 +495,28 @@ jobs:
|
||||
|
||||
macos-gui:
|
||||
name: Build MacOS GUI
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
needs: setup
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
node-version: '20.11.0'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
@@ -579,7 +566,7 @@ jobs:
|
||||
- name: Load package version
|
||||
run: |
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$packageVersion = (Get-Content -Raw -Path $rootPath\src\package.json | ConvertFrom-Json).version;
|
||||
$packageVersion = (Get-Content -Raw -Path $rootPath\package.json | ConvertFrom-Json).version;
|
||||
|
||||
Write-Output "Setting package version to $packageVersion";
|
||||
Write-Output "PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append;
|
||||
@@ -588,44 +575,36 @@ jobs:
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
# - name: Run linter
|
||||
# run: npm run lint
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:mac
|
||||
env:
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
|
||||
- name: Rename Zip Artifact
|
||||
run: |
|
||||
cd dist
|
||||
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
|
||||
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
- name: Upload .zip artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .dmg artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .dmg Blockmap artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: latest-mac.yml
|
||||
path: ./dist/latest-mac.yml
|
||||
@@ -634,7 +613,7 @@ jobs:
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- cloc
|
||||
- setup
|
||||
@@ -646,7 +625,7 @@ jobs:
|
||||
- macos-gui
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
@@ -675,22 +654,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
- name: Login to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
if: failure()
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
if: failure()
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
||||
if: failure()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
|
||||
4
.github/workflows/enforce-labels.yml
vendored
4
.github/workflows/enforce-labels.yml
vendored
@@ -7,10 +7,10 @@ on:
|
||||
jobs:
|
||||
enforce-label:
|
||||
name: EnforceLabel
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Enforce Label
|
||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
with:
|
||||
BANNED_LABELS: "hold"
|
||||
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"
|
||||
|
||||
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -17,8 +17,13 @@ on:
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Branch check
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
run: |
|
||||
@@ -29,49 +34,50 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
file: package.json
|
||||
|
||||
- name: Retrieve Directory Connector release version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
|
||||
- name: Check to make sure Mobile release version has been bumped
|
||||
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
latest_ver=$(hub release -L 1 -f '%T')
|
||||
latest_ver=${latest_ver:1}
|
||||
echo "Latest version: $latest_ver"
|
||||
ver=${{ steps.retrieve-version.outputs.package_version }}
|
||||
echo "Version: $ver"
|
||||
if [ "$latest_ver" = "$ver" ]; then
|
||||
echo "Version has not been bumped!"
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
id: deployment
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
initial-status: 'in_progress'
|
||||
environment: 'production'
|
||||
description: 'Deployment ${{ needs.setup.outputs.release-version }} from branch ${{ github.ref_name }}'
|
||||
task: release
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ steps.branch.outputs.branch-name }}
|
||||
branch: ${{ github.ref_name }}
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: main
|
||||
|
||||
- name: Create release
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
||||
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
with:
|
||||
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
|
||||
./bwdc-macos-${{ env.PKG_VERSION }}.zip,
|
||||
@@ -95,3 +101,19 @@ jobs:
|
||||
body: "<insert release notes here>"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
|
||||
- name: Update deployment status to Success
|
||||
if: ${{ success() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ failure() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
54
.github/workflows/test.yml
vendored
Normal file
54
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: Run tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc-*"
|
||||
pull_request: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Get Node Version
|
||||
id: retrieve-node-version
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
NODE_VERSION=${NODE_NVMRC/v/''}
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
# We use isolatedModules: true which disables typechecking in tests
|
||||
# Tests in apps/ are typechecked when their app is built, so we just do it here for libs/
|
||||
# See https://bitwarden.atlassian.net/browse/EC-497
|
||||
- name: Run typechecking
|
||||
run: npm run test:types --coverage
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
156
.github/workflows/version-bump.yml
vendored
156
.github/workflows/version-bump.yml
vendored
@@ -1,55 +1,121 @@
|
||||
---
|
||||
name: Version Bump
|
||||
run-name: Version Bump - v${{ inputs.version_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New Version"
|
||||
description: "New version (example: '2024.1.0')"
|
||||
required: true
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||
runs-on: ubuntu-20.04
|
||||
name: "Bump Version to v${{ inputs.version_number }}"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
repository: bitwarden/directory-connector
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Create Version Branch
|
||||
id: create-branch
|
||||
run: |
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout Version Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
with:
|
||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||
- name: Verify input version
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version has not changed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if version is newer.
|
||||
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Version check successful."
|
||||
else
|
||||
echo "Version check failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump Version - Package
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/package.json"
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "./package.json"
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-changed
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
|
||||
echo "No changes to commit!";
|
||||
fi
|
||||
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
run: git push -u origin $PR_BRANCH
|
||||
|
||||
- name: Create Version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
id: create-pr
|
||||
env:
|
||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
BASE_BRANCH: master
|
||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||
run: |
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$BASE" \
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
@@ -60,6 +126,44 @@ jobs:
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [X] Other
|
||||
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||
Automated version bump to ${{ inputs.version_number }}")
|
||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Approve PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||
|
||||
cut_rc:
|
||||
name: Cut RC branch
|
||||
needs: bump_version
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Check if RC branch exists
|
||||
run: |
|
||||
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "Remote RC branch exists."
|
||||
echo "Please delete current RC branch before running again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
git switch --quiet --create rc
|
||||
git push --quiet --set-upstream origin rc
|
||||
|
||||
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
||||
|
||||
47
.gitignore
vendored
47
.gitignore
vendored
@@ -1,17 +1,40 @@
|
||||
.vs
|
||||
.idea
|
||||
# General
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
vwd.webinfo
|
||||
dist/
|
||||
dist-cli/
|
||||
css/
|
||||
|
||||
# Build directories
|
||||
dist
|
||||
build
|
||||
.angular/cache
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
junit.xml
|
||||
|
||||
# Misc
|
||||
*.crx
|
||||
*.pem
|
||||
build-cli/
|
||||
build/
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
*.nupkg
|
||||
*.zip
|
||||
*.provisionprofile
|
||||
*.env
|
||||
.swp
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "jslib"]
|
||||
path = jslib
|
||||
url = https://github.com/bitwarden/jslib.git
|
||||
branch = master
|
||||
@@ -3,8 +3,6 @@ build
|
||||
build-cli
|
||||
dist
|
||||
|
||||
jslib
|
||||
|
||||
# External libraries / auto synced locales
|
||||
src/locales
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ We provide detailed documentation and examples for using the Directory Connector
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
|
||||
- [Node.js](https://nodejs.org) v18 (LTS)
|
||||
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
|
||||
|
||||
**Run the app**
|
||||
|
||||
35
angular.json
Normal file
35
angular.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "apps",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"projects": {
|
||||
"app": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.json",
|
||||
"assets": [],
|
||||
"styles": [],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
electron-builder.json
Normal file
67
electron-builder.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"extraMetadata": {
|
||||
"name": "bitwarden-directory-connector"
|
||||
},
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"appId": "com.bitwarden.directory-connector",
|
||||
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
"app": "build"
|
||||
},
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"mac": {
|
||||
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
|
||||
"category": "public.app-category.productivity",
|
||||
"gatekeeperAssess": false,
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "resources/entitlements.mac.plist",
|
||||
"entitlementsInherit": "resources/entitlements.mac.plist",
|
||||
"target": ["dmg", "zip"]
|
||||
},
|
||||
"win": {
|
||||
"target": ["portable", "nsis"],
|
||||
"sign": "scripts/sign.js"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"synopsis": "Sync your user directory to your Bitwarden organization.",
|
||||
"target": ["AppImage"]
|
||||
},
|
||||
"dmg": {
|
||||
"artifactName": "Bitwarden-Connector-${version}.${ext}",
|
||||
"icon": "dmg.icns",
|
||||
"contents": [
|
||||
{
|
||||
"x": 150,
|
||||
"y": 185,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"x": 390,
|
||||
"y": 180,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 540,
|
||||
"height": 380
|
||||
}
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"artifactName": "Bitwarden-Connector-Installer-${version}.${ext}",
|
||||
"uninstallDisplayName": "${productName}",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "Bitwarden-Connector-Portable-${version}.${ext}"
|
||||
},
|
||||
"appImage": {
|
||||
"artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}"
|
||||
}
|
||||
}
|
||||
42
jest.config.js
Normal file
42
jest.config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
|
||||
const tsPreset = require("ts-jest/jest-preset");
|
||||
const angularPreset = require("jest-preset-angular/jest-preset");
|
||||
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
// ...tsPreset,
|
||||
// ...angularPreset,
|
||||
preset: "jest-preset-angular",
|
||||
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||
|
||||
roots: ["<rootDir>"],
|
||||
modulePaths: [compilerOptions.baseUrl],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
|
||||
// Workaround for a memory leak that crashes tests in CI:
|
||||
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
||||
// Also anecdotally improves performance when run locally
|
||||
maxWorkers: 3,
|
||||
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"jest-preset-angular",
|
||||
// 'ts-jest',
|
||||
{
|
||||
...defaultTransformerOptions,
|
||||
tsconfig: "./tsconfig.json",
|
||||
// Further workaround for memory leak, recommended here:
|
||||
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
|
||||
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
|
||||
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
1
jslib
1
jslib
Submodule jslib deleted from e595c0548e
9
jslib/.gitignore
vendored
Normal file
9
jslib/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.vs
|
||||
.idea
|
||||
node_modules
|
||||
npm-debug.log
|
||||
vwd.webinfo
|
||||
*.crx
|
||||
*.pem
|
||||
dist
|
||||
coverage
|
||||
28
jslib/angular/spec/test.ts
Normal file
28
jslib/angular/spec/test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { webcrypto } from "crypto";
|
||||
import "jest-preset-angular/setup-jest";
|
||||
|
||||
Object.defineProperty(window, "CSS", { value: null });
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => {
|
||||
return {
|
||||
display: "none",
|
||||
appearance: ["-webkit-appearance"],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(document, "doctype", {
|
||||
value: "<!DOCTYPE html>",
|
||||
});
|
||||
Object.defineProperty(document.body.style, "transform", {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
35
jslib/angular/src/components/callout.component.html
Normal file
35
jslib/angular/src/components/callout.component.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div
|
||||
#callout
|
||||
class="callout callout-{{ calloutStyle }}"
|
||||
[ngClass]="{ clickable: clickable }"
|
||||
[attr.role]="useAlertRole ? 'alert' : null"
|
||||
>
|
||||
<h3 class="callout-heading" *ngIf="title">
|
||||
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="enforced-policy-options" *ngIf="enforcedPolicyOptions">
|
||||
{{ enforcedPolicyMessage }}
|
||||
<ul>
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{ "policyInEffectMinComplexity" | i18n : getPasswordScoreAlertDisplay() }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{ "policyInEffectMinLength" | i18n : enforcedPolicyOptions?.minLength.toString() }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{ "policyInEffectUppercase" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{ "policyInEffectLowercase" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{ "policyInEffectNumbers" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{ "policyInEffectSpecial" | i18n : "!@#$%^&*" }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
78
jslib/angular/src/components/callout.component.ts
Normal file
78
jslib/angular/src/components/callout.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { MasterPasswordPolicyOptions } from "@/jslib/common/src/models/domain/masterPasswordPolicyOptions";
|
||||
|
||||
@Component({
|
||||
selector: "app-callout",
|
||||
templateUrl: "callout.component.html",
|
||||
})
|
||||
export class CalloutComponent implements OnInit {
|
||||
@Input() type = "info";
|
||||
@Input() icon: string;
|
||||
@Input() title: string;
|
||||
@Input() clickable: boolean;
|
||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
@Input() enforcedPolicyMessage: string;
|
||||
@Input() useAlertRole = false;
|
||||
|
||||
calloutStyle: string;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.calloutStyle = this.type;
|
||||
|
||||
if (this.enforcedPolicyMessage === undefined) {
|
||||
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
|
||||
}
|
||||
|
||||
if (this.type === "warning" || this.type === "danger") {
|
||||
if (this.type === "danger") {
|
||||
this.calloutStyle = "danger";
|
||||
}
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t("warning");
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = "bwi-exclamation-triangle";
|
||||
}
|
||||
} else if (this.type === "error") {
|
||||
this.calloutStyle = "danger";
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t("error");
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = "bwi-error";
|
||||
}
|
||||
} else if (this.type === "tip") {
|
||||
this.calloutStyle = "success";
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t("tip");
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = "bwi-lightbulb";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
if (this.enforcedPolicyOptions == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let str: string;
|
||||
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||
case 4:
|
||||
str = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
str = this.i18nService.t("good");
|
||||
break;
|
||||
default:
|
||||
str = this.i18nService.t("weak");
|
||||
break;
|
||||
}
|
||||
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
|
||||
}
|
||||
}
|
||||
63
jslib/angular/src/components/environment.component.ts
Normal file
63
jslib/angular/src/components/environment.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
|
||||
@Directive()
|
||||
export class EnvironmentComponent {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
iconsUrl: string;
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
webVaultUrl: string;
|
||||
notificationsUrl: string;
|
||||
baseUrl: string;
|
||||
showCustom = false;
|
||||
|
||||
constructor(
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
this.apiUrl = urls.api || "";
|
||||
this.identityUrl = urls.identity || "";
|
||||
this.iconsUrl = urls.icons || "";
|
||||
this.notificationsUrl = urls.notifications || "";
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const resUrls = await this.environmentService.setUrls({
|
||||
base: this.baseUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
});
|
||||
|
||||
// re-set urls since service can change them, ex: prefixing https://
|
||||
this.baseUrl = resUrls.base;
|
||||
this.apiUrl = resUrls.api;
|
||||
this.identityUrl = resUrls.identity;
|
||||
this.webVaultUrl = resUrls.webVault;
|
||||
this.iconsUrl = resUrls.icons;
|
||||
this.notificationsUrl = resUrls.notifications;
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||
this.saved();
|
||||
}
|
||||
|
||||
toggleCustom() {
|
||||
this.showCustom = !this.showCustom;
|
||||
}
|
||||
|
||||
protected saved() {
|
||||
this.onSaved.emit();
|
||||
}
|
||||
}
|
||||
11
jslib/angular/src/components/icon.component.html
Normal file
11
jslib/angular/src/components/icon.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="icon" aria-hidden="true">
|
||||
<img
|
||||
[src]="image"
|
||||
appFallbackSrc="{{ fallbackImage }}"
|
||||
*ngIf="imageEnabled && image"
|
||||
alt=""
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
/>
|
||||
<i class="bwi bwi-fw bwi-lg {{ icon }}" *ngIf="!imageEnabled || !image"></i>
|
||||
</div>
|
||||
112
jslib/angular/src/components/icon.component.ts
Normal file
112
jslib/angular/src/components/icon.component.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { CipherType } from "@/jslib/common/src/enums/cipherType";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { CipherView } from "@/jslib/common/src/models/view/cipherView";
|
||||
|
||||
/**
|
||||
* Provides a mapping from supported card brands to
|
||||
* the filenames of icon that should be present in images/cards folder of clients.
|
||||
*/
|
||||
const cardIcons: Record<string, string> = {
|
||||
Visa: "card-visa",
|
||||
Mastercard: "card-mastercard",
|
||||
Amex: "card-amex",
|
||||
Discover: "card-discover",
|
||||
"Diners Club": "card-diners-club",
|
||||
JCB: "card-jcb",
|
||||
Maestro: "card-maestro",
|
||||
UnionPay: "card-union-pay",
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-icon",
|
||||
templateUrl: "icon.component.html",
|
||||
})
|
||||
export class IconComponent implements OnChanges {
|
||||
@Input() cipher: CipherView;
|
||||
icon: string;
|
||||
image: string;
|
||||
fallbackImage: string;
|
||||
imageEnabled: boolean;
|
||||
|
||||
private iconsUrl: string;
|
||||
|
||||
constructor(environmentService: EnvironmentService, private stateService: StateService) {
|
||||
this.iconsUrl = environmentService.getIconsUrl();
|
||||
}
|
||||
|
||||
async ngOnChanges() {
|
||||
// Components may be re-used when using cdk-virtual-scroll. Which puts the component in a weird state,
|
||||
// to avoid this we reset all state variables.
|
||||
this.image = null;
|
||||
this.fallbackImage = null;
|
||||
this.imageEnabled = !(await this.stateService.getDisableFavicon());
|
||||
this.load();
|
||||
}
|
||||
|
||||
protected load() {
|
||||
switch (this.cipher.type) {
|
||||
case CipherType.Login:
|
||||
this.icon = "bwi-globe";
|
||||
this.setLoginIcon();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.icon = "bwi-sticky-note";
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.icon = "bwi-credit-card";
|
||||
this.setCardIcon();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.icon = "bwi-id-card";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private setLoginIcon() {
|
||||
if (this.cipher.login.uri) {
|
||||
let hostnameUri = this.cipher.login.uri;
|
||||
let isWebsite = false;
|
||||
|
||||
if (hostnameUri.indexOf("androidapp://") === 0) {
|
||||
this.icon = "bwi-android";
|
||||
this.image = null;
|
||||
} else if (hostnameUri.indexOf("iosapp://") === 0) {
|
||||
this.icon = "bwi-apple";
|
||||
this.image = null;
|
||||
} else if (
|
||||
this.imageEnabled &&
|
||||
hostnameUri.indexOf("://") === -1 &&
|
||||
hostnameUri.indexOf(".") > -1
|
||||
) {
|
||||
hostnameUri = "http://" + hostnameUri;
|
||||
isWebsite = true;
|
||||
} else if (this.imageEnabled) {
|
||||
isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1;
|
||||
}
|
||||
|
||||
if (this.imageEnabled && isWebsite) {
|
||||
try {
|
||||
this.image = this.iconsUrl + "/" + Utils.getHostname(hostnameUri) + "/icon.png";
|
||||
this.fallbackImage = "images/bwi-globe.png";
|
||||
} catch (e) {
|
||||
// Ignore error since the fallback icon will be shown if image is null.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.image = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setCardIcon() {
|
||||
const brand = this.cipher.card.brand;
|
||||
if (this.imageEnabled && brand in cardIcons) {
|
||||
this.icon = "credit-card-icon " + cardIcons[brand];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from "@angular/cdk/a11y";
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
Type,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ModalService } from "../../services/modal.service";
|
||||
|
||||
import { ModalRef } from "./modal.ref";
|
||||
|
||||
@Component({
|
||||
selector: "app-modal",
|
||||
template: "<ng-template #modalContent></ng-template>",
|
||||
})
|
||||
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
|
||||
componentRef: ComponentRef<any>;
|
||||
|
||||
@ViewChild("modalContent", { read: ViewContainerRef, static: true })
|
||||
modalContentRef: ViewContainerRef;
|
||||
|
||||
childComponentType: Type<any>;
|
||||
setComponentParameters: (component: any) => void;
|
||||
|
||||
private focusTrap: ConfigurableFocusTrap;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private el: ElementRef<HTMLElement>,
|
||||
private focusTrapFactory: ConfigurableFocusTrapFactory,
|
||||
public modalRef: ModalRef
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.loadChildComponent(this.childComponentType);
|
||||
if (this.setComponentParameters != null) {
|
||||
this.setComponentParameters(this.componentRef.instance);
|
||||
}
|
||||
this.cd.detectChanges();
|
||||
|
||||
this.modalRef.created(this.el.nativeElement);
|
||||
this.focusTrap = this.focusTrapFactory.create(
|
||||
this.el.nativeElement.querySelector(".modal-dialog")
|
||||
);
|
||||
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
|
||||
this.focusTrap.focusFirstTabbableElementWhenReady();
|
||||
}
|
||||
}
|
||||
|
||||
loadChildComponent(componentType: Type<any>) {
|
||||
const componentFactory = this.modalService.resolveComponentFactory(componentType);
|
||||
|
||||
this.modalContentRef.clear();
|
||||
this.componentRef = this.modalContentRef.createComponent(componentFactory);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.componentRef) {
|
||||
this.componentRef.destroy();
|
||||
}
|
||||
this.focusTrap.destroy();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
getFocus() {
|
||||
const autoFocusEl = this.el.nativeElement.querySelector("[appAutoFocus]") as HTMLElement;
|
||||
autoFocusEl?.focus();
|
||||
}
|
||||
}
|
||||
18
jslib/angular/src/components/modal/modal-injector.ts
Normal file
18
jslib/angular/src/components/modal/modal-injector.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
|
||||
|
||||
export class ModalInjector implements Injector {
|
||||
constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap<any, any>) {}
|
||||
|
||||
get<T>(
|
||||
token: ProviderToken<T>,
|
||||
notFoundValue: undefined,
|
||||
options: InjectOptions & { optional?: false }
|
||||
): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
|
||||
get(token: any, notFoundValue?: any): any;
|
||||
get(token: any, notFoundValue?: any, flags?: any): any {
|
||||
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
|
||||
}
|
||||
}
|
||||
50
jslib/angular/src/components/modal/modal.ref.ts
Normal file
50
jslib/angular/src/components/modal/modal.ref.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
export class ModalRef {
|
||||
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
|
||||
onClose: Observable<any>; // Initiated close.
|
||||
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
|
||||
onShow: Observable<void>; // Start showing modal
|
||||
onShown: Observable<void>; // Modal is fully visible
|
||||
|
||||
private readonly _onCreated = new Subject<HTMLElement>();
|
||||
private readonly _onClose = new Subject<any>();
|
||||
private readonly _onClosed = new Subject<any>();
|
||||
private readonly _onShow = new Subject<void>();
|
||||
private readonly _onShown = new Subject<void>();
|
||||
private lastResult: any;
|
||||
|
||||
constructor() {
|
||||
this.onCreated = this._onCreated.asObservable();
|
||||
this.onClose = this._onClose.asObservable();
|
||||
this.onClosed = this._onClosed.asObservable();
|
||||
this.onShow = this._onShow.asObservable();
|
||||
this.onShown = this._onShow.asObservable();
|
||||
}
|
||||
|
||||
show() {
|
||||
this._onShow.next();
|
||||
}
|
||||
|
||||
shown() {
|
||||
this._onShown.next();
|
||||
}
|
||||
|
||||
close(result?: any) {
|
||||
this.lastResult = result;
|
||||
this._onClose.next(result);
|
||||
}
|
||||
|
||||
closed() {
|
||||
this._onClosed.next(this.lastResult);
|
||||
}
|
||||
|
||||
created(el: HTMLElement) {
|
||||
this._onCreated.next(el);
|
||||
}
|
||||
|
||||
onClosedPromise(): Promise<any> {
|
||||
return this.onClosed.pipe(first()).toPromise();
|
||||
}
|
||||
}
|
||||
41
jslib/angular/src/components/password-reprompt.component.ts
Normal file
41
jslib/angular/src/components/password-reprompt.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Directive } from "@angular/core";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
|
||||
import { ModalRef } from "./modal/modal.ref";
|
||||
|
||||
/**
|
||||
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||
* See UserVerificationComponent for any other situation where you need to verify the user's identity.
|
||||
*/
|
||||
@Directive()
|
||||
export class PasswordRepromptComponent {
|
||||
showPassword = false;
|
||||
masterPassword = "";
|
||||
|
||||
constructor(
|
||||
private modalRef: ModalRef,
|
||||
private cryptoService: CryptoService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null))) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("invalidMasterPassword")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalRef.close(true);
|
||||
}
|
||||
}
|
||||
95
jslib/angular/src/components/toastr.component.ts
Normal file
95
jslib/angular/src/components/toastr.component.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
|
||||
import {
|
||||
DefaultNoComponentGlobalConfig,
|
||||
GlobalConfig,
|
||||
Toast as BaseToast,
|
||||
ToastPackage,
|
||||
ToastrService,
|
||||
TOAST_CONFIG,
|
||||
} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: "[toast-component2]",
|
||||
template: `
|
||||
<button
|
||||
*ngIf="options.closeButton"
|
||||
(click)="remove()"
|
||||
type="button"
|
||||
class="toast-close-button"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div class="icon">
|
||||
<i></i>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
|
||||
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="message && options.enableHtml"
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options.messageClass"
|
||||
[innerHTML]="message"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="message && !options.enableHtml"
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options.messageClass"
|
||||
[attr.aria-label]="message"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="options.progressBar">
|
||||
<div class="toast-progress" [style.width]="width + '%'"></div>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger("flyInOut", [
|
||||
state("inactive", style({ opacity: 0 })),
|
||||
state("active", style({ opacity: 1 })),
|
||||
state("removed", style({ opacity: 0 })),
|
||||
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
|
||||
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
|
||||
]),
|
||||
],
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class BitwardenToast extends BaseToast {
|
||||
constructor(protected toastrService: ToastrService, public toastPackage: ToastPackage) {
|
||||
super(toastrService, toastPackage);
|
||||
}
|
||||
}
|
||||
|
||||
export const BitwardenToastGlobalConfig: GlobalConfig = {
|
||||
...DefaultNoComponentGlobalConfig,
|
||||
toastComponent: BitwardenToast,
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [BitwardenToast],
|
||||
exports: [BitwardenToast],
|
||||
})
|
||||
export class BitwardenToastModule {
|
||||
static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> {
|
||||
return {
|
||||
ngModule: BitwardenToastModule,
|
||||
providers: [
|
||||
{
|
||||
provide: TOAST_CONFIG,
|
||||
useValue: {
|
||||
default: BitwardenToastGlobalConfig,
|
||||
config: config,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
23
jslib/angular/src/directives/a11y-title.directive.ts
Normal file
23
jslib/angular/src/directives/a11y-title.directive.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appA11yTitle]",
|
||||
})
|
||||
export class A11yTitleDirective {
|
||||
@Input() set appA11yTitle(title: string) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
private title: string;
|
||||
|
||||
constructor(private el: ElementRef, private renderer: Renderer2) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.el.nativeElement.hasAttribute("title")) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
|
||||
}
|
||||
if (!this.el.nativeElement.hasAttribute("aria-label")) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
jslib/angular/src/directives/api-action.directive.ts
Normal file
49
jslib/angular/src/directives/api-action.directive.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
|
||||
|
||||
import { ValidationService } from "../services/validation.service";
|
||||
|
||||
/**
|
||||
* Provides error handling, in particular for any error returned by the server in an api call.
|
||||
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
|
||||
* e.g. <form [appApiAction]="this.formPromise">
|
||||
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appApiAction]",
|
||||
})
|
||||
export class ApiActionDirective implements OnChanges {
|
||||
@Input() appApiAction: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private validationService: ValidationService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.appApiAction == null || this.appApiAction.then == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.nativeElement.loading = true;
|
||||
|
||||
this.appApiAction.then(
|
||||
(response: any) => {
|
||||
this.el.nativeElement.loading = false;
|
||||
},
|
||||
(e: any) => {
|
||||
this.el.nativeElement.loading = false;
|
||||
|
||||
if ((e as ErrorResponse).captchaRequired) {
|
||||
this.logService.error("Captcha required error response: " + e.getSingleMessage());
|
||||
return;
|
||||
}
|
||||
this.logService?.error(`Received API exception: ${e}`);
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
27
jslib/angular/src/directives/autofocus.directive.ts
Normal file
27
jslib/angular/src/directives/autofocus.directive.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
|
||||
@Directive({
|
||||
selector: "[appAutofocus]",
|
||||
})
|
||||
export class AutofocusDirective {
|
||||
@Input() set appAutofocus(condition: boolean | string) {
|
||||
this.autofocus = condition === "" || condition === true;
|
||||
}
|
||||
|
||||
private autofocus: boolean;
|
||||
|
||||
constructor(private el: ElementRef, private ngZone: NgZone) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (!Utils.isMobileBrowser && this.autofocus) {
|
||||
if (this.ngZone.isStable) {
|
||||
this.el.nativeElement.focus();
|
||||
} else {
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
jslib/angular/src/directives/blur-click.directive.ts
Normal file
12
jslib/angular/src/directives/blur-click.directive.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appBlurClick]",
|
||||
})
|
||||
export class BlurClickDirective {
|
||||
constructor(private el: ElementRef) {}
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.el.nativeElement.blur();
|
||||
}
|
||||
}
|
||||
59
jslib/angular/src/directives/box-row.directive.ts
Normal file
59
jslib/angular/src/directives/box-row.directive.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appBoxRow]",
|
||||
})
|
||||
export class BoxRowDirective implements OnInit {
|
||||
el: HTMLElement = null;
|
||||
formEls: Element[];
|
||||
|
||||
constructor(elRef: ElementRef) {
|
||||
this.el = elRef.nativeElement;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formEls = Array.from(
|
||||
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')
|
||||
);
|
||||
this.formEls.forEach((formEl) => {
|
||||
formEl.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
this.el.classList.add("active");
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
formEl.addEventListener(
|
||||
"blur",
|
||||
() => {
|
||||
this.el.classList.remove("active");
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener("click", ["$event"]) onClick(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target !== this.el &&
|
||||
!target.classList.contains("progress") &&
|
||||
!target.classList.contains("progress-bar")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formEls.length > 0) {
|
||||
const formEl = this.formEls[0] as HTMLElement;
|
||||
if (formEl.tagName.toLowerCase() === "input") {
|
||||
const inputEl = formEl as HTMLInputElement;
|
||||
if (inputEl.type != null && inputEl.type.toLowerCase() === "checkbox") {
|
||||
inputEl.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
formEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
jslib/angular/src/directives/fallback-src.directive.ts
Normal file
14
jslib/angular/src/directives/fallback-src.directive.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appFallbackSrc]",
|
||||
})
|
||||
export class FallbackSrcDirective {
|
||||
@Input("appFallbackSrc") appFallbackSrc: string;
|
||||
|
||||
constructor(private el: ElementRef) {}
|
||||
|
||||
@HostListener("error") onError() {
|
||||
this.el.nativeElement.src = this.appFallbackSrc;
|
||||
}
|
||||
}
|
||||
10
jslib/angular/src/directives/stop-click.directive.ts
Normal file
10
jslib/angular/src/directives/stop-click.directive.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Directive, HostListener } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appStopClick]",
|
||||
})
|
||||
export class StopClickDirective {
|
||||
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
}
|
||||
10
jslib/angular/src/directives/stop-prop.directive.ts
Normal file
10
jslib/angular/src/directives/stop-prop.directive.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Directive, HostListener } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appStopProp]",
|
||||
})
|
||||
export class StopPropDirective {
|
||||
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
}
|
||||
14
jslib/angular/src/pipes/i18n.pipe.ts
Normal file
14
jslib/angular/src/pipes/i18n.pipe.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
|
||||
@Pipe({
|
||||
name: "i18n",
|
||||
})
|
||||
export class I18nPipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
transform(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.i18nService.t(id, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
41
jslib/angular/src/pipes/search-ciphers.pipe.ts
Normal file
41
jslib/angular/src/pipes/search-ciphers.pipe.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { CipherView } from "@/jslib/common/src/models/view/cipherView";
|
||||
|
||||
@Pipe({
|
||||
name: "searchCiphers",
|
||||
})
|
||||
export class SearchCiphersPipe implements PipeTransform {
|
||||
transform(ciphers: CipherView[], searchText: string, deleted = false): CipherView[] {
|
||||
if (ciphers == null || ciphers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (searchText == null || searchText.length < 2) {
|
||||
return ciphers.filter((c) => {
|
||||
return deleted !== c.isDeleted;
|
||||
});
|
||||
}
|
||||
|
||||
searchText = searchText.trim().toLowerCase();
|
||||
return ciphers.filter((c) => {
|
||||
if (deleted !== c.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (searchText.length >= 8 && c.id.startsWith(searchText)) {
|
||||
return true;
|
||||
}
|
||||
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(searchText) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
170
jslib/angular/src/scss/bwicons/fonts/bwi-font.svg
Normal file
170
jslib/angular/src/scss/bwicons/fonts/bwi-font.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 262 KiB |
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.ttf
Normal file
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.ttf
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.woff
Normal file
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.woff2
Normal file
BIN
jslib/angular/src/scss/bwicons/fonts/bwi-font.woff2
Normal file
Binary file not shown.
250
jslib/angular/src/scss/bwicons/styles/style.scss
Normal file
250
jslib/angular/src/scss/bwicons/styles/style.scss
Normal file
@@ -0,0 +1,250 @@
|
||||
$icomoon-font-family: "bwi-font" !default;
|
||||
$icomoon-font-path: "/jslib/angular/src/scss/bwicons/fonts/" !default;
|
||||
|
||||
// New font sheet? Update the font-face information below
|
||||
@font-face {
|
||||
font-family: "#{$icomoon-font-family}";
|
||||
src: url($icomoon-font-path + "bwi-font.svg") format("svg"),
|
||||
url($icomoon-font-path + "bwi-font.ttf") format("truetype"),
|
||||
url($icomoon-font-path + "bwi-font.woff") format("woff"),
|
||||
url($icomoon-font-path + "bwi-font.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
// Base Class
|
||||
.bwi {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: "#{$icomoon-font-family}" !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
/* Better Font Rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
// Fixed Width Icons
|
||||
.bwi-fw {
|
||||
width: calc(18em / 14);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Sizing Changes
|
||||
.bwi-sm {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.bwi-lg {
|
||||
font-size: calc(4em / 3);
|
||||
line-height: calc(3em / 4);
|
||||
vertical-align: -15%;
|
||||
}
|
||||
|
||||
.bwi-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.bwi-3x {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.bwi-4x {
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
// Spin Animations
|
||||
.bwi-spin {
|
||||
animation: bwi-spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes bwi-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
// List Icons
|
||||
.bwi-ul {
|
||||
padding-left: 0;
|
||||
margin-left: calc(30em / 14);
|
||||
list-style-type: none;
|
||||
> li {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.bwi-li {
|
||||
position: absolute;
|
||||
left: calc(-30em / 14);
|
||||
width: calc(30em / 14);
|
||||
top: calc(2em / 14);
|
||||
text-align: center;
|
||||
&.bwi-lg {
|
||||
left: calc(-30em / 14) + calc(4em / 14);
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation
|
||||
.bwi-rotate-270 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
// For new icons - add their glyph name and value to the map below
|
||||
$icons: (
|
||||
"save-changes": "\e988",
|
||||
"browser": "\e985",
|
||||
"mobile": "\e986",
|
||||
"cli": "\e987",
|
||||
"providers": "\e983",
|
||||
"vault": "\e984",
|
||||
"folder-closed-f": "\e982",
|
||||
"rocket": "\e9ee",
|
||||
"ellipsis-h": "\e9ef",
|
||||
"ellipsis-v": "\e9f0",
|
||||
"safari": "\e974",
|
||||
"opera": "\e975",
|
||||
"firefox": "\e976",
|
||||
"edge": "\e977",
|
||||
"chrome": "\e978",
|
||||
"star-f": "\e979",
|
||||
"arrow-circle-up": "\e97a",
|
||||
"arrow-circle-right": "\e97b",
|
||||
"arrow-circle-left": "\e97c",
|
||||
"arrow-circle-down": "\e97d",
|
||||
"undo": "\e97e",
|
||||
"bolt": "\e97f",
|
||||
"puzzle": "\e980",
|
||||
"rss": "\e973",
|
||||
"dbl-angle-left": "\e970",
|
||||
"dbl-angle-right": "\e971",
|
||||
"hamburger": "\e972",
|
||||
"bw-folder-open-f": "\e93e",
|
||||
"desktop": "\e96a",
|
||||
"angle-left": "\e96b",
|
||||
"user": "\e900",
|
||||
"user-f": "\e901",
|
||||
"key": "\e902",
|
||||
"share-square": "\e903",
|
||||
"hashtag": "\e904",
|
||||
"clone": "\e905",
|
||||
"list-alt": "\e906",
|
||||
"id-card": "\e907",
|
||||
"credit-card": "\e908",
|
||||
"globe": "\e909",
|
||||
"sticky-note": "\e90a",
|
||||
"folder": "\e90b",
|
||||
"lock": "\e90c",
|
||||
"lock-f": "\e90d",
|
||||
"generate": "\e90e",
|
||||
"generate-f": "\e90f",
|
||||
"cog": "\e910",
|
||||
"cog-f": "\e911",
|
||||
"check-circle": "\e912",
|
||||
"eye": "\e913",
|
||||
"pencil-square": "\e914",
|
||||
"bookmark": "\e915",
|
||||
"files": "\e916",
|
||||
"trash": "\e917",
|
||||
"plus": "\e918",
|
||||
"star": "\e919",
|
||||
"list": "\e91a",
|
||||
"angle-right": "\e91b",
|
||||
"external-link": "\e91c",
|
||||
"refresh": "\e91d",
|
||||
"search": "\e91f",
|
||||
"filter": "\e920",
|
||||
"plus-circle": "\e921",
|
||||
"user-circle": "\e922",
|
||||
"question-circle": "\e923",
|
||||
"cogs": "\e924",
|
||||
"minus-circle": "\e925",
|
||||
"send": "\e926",
|
||||
"send-f": "\e927",
|
||||
"download": "\e928",
|
||||
"pencil": "\e929",
|
||||
"sign-out": "\e92a",
|
||||
"share": "\e92b",
|
||||
"clock": "\e92c",
|
||||
"angle-down": "\e92d",
|
||||
"caret-down": "\e92e",
|
||||
"square": "\e92f",
|
||||
"collection": "\e930",
|
||||
"bank": "\e931",
|
||||
"shield": "\e932",
|
||||
"stop": "\e933",
|
||||
"plus-square": "\e934",
|
||||
"save": "\e935",
|
||||
"sign-in": "\e936",
|
||||
"spinner": "\e937",
|
||||
"dollar": "\e939",
|
||||
"check": "\e93a",
|
||||
"check-square": "\e93b",
|
||||
"minus-square": "\e93c",
|
||||
"close": "\e93d",
|
||||
"share-arrow": "\e96c",
|
||||
"paperclip": "\e93f",
|
||||
"bitcoin": "\e940",
|
||||
"cut": "\e941",
|
||||
"frown": "\e942",
|
||||
"folder-open": "\e943",
|
||||
"bug": "\e946",
|
||||
"chain-broken": "\e947",
|
||||
"dashboard": "\e948",
|
||||
"envelope": "\e949",
|
||||
"exclamation-circle": "\e94a",
|
||||
"exclamation-triangle": "\e94b",
|
||||
"caret-right": "\e94c",
|
||||
"file-pdf": "\e94e",
|
||||
"file-text": "\e94f",
|
||||
"info-circle": "\e952",
|
||||
"lightbulb": "\e953",
|
||||
"link": "\e954",
|
||||
"linux": "\e956",
|
||||
"long-arrow-right": "\e957",
|
||||
"money": "\e958",
|
||||
"play": "\e959",
|
||||
"reddit": "\e95a",
|
||||
"refresh-tab": "\e95b",
|
||||
"sitemap": "\e95c",
|
||||
"sliders": "\e95d",
|
||||
"tag": "\e95e",
|
||||
"thumb-tack": "\e95f",
|
||||
"thumbs-up": "\e960",
|
||||
"unlock": "\e962",
|
||||
"users": "\e963",
|
||||
"wrench": "\e965",
|
||||
"ban": "\e967",
|
||||
"camera": "\e968",
|
||||
"chevron-up": "\e969",
|
||||
"eye-slash": "\e96d",
|
||||
"file": "\e96e",
|
||||
"paste": "\e96f",
|
||||
"github": "\e950",
|
||||
"facebook": "\e94d",
|
||||
"paypal": "\e938",
|
||||
"google": "\e951",
|
||||
"linkedin": "\e955",
|
||||
"discourse": "\e91e",
|
||||
"twitter": "\e961",
|
||||
"youtube": "\e966",
|
||||
"windows": "\e964",
|
||||
"apple": "\e945",
|
||||
"android": "\e944",
|
||||
"error": "\e981",
|
||||
"numbered-list": "\e989",
|
||||
);
|
||||
|
||||
@each $name, $glyph in $icons {
|
||||
.bwi-#{$name}:before {
|
||||
content: $glyph;
|
||||
}
|
||||
}
|
||||
44
jslib/angular/src/scss/icons.scss
Normal file
44
jslib/angular/src/scss/icons.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
$card-icons-base: "~@bitwarden/jslib-angular/src/images/cards/";
|
||||
$card-icons: (
|
||||
"visa": $card-icons-base + "visa-light.png",
|
||||
"amex": $card-icons-base + "amex-light.png",
|
||||
"diners-club": $card-icons-base + "diners_club-light.png",
|
||||
"discover": $card-icons-base + "discover-light.png",
|
||||
"jcb": $card-icons-base + "jcb-light.png",
|
||||
"maestro": $card-icons-base + "maestro-light.png",
|
||||
"mastercard": $card-icons-base + "mastercard-light.png",
|
||||
"union-pay": $card-icons-base + "union_pay-light.png",
|
||||
);
|
||||
|
||||
$card-icons-dark: (
|
||||
"visa": $card-icons-base + "visa-dark.png",
|
||||
"amex": $card-icons-base + "amex-dark.png",
|
||||
"diners-club": $card-icons-base + "diners_club-dark.png",
|
||||
"discover": $card-icons-base + "discover-dark.png",
|
||||
"jcb": $card-icons-base + "jcb-dark.png",
|
||||
"maestro": $card-icons-base + "maestro-dark.png",
|
||||
"mastercard": $card-icons-base + "mastercard-dark.png",
|
||||
"union-pay": $card-icons-base + "union_pay-dark.png",
|
||||
);
|
||||
|
||||
.credit-card-icon {
|
||||
display: block; // Resolves the parent container being slighly to big
|
||||
height: 19px;
|
||||
width: 24px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@each $name, $url in $card-icons {
|
||||
.card-#{$name} {
|
||||
background-image: url("#{$url}");
|
||||
}
|
||||
}
|
||||
|
||||
@each $theme in $dark-icon-themes {
|
||||
@each $name, $url in $card-icons-dark {
|
||||
.#{$theme} .card-#{$name} {
|
||||
background-image: url("#{$url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
89
jslib/angular/src/scss/webfonts.css
Normal file
89
jslib/angular/src/scss/webfonts.css
Normal file
@@ -0,0 +1,89 @@
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-italic-300.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-italic-400.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-italic-600.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-italic-700.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-italic-800.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-normal-300.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-normal-400.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-normal-600.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-normal-700.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: auto;
|
||||
src: url(webfonts/Open_Sans-normal-800.woff) format("woff");
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-300.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-300.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-400.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-400.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-600.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-600.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-700.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-700.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-800.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-italic-800.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-300.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-300.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-400.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-400.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-600.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-600.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-700.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-700.woff
Normal file
Binary file not shown.
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-800.woff
Normal file
BIN
jslib/angular/src/scss/webfonts/Open_Sans-normal-800.woff
Normal file
Binary file not shown.
45
jslib/angular/src/services/auth-guard.service.ts
Normal file
45
jslib/angular/src/services/auth-guard.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuardService implements CanActivate {
|
||||
constructor(
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private router: Router,
|
||||
private messagingService: MessagingService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthed) {
|
||||
this.messagingService.send("authBlocked");
|
||||
return false;
|
||||
}
|
||||
|
||||
const locked = await this.vaultTimeoutService.isLocked();
|
||||
if (locked) {
|
||||
if (routerState != null) {
|
||||
this.messagingService.send("lockedUrl", { url: routerState.url });
|
||||
}
|
||||
this.router.navigate(["lock"], { queryParams: { promptBiometric: true } });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!routerState.url.includes("remove-password") &&
|
||||
(await this.keyConnectorService.getConvertAccountRequired())
|
||||
) {
|
||||
this.router.navigate(["/remove-password"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
6
jslib/angular/src/services/broadcaster.service.ts
Normal file
6
jslib/angular/src/services/broadcaster.service.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { BroadcasterService as BaseBroadcasterService } from "@/jslib/common/src/services/broadcaster.service";
|
||||
|
||||
@Injectable()
|
||||
export class BroadcasterService extends BaseBroadcasterService {}
|
||||
474
jslib/angular/src/services/jslib-services.module.ts
Normal file
474
jslib/angular/src/services/jslib-services.module.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@/jslib/common/src/abstractions/cipher.service";
|
||||
import { CollectionService as CollectionServiceAbstraction } from "@/jslib/common/src/abstractions/collection.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@/jslib/common/src/abstractions/fileUpload.service";
|
||||
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@/jslib/common/src/abstractions/notifications.service";
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@/jslib/common/src/abstractions/policy.service";
|
||||
import { ProviderService as ProviderServiceAbstraction } from "@/jslib/common/src/abstractions/provider.service";
|
||||
import { SearchService as SearchServiceAbstraction } from "@/jslib/common/src/abstractions/search.service";
|
||||
import { SendService as SendServiceAbstraction } from "@/jslib/common/src/abstractions/send.service";
|
||||
import { SettingsService as SettingsServiceAbstraction } from "@/jslib/common/src/abstractions/settings.service";
|
||||
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "@/jslib/common/src/abstractions/sync.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "@/jslib/common/src/abstractions/totp.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@/jslib/common/src/abstractions/userVerification.service";
|
||||
import { UsernameGenerationService as UsernameGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/usernameGeneration.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/services/appId.service";
|
||||
import { AuditService } from "@/jslib/common/src/services/audit.service";
|
||||
import { AuthService } from "@/jslib/common/src/services/auth.service";
|
||||
import { CipherService } from "@/jslib/common/src/services/cipher.service";
|
||||
import { CollectionService } from "@/jslib/common/src/services/collection.service";
|
||||
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
import { EventService } from "@/jslib/common/src/services/event.service";
|
||||
import { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
|
||||
import { FolderService } from "@/jslib/common/src/services/folder.service";
|
||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
||||
import { NotificationsService } from "@/jslib/common/src/services/notifications.service";
|
||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
||||
import { ProviderService } from "@/jslib/common/src/services/provider.service";
|
||||
import { SearchService } from "@/jslib/common/src/services/search.service";
|
||||
import { SendService } from "@/jslib/common/src/services/send.service";
|
||||
import { SettingsService } from "@/jslib/common/src/services/settings.service";
|
||||
import { StateService } from "@/jslib/common/src/services/state.service";
|
||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
import { SyncService } from "@/jslib/common/src/services/sync.service";
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
import { TotpService } from "@/jslib/common/src/services/totp.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
|
||||
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
|
||||
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.service";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
import { BroadcasterService } from "./broadcaster.service";
|
||||
import { LockGuardService } from "./lock-guard.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||
import { UnauthGuardService } from "./unauth-guard.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
providers: [
|
||||
{ provide: "WINDOW", useValue: window },
|
||||
{
|
||||
provide: LOCALE_ID,
|
||||
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
||||
deps: [I18nServiceAbstraction],
|
||||
},
|
||||
ValidationService,
|
||||
AuthGuardService,
|
||||
UnauthGuardService,
|
||||
LockGuardService,
|
||||
ModalService,
|
||||
{
|
||||
provide: AppIdServiceAbstraction,
|
||||
useClass: AppIdService,
|
||||
deps: [StorageServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: AuditServiceAbstraction,
|
||||
useClass: AuditService,
|
||||
deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: AuthServiceAbstraction,
|
||||
useClass: AuthService,
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: CipherServiceAbstraction,
|
||||
useFactory: (
|
||||
cryptoService: CryptoServiceAbstraction,
|
||||
settingsService: SettingsServiceAbstraction,
|
||||
apiService: ApiServiceAbstraction,
|
||||
fileUploadService: FileUploadServiceAbstraction,
|
||||
i18nService: I18nServiceAbstraction,
|
||||
injector: Injector,
|
||||
logService: LogService,
|
||||
stateService: StateServiceAbstraction
|
||||
) =>
|
||||
new CipherService(
|
||||
cryptoService,
|
||||
settingsService,
|
||||
apiService,
|
||||
fileUploadService,
|
||||
i18nService,
|
||||
() => injector.get(SearchServiceAbstraction),
|
||||
logService,
|
||||
stateService
|
||||
),
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
SettingsServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
FileUploadServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
Injector, // TODO: Get rid of this circular dependency!
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: FolderServiceAbstraction,
|
||||
useClass: FolderService,
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{ provide: LogService, useFactory: () => new ConsoleLogService(false) },
|
||||
{
|
||||
provide: CollectionServiceAbstraction,
|
||||
useClass: CollectionService,
|
||||
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: EnvironmentServiceAbstraction,
|
||||
useClass: EnvironmentService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: TotpServiceAbstraction,
|
||||
useClass: TotpService,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction],
|
||||
},
|
||||
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
|
||||
{
|
||||
provide: CryptoServiceAbstraction,
|
||||
useClass: CryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useClass: PasswordGenerationService,
|
||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useClass: UsernameGenerationService,
|
||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: ApiServiceAbstraction,
|
||||
useFactory: (
|
||||
tokenService: TokenServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
appIdService: AppIdServiceAbstraction
|
||||
) =>
|
||||
new ApiService(
|
||||
tokenService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
appIdService,
|
||||
async (expired: boolean) => messagingService.send("logout", { expired: expired })
|
||||
),
|
||||
deps: [
|
||||
TokenServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: FileUploadServiceAbstraction,
|
||||
useClass: FileUploadService,
|
||||
deps: [LogService, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: SyncServiceAbstraction,
|
||||
useFactory: (
|
||||
apiService: ApiServiceAbstraction,
|
||||
settingsService: SettingsServiceAbstraction,
|
||||
folderService: FolderServiceAbstraction,
|
||||
cipherService: CipherServiceAbstraction,
|
||||
cryptoService: CryptoServiceAbstraction,
|
||||
collectionService: CollectionServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
policyService: PolicyServiceAbstraction,
|
||||
sendService: SendServiceAbstraction,
|
||||
logService: LogService,
|
||||
keyConnectorService: KeyConnectorServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
organizationService: OrganizationServiceAbstraction,
|
||||
providerService: ProviderServiceAbstraction
|
||||
) =>
|
||||
new SyncService(
|
||||
apiService,
|
||||
settingsService,
|
||||
folderService,
|
||||
cipherService,
|
||||
cryptoService,
|
||||
collectionService,
|
||||
messagingService,
|
||||
policyService,
|
||||
sendService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
stateService,
|
||||
organizationService,
|
||||
providerService,
|
||||
async (expired: boolean) => messagingService.send("logout", { expired: expired })
|
||||
),
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
SettingsServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
SendServiceAbstraction,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
ProviderServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
|
||||
{
|
||||
provide: SettingsServiceAbstraction,
|
||||
useClass: SettingsService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutServiceAbstraction,
|
||||
useFactory: (
|
||||
cipherService: CipherServiceAbstraction,
|
||||
folderService: FolderServiceAbstraction,
|
||||
collectionService: CollectionServiceAbstraction,
|
||||
cryptoService: CryptoServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
searchService: SearchServiceAbstraction,
|
||||
tokenService: TokenServiceAbstraction,
|
||||
policyService: PolicyServiceAbstraction,
|
||||
keyConnectorService: KeyConnectorServiceAbstraction,
|
||||
stateService: StateServiceAbstraction
|
||||
) =>
|
||||
new VaultTimeoutService(
|
||||
cipherService,
|
||||
folderService,
|
||||
collectionService,
|
||||
cryptoService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
searchService,
|
||||
tokenService,
|
||||
policyService,
|
||||
keyConnectorService,
|
||||
stateService,
|
||||
null,
|
||||
async (userId?: string) =>
|
||||
messagingService.send("logout", { expired: false, userId: userId })
|
||||
),
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: StateServiceAbstraction,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
logService: LogService,
|
||||
stateMigrationService: StateMigrationServiceAbstraction
|
||||
) =>
|
||||
new StateService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
new StateFactory(GlobalState, Account)
|
||||
),
|
||||
deps: [
|
||||
StorageServiceAbstraction,
|
||||
"SECURE_STORAGE",
|
||||
LogService,
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: StateMigrationServiceAbstraction,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction
|
||||
) =>
|
||||
new StateMigrationService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
new StateFactory(GlobalState, Account)
|
||||
),
|
||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||
},
|
||||
{
|
||||
provide: SearchServiceAbstraction,
|
||||
useClass: SearchService,
|
||||
deps: [CipherServiceAbstraction, LogService, I18nServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: NotificationsServiceAbstraction,
|
||||
useFactory: (
|
||||
syncService: SyncServiceAbstraction,
|
||||
appIdService: AppIdServiceAbstraction,
|
||||
apiService: ApiServiceAbstraction,
|
||||
vaultTimeoutService: VaultTimeoutServiceAbstraction,
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
logService: LogService,
|
||||
stateService: StateServiceAbstraction
|
||||
) =>
|
||||
new NotificationsService(
|
||||
syncService,
|
||||
appIdService,
|
||||
apiService,
|
||||
vaultTimeoutService,
|
||||
environmentService,
|
||||
async () => messagingService.send("logout", { expired: true }),
|
||||
logService,
|
||||
stateService
|
||||
),
|
||||
deps: [
|
||||
SyncServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
VaultTimeoutServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: EventServiceAbstraction,
|
||||
useClass: EventService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: PolicyServiceAbstraction,
|
||||
useClass: PolicyService,
|
||||
deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: SendServiceAbstraction,
|
||||
useClass: SendService,
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
FileUploadServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: KeyConnectorServiceAbstraction,
|
||||
useClass: KeyConnectorService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: UserVerificationServiceAbstraction,
|
||||
useClass: UserVerificationService,
|
||||
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
|
||||
{
|
||||
provide: OrganizationServiceAbstraction,
|
||||
useClass: OrganizationService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: ProviderServiceAbstraction,
|
||||
useClass: ProviderService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: TwoFactorServiceAbstraction,
|
||||
useClass: TwoFactorService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
29
jslib/angular/src/services/lock-guard.service.ts
Normal file
29
jslib/angular/src/services/lock-guard.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate, Router } from "@angular/router";
|
||||
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
|
||||
@Injectable()
|
||||
export class LockGuardService implements CanActivate {
|
||||
protected homepage = "vault";
|
||||
protected loginpage = "login";
|
||||
constructor(
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private router: Router,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async canActivate() {
|
||||
if (await this.vaultTimeoutService.isLocked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const redirectUrl = (await this.stateService.getIsAuthenticated())
|
||||
? [this.homepage]
|
||||
: [this.loginpage];
|
||||
|
||||
this.router.navigate(redirectUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
180
jslib/angular/src/services/modal.service.ts
Normal file
180
jslib/angular/src/services/modal.service.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
ApplicationRef,
|
||||
ComponentFactory,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
Injectable,
|
||||
Injector,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
|
||||
import { ModalInjector } from "../components/modal/modal-injector";
|
||||
import { ModalRef } from "../components/modal/modal.ref";
|
||||
|
||||
export class ModalConfig<D = any> {
|
||||
data?: D;
|
||||
allowMultipleModals = false;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ModalService {
|
||||
protected modalList: ComponentRef<DynamicModalComponent>[] = [];
|
||||
|
||||
// Lazy loaded modules are not available in componentFactoryResolver,
|
||||
// therefore modules needs to manually initialize their resolvers.
|
||||
private factoryResolvers: Map<Type<any>, ComponentFactoryResolver> = new Map();
|
||||
|
||||
constructor(
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private applicationRef: ApplicationRef,
|
||||
private injector: Injector
|
||||
) {
|
||||
document.addEventListener("keyup", (event) => {
|
||||
if (event.key === "Escape" && this.modalCount > 0) {
|
||||
this.topModal.instance.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get modalCount() {
|
||||
return this.modalList.length;
|
||||
}
|
||||
|
||||
private get topModal() {
|
||||
return this.modalList[this.modalCount - 1];
|
||||
}
|
||||
|
||||
async openViewRef<T>(
|
||||
componentType: Type<T>,
|
||||
viewContainerRef: ViewContainerRef,
|
||||
setComponentParameters: (component: T) => void = null
|
||||
): Promise<[ModalRef, T]> {
|
||||
const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false);
|
||||
modalComponentRef.instance.setComponentParameters = setComponentParameters;
|
||||
|
||||
viewContainerRef.insert(modalComponentRef.hostView);
|
||||
|
||||
await modalRef.onCreated.pipe(first()).toPromise();
|
||||
|
||||
return [modalRef, modalComponentRef.instance.componentRef.instance];
|
||||
}
|
||||
|
||||
open(componentType: Type<any>, config?: ModalConfig) {
|
||||
if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [modalRef, _] = this.openInternal(componentType, config, true);
|
||||
|
||||
return modalRef;
|
||||
}
|
||||
|
||||
registerComponentFactoryResolver<T>(
|
||||
componentType: Type<T>,
|
||||
componentFactoryResolver: ComponentFactoryResolver
|
||||
): void {
|
||||
this.factoryResolvers.set(componentType, componentFactoryResolver);
|
||||
}
|
||||
|
||||
resolveComponentFactory<T>(componentType: Type<T>): ComponentFactory<T> {
|
||||
if (this.factoryResolvers.has(componentType)) {
|
||||
return this.factoryResolvers.get(componentType).resolveComponentFactory(componentType);
|
||||
}
|
||||
|
||||
return this.componentFactoryResolver.resolveComponentFactory(componentType);
|
||||
}
|
||||
|
||||
protected openInternal(
|
||||
componentType: Type<any>,
|
||||
config?: ModalConfig,
|
||||
attachToDom?: boolean
|
||||
): [ModalRef, ComponentRef<DynamicModalComponent>] {
|
||||
const [modalRef, componentRef] = this.createModalComponent(config);
|
||||
componentRef.instance.childComponentType = componentType;
|
||||
|
||||
if (attachToDom) {
|
||||
this.applicationRef.attachView(componentRef.hostView);
|
||||
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
||||
document.body.appendChild(domElem);
|
||||
}
|
||||
|
||||
modalRef.onClosed.pipe(first()).subscribe(() => {
|
||||
if (attachToDom) {
|
||||
this.applicationRef.detachView(componentRef.hostView);
|
||||
}
|
||||
componentRef.destroy();
|
||||
|
||||
this.modalList.pop();
|
||||
if (this.modalCount > 0) {
|
||||
this.topModal.instance.getFocus();
|
||||
}
|
||||
});
|
||||
|
||||
this.setupHandlers(modalRef);
|
||||
|
||||
this.modalList.push(componentRef);
|
||||
|
||||
return [modalRef, componentRef];
|
||||
}
|
||||
|
||||
protected setupHandlers(modalRef: ModalRef) {
|
||||
let backdrop: HTMLElement = null;
|
||||
|
||||
// Add backdrop, setup [data-dismiss] handler.
|
||||
modalRef.onCreated.pipe(first()).subscribe((el) => {
|
||||
document.body.classList.add("modal-open");
|
||||
|
||||
const modalEl: HTMLElement = el.querySelector(".modal");
|
||||
const dialogEl = modalEl.querySelector(".modal-dialog") as HTMLElement;
|
||||
|
||||
backdrop = document.createElement("div");
|
||||
backdrop.className = "modal-backdrop fade";
|
||||
backdrop.style.zIndex = `${this.modalCount}040`;
|
||||
modalEl.prepend(backdrop);
|
||||
|
||||
dialogEl.addEventListener("click", (e: Event) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
dialogEl.style.zIndex = `${this.modalCount}050`;
|
||||
|
||||
const modals = Array.from(
|
||||
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]')
|
||||
);
|
||||
for (const closeElement of modals) {
|
||||
closeElement.addEventListener("click", () => {
|
||||
modalRef.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// onClose is used in Web to hook into bootstrap. On other projects we pipe it directly to closed.
|
||||
modalRef.onClose.pipe(first()).subscribe(() => {
|
||||
modalRef.closed();
|
||||
|
||||
if (this.modalCount === 0) {
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected createModalComponent(
|
||||
config: ModalConfig
|
||||
): [ModalRef, ComponentRef<DynamicModalComponent>] {
|
||||
const modalRef = new ModalRef();
|
||||
|
||||
const map = new WeakMap();
|
||||
map.set(ModalConfig, config);
|
||||
map.set(ModalRef, modalRef);
|
||||
|
||||
const componentFactory =
|
||||
this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent);
|
||||
const componentRef = componentFactory.create(new ModalInjector(this.injector, map));
|
||||
|
||||
return [modalRef, componentRef];
|
||||
}
|
||||
}
|
||||
45
jslib/angular/src/services/passwordReprompt.service.ts
Normal file
45
jslib/angular/src/services/passwordReprompt.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
||||
|
||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
/**
|
||||
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||
* See UserVerificationService for any other situation where you need to verify the user's identity.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
|
||||
protected component = PasswordRepromptComponent;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
) {}
|
||||
|
||||
protectedFields() {
|
||||
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
|
||||
}
|
||||
|
||||
async showPasswordPrompt() {
|
||||
if (!(await this.enabled())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ref = this.modalService.open(this.component, { allowMultipleModals: true });
|
||||
|
||||
if (ref == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await ref.onClosedPromise();
|
||||
return result === true;
|
||||
}
|
||||
|
||||
async enabled() {
|
||||
return !(await this.keyConnectorService.getUsesKeyConnector());
|
||||
}
|
||||
}
|
||||
29
jslib/angular/src/services/unauth-guard.service.ts
Normal file
29
jslib/angular/src/services/unauth-guard.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate, Router } from "@angular/router";
|
||||
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
|
||||
@Injectable()
|
||||
export class UnauthGuardService implements CanActivate {
|
||||
protected homepage = "vault";
|
||||
constructor(
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private router: Router,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (isAuthed) {
|
||||
const locked = await this.vaultTimeoutService.isLocked();
|
||||
if (locked) {
|
||||
this.router.navigate(["lock"]);
|
||||
} else {
|
||||
this.router.navigate([this.homepage]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
38
jslib/angular/src/services/validation.service.ts
Normal file
38
jslib/angular/src/services/validation.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
|
||||
|
||||
@Injectable()
|
||||
export class ValidationService {
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
showError(data: any): string[] {
|
||||
const defaultErrorMessage = this.i18nService.t("unexpectedError");
|
||||
let errors: string[] = [];
|
||||
|
||||
if (data != null && typeof data === "string") {
|
||||
errors.push(data);
|
||||
} else if (data == null || typeof data !== "object") {
|
||||
errors.push(defaultErrorMessage);
|
||||
} else if (data.validationErrors != null) {
|
||||
errors = errors.concat((data as ErrorResponse).getAllMessages());
|
||||
} else {
|
||||
errors.push(data.message ? data.message : defaultErrorMessage);
|
||||
}
|
||||
|
||||
if (errors.length === 1) {
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors[0]);
|
||||
} else if (errors.length > 1) {
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors, {
|
||||
timeout: 5000 * errors.length,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
83
jslib/common/spec/domain/attachment.spec.ts
Normal file
83
jslib/common/spec/domain/attachment.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { AttachmentData } from "@/jslib/common/src/models/data/attachmentData";
|
||||
import { Attachment } from "@/jslib/common/src/models/domain/attachment";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
|
||||
import { makeStaticByteArray, mockEnc } from "../utils";
|
||||
|
||||
describe("Attachment", () => {
|
||||
let data: AttachmentData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
url: "url",
|
||||
fileName: "fileName",
|
||||
key: "key",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new AttachmentData();
|
||||
const attachment = new Attachment(data);
|
||||
|
||||
expect(attachment).toEqual({
|
||||
id: null,
|
||||
url: null,
|
||||
size: undefined,
|
||||
sizeName: null,
|
||||
key: null,
|
||||
fileName: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const attachment = new Attachment(data);
|
||||
|
||||
expect(attachment).toEqual({
|
||||
size: "1100",
|
||||
id: "id",
|
||||
url: "url",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: { encryptedString: "fileName", encryptionType: 0 },
|
||||
key: { encryptedString: "key", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toAttachmentData", () => {
|
||||
const attachment = new Attachment(data);
|
||||
expect(attachment.toAttachmentData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const attachment = new Attachment();
|
||||
attachment.id = "id";
|
||||
attachment.url = "url";
|
||||
attachment.size = "1100";
|
||||
attachment.sizeName = "1.1 KB";
|
||||
attachment.key = mockEnc("key");
|
||||
attachment.fileName = mockEnc("fileName");
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
cryptoService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(32));
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
const view = await attachment.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
id: "id",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "fileName",
|
||||
key: expect.any(SymmetricCryptoKey),
|
||||
});
|
||||
});
|
||||
});
|
||||
73
jslib/common/spec/domain/card.spec.ts
Normal file
73
jslib/common/spec/domain/card.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { CardData } from "@/jslib/common/src/models/data/cardData";
|
||||
import { Card } from "@/jslib/common/src/models/domain/card";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Card", () => {
|
||||
let data: CardData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
cardholderName: "encHolder",
|
||||
brand: "encBrand",
|
||||
number: "encNumber",
|
||||
expMonth: "encMonth",
|
||||
expYear: "encYear",
|
||||
code: "encCode",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new CardData();
|
||||
const card = new Card(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const card = new Card(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
cardholderName: { encryptedString: "encHolder", encryptionType: 0 },
|
||||
brand: { encryptedString: "encBrand", encryptionType: 0 },
|
||||
number: { encryptedString: "encNumber", encryptionType: 0 },
|
||||
expMonth: { encryptedString: "encMonth", encryptionType: 0 },
|
||||
expYear: { encryptedString: "encYear", encryptionType: 0 },
|
||||
code: { encryptedString: "encCode", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toCardData", () => {
|
||||
const card = new Card(data);
|
||||
expect(card.toCardData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const card = new Card();
|
||||
card.cardholderName = mockEnc("cardHolder");
|
||||
card.brand = mockEnc("brand");
|
||||
card.number = mockEnc("number");
|
||||
card.expMonth = mockEnc("expMonth");
|
||||
card.expYear = mockEnc("expYear");
|
||||
card.code = mockEnc("code");
|
||||
|
||||
const view = await card.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_brand: "brand",
|
||||
_number: "number",
|
||||
_subTitle: null,
|
||||
cardholderName: "cardHolder",
|
||||
code: "code",
|
||||
expMonth: "expMonth",
|
||||
expYear: "expYear",
|
||||
});
|
||||
});
|
||||
});
|
||||
599
jslib/common/spec/domain/cipher.spec.ts
Normal file
599
jslib/common/spec/domain/cipher.spec.ts
Normal file
@@ -0,0 +1,599 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { CipherRepromptType } from "@/jslib/common/src/enums/cipherRepromptType";
|
||||
import { CipherType } from "@/jslib/common/src/enums/cipherType";
|
||||
import { FieldType } from "@/jslib/common/src/enums/fieldType";
|
||||
import { SecureNoteType } from "@/jslib/common/src/enums/secureNoteType";
|
||||
import { UriMatchType } from "@/jslib/common/src/enums/uriMatchType";
|
||||
import { CipherData } from "@/jslib/common/src/models/data/cipherData";
|
||||
import { Card } from "@/jslib/common/src/models/domain/card";
|
||||
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
|
||||
import { Identity } from "@/jslib/common/src/models/domain/identity";
|
||||
import { Login } from "@/jslib/common/src/models/domain/login";
|
||||
import { SecureNote } from "@/jslib/common/src/models/domain/secureNote";
|
||||
import { CardView } from "@/jslib/common/src/models/view/cardView";
|
||||
import { IdentityView } from "@/jslib/common/src/models/view/identityView";
|
||||
import { LoginView } from "@/jslib/common/src/models/view/loginView";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Cipher DTO", () => {
|
||||
it("Convert from empty CipherData", () => {
|
||||
const data = new CipherData();
|
||||
const cipher = new Cipher(data);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
id: null,
|
||||
userId: null,
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: null,
|
||||
notes: null,
|
||||
type: undefined,
|
||||
favorite: undefined,
|
||||
organizationUseTotp: undefined,
|
||||
edit: undefined,
|
||||
viewPassword: true,
|
||||
revisionDate: null,
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
deletedDate: null,
|
||||
reprompt: undefined,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoginCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
userId: "userId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Login,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
login: {
|
||||
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
|
||||
username: "EncryptedString",
|
||||
password: "EncryptedString",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "EncryptedString",
|
||||
autofillOnPageLoad: false,
|
||||
},
|
||||
passwordHistory: [
|
||||
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
|
||||
],
|
||||
attachments: [
|
||||
{
|
||||
id: "a1",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
},
|
||||
{
|
||||
id: "a2",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
type: FieldType.Text,
|
||||
linkedId: null,
|
||||
},
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
type: FieldType.Hidden,
|
||||
linkedId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
id: "id",
|
||||
userId: "userId",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
login: {
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
autofillOnPageLoad: false,
|
||||
username: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
password: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
totp: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
uris: [{ match: 0, uri: { encryptedString: "EncryptedString", encryptionType: 0 } }],
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
fileName: { encryptedString: "file", encryptionType: 0 },
|
||||
id: "a1",
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
url: "url",
|
||||
},
|
||||
{
|
||||
fileName: { encryptedString: "file", encryptionType: 0 },
|
||||
id: "a2",
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
url: "url",
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
linkedId: null,
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 0,
|
||||
value: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
{
|
||||
linkedId: null,
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 1,
|
||||
value: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
],
|
||||
passwordHistory: [
|
||||
{
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
password: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData("userId")).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Login;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const loginView = new LoginView();
|
||||
loginView.username = "username";
|
||||
loginView.password = "password";
|
||||
|
||||
const login = Substitute.for<Login>();
|
||||
login.decrypt(Arg.any(), Arg.any()).resolves(loginView);
|
||||
cipher.login = login;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
login: loginView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SecureNoteCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
userId: "userId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.SecureNote,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
secureNote: {
|
||||
type: SecureNoteType.Generic,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
id: "id",
|
||||
userId: "userId",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 2,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
secureNote: { type: SecureNoteType.Generic },
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData("userId")).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.SecureNote;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
cipher.secureNote = new SecureNote();
|
||||
cipher.secureNote.type = SecureNoteType.Generic;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 2,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
secureNote: { type: 0 },
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CardCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
userId: "userId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Card,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
card: {
|
||||
cardholderName: "EncryptedString",
|
||||
brand: "EncryptedString",
|
||||
number: "EncryptedString",
|
||||
expMonth: "EncryptedString",
|
||||
expYear: "EncryptedString",
|
||||
code: "EncryptedString",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
id: "id",
|
||||
userId: "userId",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 3,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
card: {
|
||||
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
brand: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
number: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
expMonth: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
expYear: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
code: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData("userId")).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const cardView = new CardView();
|
||||
cardView.cardholderName = "cardholderName";
|
||||
cardView.number = "4111111111111111";
|
||||
|
||||
const card = Substitute.for<Card>();
|
||||
card.decrypt(Arg.any(), Arg.any()).resolves(cardView);
|
||||
cipher.card = card;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 3,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
card: cardView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IdentityCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
userId: "userId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Identity,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
identity: {
|
||||
title: "EncryptedString",
|
||||
firstName: "EncryptedString",
|
||||
middleName: "EncryptedString",
|
||||
lastName: "EncryptedString",
|
||||
address1: "EncryptedString",
|
||||
address2: "EncryptedString",
|
||||
address3: "EncryptedString",
|
||||
city: "EncryptedString",
|
||||
state: "EncryptedString",
|
||||
postalCode: "EncryptedString",
|
||||
country: "EncryptedString",
|
||||
company: "EncryptedString",
|
||||
email: "EncryptedString",
|
||||
phone: "EncryptedString",
|
||||
ssn: "EncryptedString",
|
||||
username: "EncryptedString",
|
||||
passportNumber: "EncryptedString",
|
||||
licenseNumber: "EncryptedString",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
id: "id",
|
||||
userId: "userId",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 4,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
identity: {
|
||||
title: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
middleName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
lastName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address1: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address2: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address3: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
city: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
state: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
postalCode: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
country: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
company: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
email: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
phone: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
ssn: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
username: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData("userId")).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Identity;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const identityView = new IdentityView();
|
||||
identityView.firstName = "firstName";
|
||||
identityView.lastName = "lastName";
|
||||
|
||||
const identity = Substitute.for<Identity>();
|
||||
identity.decrypt(Arg.any(), Arg.any()).resolves(identityView);
|
||||
cipher.identity = identity;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 4,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
identity: identityView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
66
jslib/common/spec/domain/collection.spec.ts
Normal file
66
jslib/common/spec/domain/collection.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { CollectionData } from "@/jslib/common/src/models/data/collectionData";
|
||||
import { Collection } from "@/jslib/common/src/models/domain/collection";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Collection", () => {
|
||||
let data: CollectionData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
name: "encName",
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new CollectionData({} as any);
|
||||
const card = new Collection(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
externalId: null,
|
||||
hidePasswords: null,
|
||||
id: null,
|
||||
name: null,
|
||||
organizationId: null,
|
||||
readOnly: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const collection = new Collection(data);
|
||||
|
||||
expect(collection).toEqual({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
hidePasswords: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const collection = new Collection();
|
||||
collection.id = "id";
|
||||
collection.organizationId = "orgId";
|
||||
collection.name = mockEnc("encName");
|
||||
collection.externalId = "extId";
|
||||
collection.readOnly = false;
|
||||
collection.hidePasswords = false;
|
||||
|
||||
const view = await collection.decrypt();
|
||||
|
||||
expect(view).toEqual({
|
||||
externalId: "extId",
|
||||
hidePasswords: false,
|
||||
id: "id",
|
||||
name: "encName",
|
||||
organizationId: "orgId",
|
||||
readOnly: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
195
jslib/common/spec/domain/encString.spec.ts
Normal file
195
jslib/common/spec/domain/encString.spec.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
|
||||
describe("EncString", () => {
|
||||
afterEach(() => {
|
||||
(window as any).bitwardenContainerService = undefined;
|
||||
});
|
||||
|
||||
describe("Rsa2048_OaepSha256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "3.data",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
|
||||
describe("parse existing", () => {
|
||||
it("valid", () => {
|
||||
const encString = new EncString("3.data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "3.data",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("3.data|test");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "3.data|test",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
});
|
||||
|
||||
it("decrypts correctly", async () => {
|
||||
const decrypted = await encString.decrypt(null);
|
||||
|
||||
expect(decrypted).toBe("decrypted");
|
||||
});
|
||||
|
||||
it("result should be cached", async () => {
|
||||
const decrypted = await encString.decrypt(null);
|
||||
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
|
||||
|
||||
expect(decrypted).toBe("decrypted");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AesCbc256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "0.iv|data",
|
||||
encryptionType: 0,
|
||||
iv: "iv",
|
||||
});
|
||||
});
|
||||
|
||||
describe("parse existing", () => {
|
||||
it("valid", () => {
|
||||
const encString = new EncString("0.iv|data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "0.iv|data",
|
||||
encryptionType: 0,
|
||||
iv: "iv",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("0.iv|data|mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "0.iv|data|mac",
|
||||
encryptionType: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AesCbc256_HmacSha256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "2.iv|data|mac",
|
||||
encryptionType: 2,
|
||||
iv: "iv",
|
||||
mac: "mac",
|
||||
});
|
||||
});
|
||||
|
||||
it("valid", () => {
|
||||
const encString = new EncString("2.iv|data|mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "2.iv|data|mac",
|
||||
encryptionType: 2,
|
||||
iv: "iv",
|
||||
mac: "mac",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("2.iv|data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "2.iv|data",
|
||||
encryptionType: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Exit early if null", () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
it("throws exception when bitwarden container not initialized", async () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await encString.decrypt(null);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
|
||||
}
|
||||
});
|
||||
|
||||
it("handles value it can't decrypt", async () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
const decrypted = await encString.decrypt(null);
|
||||
|
||||
expect(decrypted).toBe("[error: cannot decrypt]");
|
||||
|
||||
expect(encString).toEqual({
|
||||
decryptedValue: "[error: cannot decrypt]",
|
||||
encryptedString: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes along key", async () => {
|
||||
const encString = new EncString(null);
|
||||
const key = Substitute.for<SymmetricCryptoKey>();
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
cryptoService.received().decryptToUtf8(encString, key);
|
||||
});
|
||||
});
|
||||
});
|
||||
64
jslib/common/spec/domain/field.spec.ts
Normal file
64
jslib/common/spec/domain/field.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { FieldType } from "@/jslib/common/src/enums/fieldType";
|
||||
import { FieldData } from "@/jslib/common/src/models/data/fieldData";
|
||||
import { Field } from "@/jslib/common/src/models/domain/field";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Field", () => {
|
||||
let data: FieldData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
type: FieldType.Text,
|
||||
name: "encName",
|
||||
value: "encValue",
|
||||
linkedId: null,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new FieldData();
|
||||
const field = new Field(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
type: undefined,
|
||||
name: null,
|
||||
value: null,
|
||||
linkedId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const field = new Field(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
type: FieldType.Text,
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
value: { encryptedString: "encValue", encryptionType: 0 },
|
||||
linkedId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toFieldData", () => {
|
||||
const field = new Field(data);
|
||||
expect(field.toFieldData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const field = new Field();
|
||||
field.type = FieldType.Text;
|
||||
field.name = mockEnc("encName");
|
||||
field.value = mockEnc("encValue");
|
||||
|
||||
const view = await field.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
type: 0,
|
||||
name: "encName",
|
||||
value: "encValue",
|
||||
newField: false,
|
||||
showCount: false,
|
||||
showValue: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
42
jslib/common/spec/domain/folder.spec.ts
Normal file
42
jslib/common/spec/domain/folder.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FolderData } from "@/jslib/common/src/models/data/folderData";
|
||||
import { Folder } from "@/jslib/common/src/models/domain/folder";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Folder", () => {
|
||||
let data: FolderData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
userId: "userId",
|
||||
name: "encName",
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const field = new Folder(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
id: "id",
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const folder = new Folder();
|
||||
folder.id = "id";
|
||||
folder.name = mockEnc("encName");
|
||||
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const view = await folder.decrypt();
|
||||
|
||||
expect(view).toEqual({
|
||||
id: "id",
|
||||
name: "encName",
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
});
|
||||
134
jslib/common/spec/domain/identity.spec.ts
Normal file
134
jslib/common/spec/domain/identity.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { IdentityData } from "@/jslib/common/src/models/data/identityData";
|
||||
import { Identity } from "@/jslib/common/src/models/domain/identity";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Identity", () => {
|
||||
let data: IdentityData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
title: "enctitle",
|
||||
firstName: "encfirstName",
|
||||
middleName: "encmiddleName",
|
||||
lastName: "enclastName",
|
||||
address1: "encaddress1",
|
||||
address2: "encaddress2",
|
||||
address3: "encaddress3",
|
||||
city: "enccity",
|
||||
state: "encstate",
|
||||
postalCode: "encpostalCode",
|
||||
country: "enccountry",
|
||||
company: "enccompany",
|
||||
email: "encemail",
|
||||
phone: "encphone",
|
||||
ssn: "encssn",
|
||||
username: "encusername",
|
||||
passportNumber: "encpassportNumber",
|
||||
licenseNumber: "enclicenseNumber",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new IdentityData();
|
||||
const identity = new Identity(data);
|
||||
|
||||
expect(identity).toEqual({
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
company: null,
|
||||
country: null,
|
||||
email: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
licenseNumber: null,
|
||||
middleName: null,
|
||||
passportNumber: null,
|
||||
phone: null,
|
||||
postalCode: null,
|
||||
ssn: null,
|
||||
state: null,
|
||||
title: null,
|
||||
username: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const identity = new Identity(data);
|
||||
|
||||
expect(identity).toEqual({
|
||||
title: { encryptedString: "enctitle", encryptionType: 0 },
|
||||
firstName: { encryptedString: "encfirstName", encryptionType: 0 },
|
||||
middleName: { encryptedString: "encmiddleName", encryptionType: 0 },
|
||||
lastName: { encryptedString: "enclastName", encryptionType: 0 },
|
||||
address1: { encryptedString: "encaddress1", encryptionType: 0 },
|
||||
address2: { encryptedString: "encaddress2", encryptionType: 0 },
|
||||
address3: { encryptedString: "encaddress3", encryptionType: 0 },
|
||||
city: { encryptedString: "enccity", encryptionType: 0 },
|
||||
state: { encryptedString: "encstate", encryptionType: 0 },
|
||||
postalCode: { encryptedString: "encpostalCode", encryptionType: 0 },
|
||||
country: { encryptedString: "enccountry", encryptionType: 0 },
|
||||
company: { encryptedString: "enccompany", encryptionType: 0 },
|
||||
email: { encryptedString: "encemail", encryptionType: 0 },
|
||||
phone: { encryptedString: "encphone", encryptionType: 0 },
|
||||
ssn: { encryptedString: "encssn", encryptionType: 0 },
|
||||
username: { encryptedString: "encusername", encryptionType: 0 },
|
||||
passportNumber: { encryptedString: "encpassportNumber", encryptionType: 0 },
|
||||
licenseNumber: { encryptedString: "enclicenseNumber", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toIdentityData", () => {
|
||||
const identity = new Identity(data);
|
||||
expect(identity.toIdentityData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const identity = new Identity();
|
||||
|
||||
identity.title = mockEnc("mockTitle");
|
||||
identity.firstName = mockEnc("mockFirstName");
|
||||
identity.middleName = mockEnc("mockMiddleName");
|
||||
identity.lastName = mockEnc("mockLastName");
|
||||
identity.address1 = mockEnc("mockAddress1");
|
||||
identity.address2 = mockEnc("mockAddress2");
|
||||
identity.address3 = mockEnc("mockAddress3");
|
||||
identity.city = mockEnc("mockCity");
|
||||
identity.state = mockEnc("mockState");
|
||||
identity.postalCode = mockEnc("mockPostalCode");
|
||||
identity.country = mockEnc("mockCountry");
|
||||
identity.company = mockEnc("mockCompany");
|
||||
identity.email = mockEnc("mockEmail");
|
||||
identity.phone = mockEnc("mockPhone");
|
||||
identity.ssn = mockEnc("mockSsn");
|
||||
identity.username = mockEnc("mockUsername");
|
||||
identity.passportNumber = mockEnc("mockPassportNumber");
|
||||
identity.licenseNumber = mockEnc("mockLicenseNumber");
|
||||
|
||||
const view = await identity.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_firstName: "mockFirstName",
|
||||
_lastName: "mockLastName",
|
||||
_subTitle: null,
|
||||
address1: "mockAddress1",
|
||||
address2: "mockAddress2",
|
||||
address3: "mockAddress3",
|
||||
city: "mockCity",
|
||||
company: "mockCompany",
|
||||
country: "mockCountry",
|
||||
email: "mockEmail",
|
||||
licenseNumber: "mockLicenseNumber",
|
||||
middleName: "mockMiddleName",
|
||||
passportNumber: "mockPassportNumber",
|
||||
phone: "mockPhone",
|
||||
postalCode: "mockPostalCode",
|
||||
ssn: "mockSsn",
|
||||
state: "mockState",
|
||||
title: "mockTitle",
|
||||
username: "mockUsername",
|
||||
});
|
||||
});
|
||||
});
|
||||
101
jslib/common/spec/domain/login.spec.ts
Normal file
101
jslib/common/spec/domain/login.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { UriMatchType } from "@/jslib/common/src/enums/uriMatchType";
|
||||
import { LoginData } from "@/jslib/common/src/models/data/loginData";
|
||||
import { Login } from "@/jslib/common/src/models/domain/login";
|
||||
import { LoginUri } from "@/jslib/common/src/models/domain/loginUri";
|
||||
import { LoginUriView } from "@/jslib/common/src/models/view/loginUriView";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Login DTO", () => {
|
||||
it("Convert from empty LoginData", () => {
|
||||
const data = new LoginData();
|
||||
const login = new Login(data);
|
||||
|
||||
expect(login).toEqual({
|
||||
passwordRevisionDate: null,
|
||||
autofillOnPageLoad: undefined,
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert from full LoginData", () => {
|
||||
const data: LoginData = {
|
||||
uris: [{ uri: "uri", match: UriMatchType.Domain }],
|
||||
username: "username",
|
||||
password: "password",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "123",
|
||||
autofillOnPageLoad: false,
|
||||
};
|
||||
const login = new Login(data);
|
||||
|
||||
expect(login).toEqual({
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
autofillOnPageLoad: false,
|
||||
username: { encryptedString: "username", encryptionType: 0 },
|
||||
password: { encryptedString: "password", encryptionType: 0 },
|
||||
totp: { encryptedString: "123", encryptionType: 0 },
|
||||
uris: [{ match: 0, uri: { encryptedString: "uri", encryptionType: 0 } }],
|
||||
});
|
||||
});
|
||||
|
||||
it("Initialize without LoginData", () => {
|
||||
const login = new Login();
|
||||
|
||||
expect(login).toEqual({});
|
||||
});
|
||||
|
||||
it("Decrypts correctly", async () => {
|
||||
const loginUri = Substitute.for<LoginUri>();
|
||||
const loginUriView = new LoginUriView();
|
||||
loginUriView.uri = "decrypted uri";
|
||||
loginUri.decrypt(Arg.any()).resolves(loginUriView);
|
||||
|
||||
const login = new Login();
|
||||
login.uris = [loginUri];
|
||||
login.username = mockEnc("encrypted username");
|
||||
login.password = mockEnc("encrypted password");
|
||||
login.passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
login.totp = mockEnc("encrypted totp");
|
||||
login.autofillOnPageLoad = true;
|
||||
|
||||
const loginView = await login.decrypt(null);
|
||||
expect(loginView).toEqual({
|
||||
username: "encrypted username",
|
||||
password: "encrypted password",
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
totp: "encrypted totp",
|
||||
uris: [
|
||||
{
|
||||
match: null,
|
||||
_uri: "decrypted uri",
|
||||
_domain: null,
|
||||
_hostname: null,
|
||||
_host: null,
|
||||
_canLaunch: null,
|
||||
},
|
||||
],
|
||||
autofillOnPageLoad: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("Converts from LoginData and back", () => {
|
||||
const data: LoginData = {
|
||||
uris: [{ uri: "uri", match: UriMatchType.Domain }],
|
||||
username: "username",
|
||||
password: "password",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "123",
|
||||
autofillOnPageLoad: false,
|
||||
};
|
||||
const login = new Login(data);
|
||||
|
||||
const loginData = login.toLoginData();
|
||||
|
||||
expect(loginData).toEqual(data);
|
||||
});
|
||||
});
|
||||
57
jslib/common/spec/domain/loginUri.spec.ts
Normal file
57
jslib/common/spec/domain/loginUri.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { UriMatchType } from "@/jslib/common/src/enums/uriMatchType";
|
||||
import { LoginUriData } from "@/jslib/common/src/models/data/loginUriData";
|
||||
import { LoginUri } from "@/jslib/common/src/models/domain/loginUri";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("LoginUri", () => {
|
||||
let data: LoginUriData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
uri: "encUri",
|
||||
match: UriMatchType.Domain,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new LoginUriData();
|
||||
const loginUri = new LoginUri(data);
|
||||
|
||||
expect(loginUri).toEqual({
|
||||
match: null,
|
||||
uri: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const loginUri = new LoginUri(data);
|
||||
|
||||
expect(loginUri).toEqual({
|
||||
match: 0,
|
||||
uri: { encryptedString: "encUri", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toLoginUriData", () => {
|
||||
const loginUri = new LoginUri(data);
|
||||
expect(loginUri.toLoginUriData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const loginUri = new LoginUri();
|
||||
loginUri.match = UriMatchType.Exact;
|
||||
loginUri.uri = mockEnc("uri");
|
||||
|
||||
const view = await loginUri.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_canLaunch: null,
|
||||
_domain: null,
|
||||
_host: null,
|
||||
_hostname: null,
|
||||
_uri: "uri",
|
||||
match: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
51
jslib/common/spec/domain/password.spec.ts
Normal file
51
jslib/common/spec/domain/password.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { PasswordHistoryData } from "@/jslib/common/src/models/data/passwordHistoryData";
|
||||
import { Password } from "@/jslib/common/src/models/domain/password";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("Password", () => {
|
||||
let data: PasswordHistoryData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
password: "encPassword",
|
||||
lastUsedDate: "2022-01-31T12:00:00.000Z",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new PasswordHistoryData();
|
||||
const password = new Password(data);
|
||||
|
||||
expect(password).toMatchObject({
|
||||
password: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const password = new Password(data);
|
||||
|
||||
expect(password).toEqual({
|
||||
password: { encryptedString: "encPassword", encryptionType: 0 },
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("toPasswordHistoryData", () => {
|
||||
const password = new Password(data);
|
||||
expect(password.toPasswordHistoryData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const password = new Password();
|
||||
password.password = mockEnc("password");
|
||||
password.lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const view = await password.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
password: "password",
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
});
|
||||
46
jslib/common/spec/domain/secureNote.spec.ts
Normal file
46
jslib/common/spec/domain/secureNote.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { SecureNoteType } from "@/jslib/common/src/enums/secureNoteType";
|
||||
import { SecureNoteData } from "@/jslib/common/src/models/data/secureNoteData";
|
||||
import { SecureNote } from "@/jslib/common/src/models/domain/secureNote";
|
||||
|
||||
describe("SecureNote", () => {
|
||||
let data: SecureNoteData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
type: SecureNoteType.Generic,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new SecureNoteData();
|
||||
const secureNote = new SecureNote(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
type: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const secureNote = new SecureNote(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
type: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("toSecureNoteData", () => {
|
||||
const secureNote = new SecureNote(data);
|
||||
expect(secureNote.toSecureNoteData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const secureNote = new SecureNote();
|
||||
secureNote.type = SecureNoteType.Generic;
|
||||
|
||||
const view = await secureNote.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
type: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
144
jslib/common/spec/domain/send.spec.ts
Normal file
144
jslib/common/spec/domain/send.spec.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Substitute, Arg, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { SendType } from "@/jslib/common/src/enums/sendType";
|
||||
import { SendData } from "@/jslib/common/src/models/data/sendData";
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
import { Send } from "@/jslib/common/src/models/domain/send";
|
||||
import { SendText } from "@/jslib/common/src/models/domain/sendText";
|
||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
|
||||
import { makeStaticByteArray, mockEnc } from "../utils";
|
||||
|
||||
describe("Send", () => {
|
||||
let data: SendData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
accessId: "accessId",
|
||||
userId: "userId",
|
||||
type: SendType.Text,
|
||||
name: "encName",
|
||||
notes: "encNotes",
|
||||
text: {
|
||||
text: "encText",
|
||||
hidden: true,
|
||||
},
|
||||
file: null,
|
||||
key: "encKey",
|
||||
maxAccessCount: null,
|
||||
accessCount: 10,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
expirationDate: "2022-01-31T12:00:00.000Z",
|
||||
deletionDate: "2022-01-31T12:00:00.000Z",
|
||||
password: "password",
|
||||
disabled: false,
|
||||
hideEmail: true,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new SendData();
|
||||
const send = new Send(data);
|
||||
|
||||
expect(send).toEqual({
|
||||
id: null,
|
||||
accessId: null,
|
||||
userId: null,
|
||||
type: undefined,
|
||||
name: null,
|
||||
notes: null,
|
||||
text: undefined,
|
||||
file: undefined,
|
||||
key: null,
|
||||
maxAccessCount: undefined,
|
||||
accessCount: undefined,
|
||||
revisionDate: null,
|
||||
expirationDate: null,
|
||||
deletionDate: null,
|
||||
password: undefined,
|
||||
disabled: undefined,
|
||||
hideEmail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const send = new Send(data);
|
||||
|
||||
expect(send).toEqual({
|
||||
id: "id",
|
||||
accessId: "accessId",
|
||||
userId: "userId",
|
||||
type: SendType.Text,
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
notes: { encryptedString: "encNotes", encryptionType: 0 },
|
||||
text: {
|
||||
text: { encryptedString: "encText", encryptionType: 0 },
|
||||
hidden: true,
|
||||
},
|
||||
key: { encryptedString: "encKey", encryptionType: 0 },
|
||||
maxAccessCount: null,
|
||||
accessCount: 10,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
password: "password",
|
||||
disabled: false,
|
||||
hideEmail: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const text = Substitute.for<SendText>();
|
||||
text.decrypt(Arg.any()).resolves("textView" as any);
|
||||
|
||||
const send = new Send();
|
||||
send.id = "id";
|
||||
send.accessId = "accessId";
|
||||
send.userId = "userId";
|
||||
send.type = SendType.Text;
|
||||
send.name = mockEnc("name");
|
||||
send.notes = mockEnc("notes");
|
||||
send.text = text;
|
||||
send.key = mockEnc("key");
|
||||
send.accessCount = 10;
|
||||
send.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
send.expirationDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
send.deletionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
send.password = "password";
|
||||
send.disabled = false;
|
||||
send.hideEmail = true;
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.decryptToBytes(send.key, null).resolves(makeStaticByteArray(32));
|
||||
cryptoService.makeSendKey(Arg.any()).resolves("cryptoKey" as any);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
const view = await send.decrypt();
|
||||
|
||||
text.received(1).decrypt("cryptoKey" as any);
|
||||
(send.name as SubstituteOf<EncString>).received(1).decrypt(null, "cryptoKey" as any);
|
||||
|
||||
expect(view).toMatchObject({
|
||||
id: "id",
|
||||
accessId: "accessId",
|
||||
name: "name",
|
||||
notes: "notes",
|
||||
type: 0,
|
||||
key: expect.anything(),
|
||||
cryptoKey: "cryptoKey",
|
||||
file: expect.anything(),
|
||||
text: "textView",
|
||||
maxAccessCount: undefined,
|
||||
accessCount: 10,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
password: "password",
|
||||
disabled: false,
|
||||
hideEmail: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
84
jslib/common/spec/domain/sendAccess.spec.ts
Normal file
84
jslib/common/spec/domain/sendAccess.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { SendType } from "@/jslib/common/src/enums/sendType";
|
||||
import { SendAccess } from "@/jslib/common/src/models/domain/sendAccess";
|
||||
import { SendText } from "@/jslib/common/src/models/domain/sendText";
|
||||
import { SendAccessResponse } from "@/jslib/common/src/models/response/sendAccessResponse";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("SendAccess", () => {
|
||||
let request: SendAccessResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
request = {
|
||||
id: "id",
|
||||
type: SendType.Text,
|
||||
name: "encName",
|
||||
file: null,
|
||||
text: {
|
||||
text: "encText",
|
||||
hidden: true,
|
||||
},
|
||||
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creatorIdentifier: "creatorIdentifier",
|
||||
} as SendAccessResponse;
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const request = new SendAccessResponse({});
|
||||
const sendAccess = new SendAccess(request);
|
||||
|
||||
expect(sendAccess).toEqual({
|
||||
id: null,
|
||||
type: undefined,
|
||||
name: null,
|
||||
creatorIdentifier: null,
|
||||
expirationDate: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const sendAccess = new SendAccess(request);
|
||||
|
||||
expect(sendAccess).toEqual({
|
||||
id: "id",
|
||||
type: 0,
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
text: {
|
||||
hidden: true,
|
||||
text: { encryptedString: "encText", encryptionType: 0 },
|
||||
},
|
||||
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creatorIdentifier: "creatorIdentifier",
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const sendAccess = new SendAccess();
|
||||
sendAccess.id = "id";
|
||||
sendAccess.type = SendType.Text;
|
||||
sendAccess.name = mockEnc("name");
|
||||
|
||||
const text = Substitute.for<SendText>();
|
||||
text.decrypt(Arg.any()).resolves({} as any);
|
||||
sendAccess.text = text;
|
||||
|
||||
sendAccess.expirationDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
sendAccess.creatorIdentifier = "creatorIdentifier";
|
||||
|
||||
const view = await sendAccess.decrypt(null);
|
||||
|
||||
text.received(1).decrypt(Arg.any());
|
||||
|
||||
expect(view).toEqual({
|
||||
id: "id",
|
||||
type: 0,
|
||||
name: "name",
|
||||
text: {},
|
||||
file: expect.anything(),
|
||||
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creatorIdentifier: "creatorIdentifier",
|
||||
});
|
||||
});
|
||||
});
|
||||
57
jslib/common/spec/domain/sendFile.spec.ts
Normal file
57
jslib/common/spec/domain/sendFile.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { SendFileData } from "@/jslib/common/src/models/data/sendFileData";
|
||||
import { SendFile } from "@/jslib/common/src/models/domain/sendFile";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("SendFile", () => {
|
||||
let data: SendFileData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "encFileName",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new SendFileData();
|
||||
const sendFile = new SendFile(data);
|
||||
|
||||
expect(sendFile).toEqual({
|
||||
fileName: null,
|
||||
id: null,
|
||||
size: undefined,
|
||||
sizeName: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const sendFile = new SendFile(data);
|
||||
|
||||
expect(sendFile).toEqual({
|
||||
id: "id",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: { encryptedString: "encFileName", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const sendFile = new SendFile();
|
||||
sendFile.id = "id";
|
||||
sendFile.size = "1100";
|
||||
sendFile.sizeName = "1.1 KB";
|
||||
sendFile.fileName = mockEnc("fileName");
|
||||
|
||||
const view = await sendFile.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
fileName: "fileName",
|
||||
id: "id",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
});
|
||||
});
|
||||
});
|
||||
47
jslib/common/spec/domain/sendText.spec.ts
Normal file
47
jslib/common/spec/domain/sendText.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { SendTextData } from "@/jslib/common/src/models/data/sendTextData";
|
||||
import { SendText } from "@/jslib/common/src/models/domain/sendText";
|
||||
|
||||
import { mockEnc } from "../utils";
|
||||
|
||||
describe("SendText", () => {
|
||||
let data: SendTextData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
text: "encText",
|
||||
hidden: false,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new SendTextData();
|
||||
const secureNote = new SendText(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
hidden: undefined,
|
||||
text: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const secureNote = new SendText(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
hidden: false,
|
||||
text: { encryptedString: "encText", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const secureNote = new SendText();
|
||||
secureNote.text = mockEnc("text");
|
||||
secureNote.hidden = true;
|
||||
|
||||
const view = await secureNote.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
text: "text",
|
||||
hidden: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
69
jslib/common/spec/domain/symmetricCryptoKey.spec.ts
Normal file
69
jslib/common/spec/domain/symmetricCryptoKey.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { makeStaticByteArray } from "../utils";
|
||||
|
||||
describe("SymmetricCryptoKey", () => {
|
||||
it("errors if no key", () => {
|
||||
const t = () => {
|
||||
new SymmetricCryptoKey(null);
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Must provide key");
|
||||
});
|
||||
|
||||
describe("guesses encKey from key length", () => {
|
||||
it("AesCbc256_B64", () => {
|
||||
const key = makeStaticByteArray(32);
|
||||
const cryptoKey = new SymmetricCryptoKey(key);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key,
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
encType: 0,
|
||||
key: key,
|
||||
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
macKey: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("AesCbc128_HmacSha256_B64", () => {
|
||||
const key = makeStaticByteArray(32);
|
||||
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key.slice(0, 16),
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
|
||||
encType: 1,
|
||||
key: key,
|
||||
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
macKey: key.slice(16, 32),
|
||||
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
|
||||
});
|
||||
});
|
||||
|
||||
it("AesCbc256_HmacSha256_B64", () => {
|
||||
const key = makeStaticByteArray(64);
|
||||
const cryptoKey = new SymmetricCryptoKey(key);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key.slice(0, 32),
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
encType: 2,
|
||||
key: key,
|
||||
keyB64:
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
|
||||
macKey: key.slice(32, 64),
|
||||
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
|
||||
});
|
||||
});
|
||||
|
||||
it("unknown length", () => {
|
||||
const t = () => {
|
||||
new SymmetricCryptoKey(makeStaticByteArray(30));
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Unable to determine encType.");
|
||||
});
|
||||
});
|
||||
});
|
||||
114
jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
114
jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("ApiLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
|
||||
let apiLogInStrategy: ApiLogInStrategy;
|
||||
let credentials: ApiLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
const apiClientId = "API_CLIENT_ID";
|
||||
const apiClientSecret = "API_CLIENT_SECRET";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
|
||||
apiLogInStrategy = new ApiLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
environmentService,
|
||||
keyConnectorService
|
||||
);
|
||||
|
||||
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
|
||||
});
|
||||
|
||||
it("sends api key credentials to the server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const apiTokenRequest = actual as any;
|
||||
return (
|
||||
apiTokenRequest.clientId === apiClientId &&
|
||||
apiTokenRequest.clientSecret === apiClientSecret &&
|
||||
apiTokenRequest.device.identifier === deviceId &&
|
||||
apiTokenRequest.twoFactor.provider == null &&
|
||||
apiTokenRequest.twoFactor.token == null &&
|
||||
apiTokenRequest.captchaResponse == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).setApiKeyClientId(apiClientId);
|
||||
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
|
||||
stateService.received(1).addAccount(Arg.any());
|
||||
});
|
||||
|
||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
});
|
||||
});
|
||||
288
jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
288
jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { Account, AccountProfile, AccountTokens } from "@/jslib/common/src/models/domain/account";
|
||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
import { PasswordTokenRequest } from "@/jslib/common/src/models/request/identityToken/passwordTokenRequest";
|
||||
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
||||
import { IdentityCaptchaResponse } from "@/jslib/common/src/models/response/identityCaptchaResponse";
|
||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
||||
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const encKey = "ENC_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
||||
const kdf = 0;
|
||||
const kdfIterations = 10000;
|
||||
const userId = Utils.newGuid();
|
||||
const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
||||
|
||||
const decodedToken = {
|
||||
sub: userId,
|
||||
email: email,
|
||||
premium: false,
|
||||
};
|
||||
|
||||
const twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
const twoFactorToken = "TWO_FACTOR_TOKEN";
|
||||
const twoFactorRemember = true;
|
||||
|
||||
export function identityTokenResponseFactory() {
|
||||
return new IdentityTokenResponse({
|
||||
ForcePasswordReset: false,
|
||||
Kdf: kdf,
|
||||
KdfIterations: kdfIterations,
|
||||
Key: encKey,
|
||||
PrivateKey: privateKey,
|
||||
ResetMasterPassword: false,
|
||||
access_token: accessToken,
|
||||
expires_in: 3600,
|
||||
refresh_token: refreshToken,
|
||||
scope: "api offline_access",
|
||||
token_type: "Bearer",
|
||||
});
|
||||
}
|
||||
|
||||
describe("LogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
|
||||
// The base class is abstract so we test it via PasswordLogInStrategy
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
tokenService.decodeToken(accessToken).resolves(decodedToken);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: userId,
|
||||
email: email,
|
||||
hasPremiumPersonally: false,
|
||||
kdfIterations: kdfIterations,
|
||||
kdfType: kdf,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
cryptoService.received(1).setEncKey(encKey);
|
||||
cryptoService.received(1).setEncPrivateKey(privateKey);
|
||||
|
||||
stateService.received(1).setBiometricLocked(false);
|
||||
messagingService.received(1).send("loggedIn");
|
||||
});
|
||||
|
||||
it("builds AuthResult", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.forcePasswordReset = true;
|
||||
tokenResponse.resetMasterPassword = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.forcePasswordReset = true;
|
||||
expected.resetMasterPassword = true;
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("rejects login if CAPTCHA is required", async () => {
|
||||
// Sample CAPTCHA response
|
||||
const tokenResponse = new IdentityCaptchaResponse({
|
||||
error: "invalid_grant",
|
||||
error_description: "Captcha required.",
|
||||
HCaptcha_SiteKey: captchaSiteKey,
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.captchaSiteKey = captchaSiteKey;
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postAccountKeys(Arg.any());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Two-factor authentication", () => {
|
||||
it("rejects login if 2FA is required", async () => {
|
||||
// Sample response where TOTP 2FA required
|
||||
const tokenResponse = new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
||||
expected.twoFactorProviders.set(0, null);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("sends stored 2FA token to server", async () => {
|
||||
tokenService.getTwoFactorToken().resolves(twoFactorToken);
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === false
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends 2FA token provided by user to server (single step)", async () => {
|
||||
// This occurs if the user enters the 2FA code as an argument in the CLI
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
credentials.twoFactor = new TokenRequestTwoFactor(
|
||||
twoFactorProviderType,
|
||||
twoFactorToken,
|
||||
twoFactorRemember
|
||||
);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends 2FA token provided by user to server (two-step)", async () => {
|
||||
// Simulate a partially completed login
|
||||
passwordLogInStrategy.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterPasswordHash,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logInTwoFactor(
|
||||
new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
|
||||
null
|
||||
);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { HashPurpose } from "@/jslib/common/src/enums/hashPurpose";
|
||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
const hashedPassword = "HASHED_PASSWORD";
|
||||
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
|
||||
const preloginKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(
|
||||
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
|
||||
)
|
||||
);
|
||||
const deviceId = Utils.newGuid();
|
||||
|
||||
describe("PasswordLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
|
||||
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
|
||||
|
||||
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
|
||||
cryptoService
|
||||
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
|
||||
.resolves(localHashedPassword);
|
||||
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
});
|
||||
|
||||
it("sends master password credentials to the server", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any; // Need to access private fields
|
||||
return (
|
||||
passwordTokenRequest.email === email &&
|
||||
passwordTokenRequest.masterPasswordHash === hashedPassword &&
|
||||
passwordTokenRequest.device.identifier === deviceId &&
|
||||
passwordTokenRequest.twoFactor.provider == null &&
|
||||
passwordTokenRequest.twoFactor.token == null &&
|
||||
passwordTokenRequest.captchaResponse == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.received(1).setKey(preloginKey);
|
||||
cryptoService.received(1).setKeyHash(localHashedPassword);
|
||||
});
|
||||
});
|
||||
127
jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
127
jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { SsoLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/ssoLogin.strategy";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { SsoLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("SsoLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
|
||||
let ssoLogInStrategy: SsoLogInStrategy;
|
||||
let credentials: SsoLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const encKey = "ENC_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
|
||||
const ssoCode = "SSO_CODE";
|
||||
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
|
||||
const ssoRedirectUrl = "SSO_REDIRECT_URL";
|
||||
const ssoOrgId = "SSO_ORG_ID";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
|
||||
ssoLogInStrategy = new SsoLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
keyConnectorService
|
||||
);
|
||||
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
|
||||
it("sends SSO information to server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const ssoTokenRequest = actual as any;
|
||||
return (
|
||||
ssoTokenRequest.code === ssoCode &&
|
||||
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
|
||||
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
|
||||
ssoTokenRequest.device.identifier === deviceId &&
|
||||
ssoTokenRequest.twoFactor.provider == null &&
|
||||
ssoTokenRequest.twoFactor.token == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not set keys for new SSO user flow", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
|
||||
cryptoService.didNotReceive().setEncKey(encKey);
|
||||
});
|
||||
|
||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
tokenResponse.key = null;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
|
||||
});
|
||||
});
|
||||
127
jslib/common/spec/misc/sequentialize.spec.ts
Normal file
127
jslib/common/spec/misc/sequentialize.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
|
||||
|
||||
describe("sequentialize decorator", () => {
|
||||
it("should call the function once", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function once for each instance of the object", async () => {
|
||||
const foo = new Foo();
|
||||
const foo2 = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
promises.push(foo2.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
expect(foo2.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function once with key function", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.baz(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function again when already resolved", async () => {
|
||||
const foo = new Foo();
|
||||
await foo.bar(1);
|
||||
expect(foo.calls).toBe(1);
|
||||
await foo.bar(1);
|
||||
expect(foo.calls).toBe(2);
|
||||
});
|
||||
|
||||
it("should call the function again when already resolved with a key function", async () => {
|
||||
const foo = new Foo();
|
||||
await foo.baz(1);
|
||||
expect(foo.calls).toBe(1);
|
||||
await foo.baz(1);
|
||||
expect(foo.calls).toBe(2);
|
||||
});
|
||||
|
||||
it("should call the function for each argument", async () => {
|
||||
const foo = new Foo();
|
||||
await Promise.all([foo.bar(1), foo.bar(1), foo.bar(2), foo.bar(2), foo.bar(3), foo.bar(3)]);
|
||||
expect(foo.calls).toBe(3);
|
||||
});
|
||||
|
||||
it("should call the function for each argument with key function", async () => {
|
||||
const foo = new Foo();
|
||||
await Promise.all([foo.baz(1), foo.baz(1), foo.baz(2), foo.baz(2), foo.baz(3), foo.baz(3)]);
|
||||
expect(foo.calls).toBe(3);
|
||||
});
|
||||
|
||||
it("should return correct result for each call", async () => {
|
||||
const foo = new Foo();
|
||||
const allRes: number[] = [];
|
||||
|
||||
await Promise.all([
|
||||
foo.bar(1).then((res) => allRes.push(res)),
|
||||
foo.bar(1).then((res) => allRes.push(res)),
|
||||
foo.bar(2).then((res) => allRes.push(res)),
|
||||
foo.bar(2).then((res) => allRes.push(res)),
|
||||
foo.bar(3).then((res) => allRes.push(res)),
|
||||
foo.bar(3).then((res) => allRes.push(res)),
|
||||
]);
|
||||
expect(foo.calls).toBe(3);
|
||||
expect(allRes.length).toBe(6);
|
||||
allRes.sort();
|
||||
expect(allRes).toEqual([2, 2, 4, 4, 6, 6]);
|
||||
});
|
||||
|
||||
it("should return correct result for each call with key function", async () => {
|
||||
const foo = new Foo();
|
||||
const allRes: number[] = [];
|
||||
|
||||
await Promise.all([
|
||||
foo.baz(1).then((res) => allRes.push(res)),
|
||||
foo.baz(1).then((res) => allRes.push(res)),
|
||||
foo.baz(2).then((res) => allRes.push(res)),
|
||||
foo.baz(2).then((res) => allRes.push(res)),
|
||||
foo.baz(3).then((res) => allRes.push(res)),
|
||||
foo.baz(3).then((res) => allRes.push(res)),
|
||||
]);
|
||||
expect(foo.calls).toBe(3);
|
||||
expect(allRes.length).toBe(6);
|
||||
allRes.sort();
|
||||
expect(allRes).toEqual([3, 3, 6, 6, 9, 9]);
|
||||
});
|
||||
});
|
||||
|
||||
class Foo {
|
||||
calls = 0;
|
||||
|
||||
@sequentialize((args) => "bar" + args[0])
|
||||
bar(a: number): Promise<number> {
|
||||
this.calls++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res(a * 2);
|
||||
}, Math.random() * 100);
|
||||
});
|
||||
}
|
||||
|
||||
@sequentialize((args) => "baz" + args[0])
|
||||
baz(a: number): Promise<number> {
|
||||
this.calls++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res(a * 3);
|
||||
}, Math.random() * 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
110
jslib/common/spec/misc/throttle.spec.ts
Normal file
110
jslib/common/spec/misc/throttle.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
|
||||
import { throttle } from "@/jslib/common/src/misc/throttle";
|
||||
|
||||
describe("throttle decorator", () => {
|
||||
it("should call the function once at a time", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(10);
|
||||
});
|
||||
|
||||
it("should call the function once at a time for each object", async () => {
|
||||
const foo = new Foo();
|
||||
const foo2 = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
promises.push(foo2.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(10);
|
||||
expect(foo2.calls).toBe(10);
|
||||
});
|
||||
|
||||
it("should call the function limit at a time", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.baz(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(10);
|
||||
});
|
||||
|
||||
it("should call the function limit at a time for each object", async () => {
|
||||
const foo = new Foo();
|
||||
const foo2 = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.baz(1));
|
||||
promises.push(foo2.baz(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(10);
|
||||
expect(foo2.calls).toBe(10);
|
||||
});
|
||||
|
||||
it("should work together with sequentialize", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.qux(Math.floor(i / 2) * 2));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
class Foo {
|
||||
calls = 0;
|
||||
inflight = 0;
|
||||
|
||||
@throttle(1, () => "bar")
|
||||
bar(a: number) {
|
||||
this.calls++;
|
||||
this.inflight++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
expect(this.inflight).toBe(1);
|
||||
this.inflight--;
|
||||
res(a * 2);
|
||||
}, Math.random() * 10);
|
||||
});
|
||||
}
|
||||
|
||||
@throttle(5, () => "baz")
|
||||
baz(a: number) {
|
||||
this.calls++;
|
||||
this.inflight++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
expect(this.inflight).toBeLessThanOrEqual(5);
|
||||
this.inflight--;
|
||||
res(a * 3);
|
||||
}, Math.random() * 10);
|
||||
});
|
||||
}
|
||||
|
||||
@sequentialize((args) => "qux" + args[0])
|
||||
@throttle(1, () => "qux")
|
||||
qux(a: number) {
|
||||
this.calls++;
|
||||
this.inflight++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
expect(this.inflight).toBe(1);
|
||||
this.inflight--;
|
||||
res(a * 3);
|
||||
}, Math.random() * 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
69
jslib/common/spec/services/cipher.service.spec.ts
Normal file
69
jslib/common/spec/services/cipher.service.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { FileUploadService } from "@/jslib/common/src/abstractions/fileUpload.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { SearchService } from "@/jslib/common/src/abstractions/search.service";
|
||||
import { SettingsService } from "@/jslib/common/src/abstractions/settings.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
|
||||
import { EncArrayBuffer } from "@/jslib/common/src/models/domain/encArrayBuffer";
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
import { CipherService } from "@/jslib/common/src/services/cipher.service";
|
||||
|
||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
||||
const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT).buffer);
|
||||
|
||||
describe("Cipher Service", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let settingsService: SubstituteOf<SettingsService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let fileUploadService: SubstituteOf<FileUploadService>;
|
||||
let i18nService: SubstituteOf<I18nService>;
|
||||
let searchService: SubstituteOf<SearchService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
|
||||
let cipherService: CipherService;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
settingsService = Substitute.for<SettingsService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
fileUploadService = Substitute.for<FileUploadService>();
|
||||
i18nService = Substitute.for<I18nService>();
|
||||
searchService = Substitute.for<SearchService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
|
||||
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
|
||||
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT));
|
||||
|
||||
cipherService = new CipherService(
|
||||
cryptoService,
|
||||
settingsService,
|
||||
apiService,
|
||||
fileUploadService,
|
||||
i18nService,
|
||||
() => searchService,
|
||||
logService,
|
||||
stateService
|
||||
);
|
||||
});
|
||||
|
||||
it("attachments upload encrypted file contents", async () => {
|
||||
const fileName = "filename";
|
||||
const fileData = new Uint8Array(10).buffer;
|
||||
cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer));
|
||||
|
||||
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
|
||||
|
||||
fileUploadService
|
||||
.received(1)
|
||||
.uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES);
|
||||
});
|
||||
});
|
||||
102
jslib/common/spec/services/consoleLog.service.spec.ts
Normal file
102
jslib/common/spec/services/consoleLog.service.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||
|
||||
const originalConsole = console;
|
||||
let caughtMessage: any;
|
||||
|
||||
declare let console: any;
|
||||
|
||||
export function interceptConsole(interceptions: any): object {
|
||||
console = {
|
||||
log: function () {
|
||||
// eslint-disable-next-line
|
||||
interceptions.log = arguments;
|
||||
},
|
||||
warn: function () {
|
||||
// eslint-disable-next-line
|
||||
interceptions.warn = arguments;
|
||||
},
|
||||
error: function () {
|
||||
// eslint-disable-next-line
|
||||
interceptions.error = arguments;
|
||||
},
|
||||
};
|
||||
return interceptions;
|
||||
}
|
||||
|
||||
export function restoreConsole() {
|
||||
console = originalConsole;
|
||||
}
|
||||
|
||||
describe("ConsoleLogService", () => {
|
||||
let logService: ConsoleLogService;
|
||||
beforeEach(() => {
|
||||
caughtMessage = {};
|
||||
interceptConsole(caughtMessage);
|
||||
logService = new ConsoleLogService(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it("filters messages below the set threshold", () => {
|
||||
logService = new ConsoleLogService(true, () => true);
|
||||
logService.debug("debug");
|
||||
logService.info("info");
|
||||
logService.warning("warning");
|
||||
logService.error("error");
|
||||
|
||||
expect(caughtMessage).toEqual({});
|
||||
});
|
||||
it("only writes debug messages in dev mode", () => {
|
||||
logService = new ConsoleLogService(false);
|
||||
|
||||
logService.debug("debug message");
|
||||
expect(caughtMessage.log).toBeUndefined();
|
||||
});
|
||||
|
||||
it("writes debug/info messages to console.log", () => {
|
||||
logService.debug("this is a debug message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
log: { "0": "this is a debug message" },
|
||||
});
|
||||
|
||||
logService.info("this is an info message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
log: { "0": "this is an info message" },
|
||||
});
|
||||
});
|
||||
it("writes warning messages to console.warn", () => {
|
||||
logService.warning("this is a warning message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
warn: { 0: "this is a warning message" },
|
||||
});
|
||||
});
|
||||
it("writes error messages to console.error", () => {
|
||||
logService.error("this is an error message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
error: { 0: "this is an error message" },
|
||||
});
|
||||
});
|
||||
|
||||
it("times with output to info", async () => {
|
||||
logService.time();
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
const duration = logService.timeEnd();
|
||||
expect(duration[0]).toBe(0);
|
||||
expect(duration[1]).toBeGreaterThan(0);
|
||||
expect(duration[1]).toBeLessThan(500 * 10e6);
|
||||
|
||||
expect(caughtMessage).toEqual(expect.arrayContaining([]));
|
||||
expect(caughtMessage.log.length).toBe(1);
|
||||
expect(caughtMessage.log[0]).toEqual(expect.stringMatching(/^default: \d+\.?\d*ms$/));
|
||||
});
|
||||
|
||||
it("filters time output", async () => {
|
||||
logService = new ConsoleLogService(true, () => true);
|
||||
logService.time();
|
||||
logService.timeEnd();
|
||||
|
||||
expect(caughtMessage).toEqual({});
|
||||
});
|
||||
});
|
||||
84
jslib/common/spec/services/stateMigration.service.ts
Normal file
84
jslib/common/spec/services/stateMigration.service.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
|
||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
|
||||
const userId = "USER_ID";
|
||||
|
||||
describe("State Migration Service", () => {
|
||||
let storageService: SubstituteOf<StorageService>;
|
||||
let secureStorageService: SubstituteOf<StorageService>;
|
||||
let stateFactory: SubstituteOf<StateFactory>;
|
||||
|
||||
let stateMigrationService: StateMigrationService;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = Substitute.for<StorageService>();
|
||||
secureStorageService = Substitute.for<StorageService>();
|
||||
stateFactory = Substitute.for<StateFactory>();
|
||||
|
||||
stateMigrationService = new StateMigrationService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
stateFactory
|
||||
);
|
||||
});
|
||||
|
||||
describe("StateVersion 3 to 4 migration", async () => {
|
||||
beforeEach(() => {
|
||||
const globalVersion3: Partial<GlobalState> = {
|
||||
stateVersion: StateVersion.Three,
|
||||
};
|
||||
|
||||
storageService.get("global", Arg.any()).resolves(globalVersion3);
|
||||
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
|
||||
});
|
||||
|
||||
it("clears everBeenUnlocked", async () => {
|
||||
const accountVersion3: Account = {
|
||||
profile: {
|
||||
apiKeyClientId: null,
|
||||
convertAccountToKeyConnector: null,
|
||||
email: "EMAIL",
|
||||
emailVerified: true,
|
||||
everBeenUnlocked: true,
|
||||
hasPremiumPersonally: false,
|
||||
kdfIterations: 100000,
|
||||
kdfType: 0,
|
||||
keyHash: "KEY_HASH",
|
||||
lastSync: "LAST_SYNC",
|
||||
userId: userId,
|
||||
usesKeyConnector: false,
|
||||
forcePasswordReset: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedAccountVersion4: Account = {
|
||||
profile: {
|
||||
...accountVersion3.profile,
|
||||
},
|
||||
};
|
||||
delete expectedAccountVersion4.profile.everBeenUnlocked;
|
||||
|
||||
storageService.get(userId, Arg.any()).resolves(accountVersion3);
|
||||
|
||||
await stateMigrationService.migrate();
|
||||
|
||||
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
|
||||
});
|
||||
|
||||
it("updates StateVersion number", async () => {
|
||||
await stateMigrationService.migrate();
|
||||
|
||||
storageService.received(1).save(
|
||||
"global",
|
||||
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
|
||||
Arg.any()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
5
jslib/common/spec/test.ts
Normal file
5
jslib/common/spec/test.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { webcrypto } from "crypto";
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
37
jslib/common/spec/utils.ts
Normal file
37
jslib/common/spec/utils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
|
||||
function newGuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetUniqueString(prefix = "") {
|
||||
return prefix + "_" + newGuid();
|
||||
}
|
||||
|
||||
export function BuildTestObject<T, K extends keyof T = keyof T>(
|
||||
def: Partial<Pick<T, K>> | T,
|
||||
constructor?: new () => T
|
||||
): T {
|
||||
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
|
||||
}
|
||||
|
||||
export function mockEnc(s: string): EncString {
|
||||
const mock = Substitute.for<EncString>();
|
||||
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
export function makeStaticByteArray(length: number, start = 0) {
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = start + i;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
679
jslib/common/src/abstractions/api.service.ts
Normal file
679
jslib/common/src/abstractions/api.service.ts
Normal file
@@ -0,0 +1,679 @@
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
|
||||
import { AttachmentRequest } from "../models/request/attachmentRequest";
|
||||
import { BitPayInvoiceRequest } from "../models/request/bitPayInvoiceRequest";
|
||||
import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest";
|
||||
import { CipherBulkMoveRequest } from "../models/request/cipherBulkMoveRequest";
|
||||
import { CipherBulkRestoreRequest } from "../models/request/cipherBulkRestoreRequest";
|
||||
import { CipherBulkShareRequest } from "../models/request/cipherBulkShareRequest";
|
||||
import { CipherCollectionsRequest } from "../models/request/cipherCollectionsRequest";
|
||||
import { CipherCreateRequest } from "../models/request/cipherCreateRequest";
|
||||
import { CipherRequest } from "../models/request/cipherRequest";
|
||||
import { CipherShareRequest } from "../models/request/cipherShareRequest";
|
||||
import { CollectionRequest } from "../models/request/collectionRequest";
|
||||
import { DeleteRecoverRequest } from "../models/request/deleteRecoverRequest";
|
||||
import { EmailRequest } from "../models/request/emailRequest";
|
||||
import { EmailTokenRequest } from "../models/request/emailTokenRequest";
|
||||
import { EmergencyAccessAcceptRequest } from "../models/request/emergencyAccessAcceptRequest";
|
||||
import { EmergencyAccessConfirmRequest } from "../models/request/emergencyAccessConfirmRequest";
|
||||
import { EmergencyAccessInviteRequest } from "../models/request/emergencyAccessInviteRequest";
|
||||
import { EmergencyAccessPasswordRequest } from "../models/request/emergencyAccessPasswordRequest";
|
||||
import { EmergencyAccessUpdateRequest } from "../models/request/emergencyAccessUpdateRequest";
|
||||
import { EventRequest } from "../models/request/eventRequest";
|
||||
import { FolderRequest } from "../models/request/folderRequest";
|
||||
import { GroupRequest } from "../models/request/groupRequest";
|
||||
import { IapCheckRequest } from "../models/request/iapCheckRequest";
|
||||
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
|
||||
import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
|
||||
import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
|
||||
import { ImportCiphersRequest } from "../models/request/importCiphersRequest";
|
||||
import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest";
|
||||
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
|
||||
import { KdfRequest } from "../models/request/kdfRequest";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
||||
import { KeysRequest } from "../models/request/keysRequest";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../models/request/organization/organizationSponsorshipCreateRequest";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organizationSponsorshipRedeemRequest";
|
||||
import { OrganizationSsoRequest } from "../models/request/organization/organizationSsoRequest";
|
||||
import { OrganizationCreateRequest } from "../models/request/organizationCreateRequest";
|
||||
import { OrganizationImportRequest } from "../models/request/organizationImportRequest";
|
||||
import { OrganizationKeysRequest } from "../models/request/organizationKeysRequest";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../models/request/organizationSubscriptionUpdateRequest";
|
||||
import { OrganizationTaxInfoUpdateRequest } from "../models/request/organizationTaxInfoUpdateRequest";
|
||||
import { OrganizationUpdateRequest } from "../models/request/organizationUpdateRequest";
|
||||
import { OrganizationUpgradeRequest } from "../models/request/organizationUpgradeRequest";
|
||||
import { OrganizationUserAcceptRequest } from "../models/request/organizationUserAcceptRequest";
|
||||
import { OrganizationUserBulkConfirmRequest } from "../models/request/organizationUserBulkConfirmRequest";
|
||||
import { OrganizationUserBulkRequest } from "../models/request/organizationUserBulkRequest";
|
||||
import { OrganizationUserConfirmRequest } from "../models/request/organizationUserConfirmRequest";
|
||||
import { OrganizationUserInviteRequest } from "../models/request/organizationUserInviteRequest";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "../models/request/organizationUserResetPasswordEnrollmentRequest";
|
||||
import { OrganizationUserResetPasswordRequest } from "../models/request/organizationUserResetPasswordRequest";
|
||||
import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizationUserUpdateGroupsRequest";
|
||||
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
|
||||
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
|
||||
import { PasswordRequest } from "../models/request/passwordRequest";
|
||||
import { PaymentRequest } from "../models/request/paymentRequest";
|
||||
import { PolicyRequest } from "../models/request/policyRequest";
|
||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
|
||||
import { ProviderOrganizationCreateRequest } from "../models/request/provider/providerOrganizationCreateRequest";
|
||||
import { ProviderSetupRequest } from "../models/request/provider/providerSetupRequest";
|
||||
import { ProviderUpdateRequest } from "../models/request/provider/providerUpdateRequest";
|
||||
import { ProviderUserAcceptRequest } from "../models/request/provider/providerUserAcceptRequest";
|
||||
import { ProviderUserBulkConfirmRequest } from "../models/request/provider/providerUserBulkConfirmRequest";
|
||||
import { ProviderUserBulkRequest } from "../models/request/provider/providerUserBulkRequest";
|
||||
import { ProviderUserConfirmRequest } from "../models/request/provider/providerUserConfirmRequest";
|
||||
import { ProviderUserInviteRequest } from "../models/request/provider/providerUserInviteRequest";
|
||||
import { ProviderUserUpdateRequest } from "../models/request/provider/providerUserUpdateRequest";
|
||||
import { RegisterRequest } from "../models/request/registerRequest";
|
||||
import { SeatRequest } from "../models/request/seatRequest";
|
||||
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
|
||||
import { SelectionReadOnlyRequest } from "../models/request/selectionReadOnlyRequest";
|
||||
import { SendAccessRequest } from "../models/request/sendAccessRequest";
|
||||
import { SendRequest } from "../models/request/sendRequest";
|
||||
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
||||
import { StorageRequest } from "../models/request/storageRequest";
|
||||
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
||||
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
||||
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
||||
import { UpdateDomainsRequest } from "../models/request/updateDomainsRequest";
|
||||
import { UpdateKeyRequest } from "../models/request/updateKeyRequest";
|
||||
import { UpdateProfileRequest } from "../models/request/updateProfileRequest";
|
||||
import { UpdateTempPasswordRequest } from "../models/request/updateTempPasswordRequest";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "../models/request/updateTwoFactorAuthenticatorRequest";
|
||||
import { UpdateTwoFactorDuoRequest } from "../models/request/updateTwoFactorDuoRequest";
|
||||
import { UpdateTwoFactorEmailRequest } from "../models/request/updateTwoFactorEmailRequest";
|
||||
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../models/request/updateTwoFactorWebAuthnDeleteRequest";
|
||||
import { UpdateTwoFactorWebAuthnRequest } from "../models/request/updateTwoFactorWebAuthnRequest";
|
||||
import { UpdateTwoFactorYubioOtpRequest } from "../models/request/updateTwoFactorYubioOtpRequest";
|
||||
import { VerifyBankRequest } from "../models/request/verifyBankRequest";
|
||||
import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest";
|
||||
import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||
import { BillingResponse } from "../models/response/billingResponse";
|
||||
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
|
||||
import { CipherResponse } from "../models/response/cipherResponse";
|
||||
import {
|
||||
CollectionGroupDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../models/response/collectionResponse";
|
||||
import { DomainsResponse } from "../models/response/domainsResponse";
|
||||
import {
|
||||
EmergencyAccessGranteeDetailsResponse,
|
||||
EmergencyAccessGrantorDetailsResponse,
|
||||
EmergencyAccessTakeoverResponse,
|
||||
EmergencyAccessViewResponse,
|
||||
} from "../models/response/emergencyAccessResponse";
|
||||
import { EventResponse } from "../models/response/eventResponse";
|
||||
import { FolderResponse } from "../models/response/folderResponse";
|
||||
import { GroupDetailsResponse, GroupResponse } from "../models/response/groupResponse";
|
||||
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
||||
import { KeyConnectorUserKeyResponse } from "../models/response/keyConnectorUserKeyResponse";
|
||||
import { ListResponse } from "../models/response/listResponse";
|
||||
import { OrganizationSsoResponse } from "../models/response/organization/organizationSsoResponse";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../models/response/organizationAutoEnrollStatusResponse";
|
||||
import { OrganizationKeysResponse } from "../models/response/organizationKeysResponse";
|
||||
import { OrganizationResponse } from "../models/response/organizationResponse";
|
||||
import { OrganizationSubscriptionResponse } from "../models/response/organizationSubscriptionResponse";
|
||||
import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organizationUserBulkPublicKeyResponse";
|
||||
import { OrganizationUserBulkResponse } from "../models/response/organizationUserBulkResponse";
|
||||
import {
|
||||
OrganizationUserDetailsResponse,
|
||||
OrganizationUserResetPasswordDetailsReponse,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "../models/response/organizationUserResponse";
|
||||
import { PaymentResponse } from "../models/response/paymentResponse";
|
||||
import { PlanResponse } from "../models/response/planResponse";
|
||||
import { PolicyResponse } from "../models/response/policyResponse";
|
||||
import { PreloginResponse } from "../models/response/preloginResponse";
|
||||
import { ProfileResponse } from "../models/response/profileResponse";
|
||||
import {
|
||||
ProviderOrganizationOrganizationDetailsResponse,
|
||||
ProviderOrganizationResponse,
|
||||
} from "../models/response/provider/providerOrganizationResponse";
|
||||
import { ProviderResponse } from "../models/response/provider/providerResponse";
|
||||
import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/providerUserBulkPublicKeyResponse";
|
||||
import { ProviderUserBulkResponse } from "../models/response/provider/providerUserBulkResponse";
|
||||
import {
|
||||
ProviderUserResponse,
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../models/response/provider/providerUserResponse";
|
||||
import { SelectionReadOnlyResponse } from "../models/response/selectionReadOnlyResponse";
|
||||
import { SendAccessResponse } from "../models/response/sendAccessResponse";
|
||||
import { SendFileDownloadDataResponse } from "../models/response/sendFileDownloadDataResponse";
|
||||
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
|
||||
import { SendResponse } from "../models/response/sendResponse";
|
||||
import { SubscriptionResponse } from "../models/response/subscriptionResponse";
|
||||
import { SyncResponse } from "../models/response/syncResponse";
|
||||
import { TaxInfoResponse } from "../models/response/taxInfoResponse";
|
||||
import { TaxRateResponse } from "../models/response/taxRateResponse";
|
||||
import { TwoFactorAuthenticatorResponse } from "../models/response/twoFactorAuthenticatorResponse";
|
||||
import { TwoFactorDuoResponse } from "../models/response/twoFactorDuoResponse";
|
||||
import { TwoFactorEmailResponse } from "../models/response/twoFactorEmailResponse";
|
||||
import { TwoFactorProviderResponse } from "../models/response/twoFactorProviderResponse";
|
||||
import { TwoFactorRecoverResponse } from "../models/response/twoFactorRescoverResponse";
|
||||
import {
|
||||
ChallengeResponse,
|
||||
TwoFactorWebAuthnResponse,
|
||||
} from "../models/response/twoFactorWebAuthnResponse";
|
||||
import { TwoFactorYubiKeyResponse } from "../models/response/twoFactorYubiKeyResponse";
|
||||
import { UserKeyResponse } from "../models/response/userKeyResponse";
|
||||
import { SendAccessView } from "../models/view/sendAccessView";
|
||||
|
||||
export abstract class ApiService {
|
||||
postIdentityToken: (
|
||||
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
|
||||
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||
refreshIdentityToken: () => Promise<any>;
|
||||
|
||||
getProfile: () => Promise<ProfileResponse>;
|
||||
getUserBilling: () => Promise<BillingResponse>;
|
||||
getUserSubscription: () => Promise<SubscriptionResponse>;
|
||||
getTaxInfo: () => Promise<TaxInfoResponse>;
|
||||
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
|
||||
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
postEmail: (request: EmailRequest) => Promise<any>;
|
||||
postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
setPassword: (request: SetPasswordRequest) => Promise<any>;
|
||||
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
|
||||
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
|
||||
deleteAccount: (request: SecretVerificationRequest) => Promise<any>;
|
||||
getAccountRevisionDate: () => Promise<number>;
|
||||
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
|
||||
postRegister: (request: RegisterRequest) => Promise<any>;
|
||||
postPremium: (data: FormData) => Promise<PaymentResponse>;
|
||||
postIapCheck: (request: IapCheckRequest) => Promise<any>;
|
||||
postReinstatePremium: () => Promise<any>;
|
||||
postCancelPremium: () => Promise<any>;
|
||||
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
|
||||
postAccountPayment: (request: PaymentRequest) => Promise<any>;
|
||||
postAccountLicense: (data: FormData) => Promise<any>;
|
||||
postAccountKey: (request: UpdateKeyRequest) => Promise<any>;
|
||||
postAccountKeys: (request: KeysRequest) => Promise<any>;
|
||||
postAccountVerifyEmail: () => Promise<any>;
|
||||
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
|
||||
postAccountVerifyPassword: (request: SecretVerificationRequest) => Promise<any>;
|
||||
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
|
||||
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
|
||||
postAccountKdf: (request: KdfRequest) => Promise<any>;
|
||||
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||
postAccountRequestOTP: () => Promise<void>;
|
||||
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
|
||||
getFolder: (id: string) => Promise<FolderResponse>;
|
||||
postFolder: (request: FolderRequest) => Promise<FolderResponse>;
|
||||
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;
|
||||
deleteFolder: (id: string) => Promise<any>;
|
||||
|
||||
getSend: (id: string) => Promise<SendResponse>;
|
||||
postSendAccess: (
|
||||
id: string,
|
||||
request: SendAccessRequest,
|
||||
apiUrl?: string
|
||||
) => Promise<SendAccessResponse>;
|
||||
getSends: () => Promise<ListResponse<SendResponse>>;
|
||||
postSend: (request: SendRequest) => Promise<SendResponse>;
|
||||
postFileTypeSend: (request: SendRequest) => Promise<SendFileUploadDataResponse>;
|
||||
postSendFile: (sendId: string, fileId: string, data: FormData) => Promise<any>;
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
* This method still exists for backward compatibility with old server versions.
|
||||
*/
|
||||
postSendFileLegacy: (data: FormData) => Promise<SendResponse>;
|
||||
putSend: (id: string, request: SendRequest) => Promise<SendResponse>;
|
||||
putSendRemovePassword: (id: string) => Promise<SendResponse>;
|
||||
deleteSend: (id: string) => Promise<any>;
|
||||
getSendFileDownloadData: (
|
||||
send: SendAccessView,
|
||||
request: SendAccessRequest,
|
||||
apiUrl?: string
|
||||
) => Promise<SendFileDownloadDataResponse>;
|
||||
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
|
||||
|
||||
getCipher: (id: string) => Promise<CipherResponse>;
|
||||
getCipherAdmin: (id: string) => Promise<CipherResponse>;
|
||||
getAttachmentData: (
|
||||
cipherId: string,
|
||||
attachmentId: string,
|
||||
emergencyAccessId?: string
|
||||
) => Promise<AttachmentResponse>;
|
||||
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;
|
||||
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
|
||||
postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>;
|
||||
postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>;
|
||||
putCipher: (id: string, request: CipherRequest) => Promise<CipherResponse>;
|
||||
putCipherAdmin: (id: string, request: CipherRequest) => Promise<CipherResponse>;
|
||||
deleteCipher: (id: string) => Promise<any>;
|
||||
deleteCipherAdmin: (id: string) => Promise<any>;
|
||||
deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
|
||||
deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
|
||||
putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>;
|
||||
putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>;
|
||||
putShareCiphers: (request: CipherBulkShareRequest) => Promise<any>;
|
||||
putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise<any>;
|
||||
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
|
||||
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
|
||||
postImportCiphers: (request: ImportCiphersRequest) => Promise<any>;
|
||||
postImportOrganizationCiphers: (
|
||||
organizationId: string,
|
||||
request: ImportOrganizationCiphersRequest
|
||||
) => Promise<any>;
|
||||
putDeleteCipher: (id: string) => Promise<any>;
|
||||
putDeleteCipherAdmin: (id: string) => Promise<any>;
|
||||
putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
|
||||
putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
|
||||
putRestoreCipher: (id: string) => Promise<CipherResponse>;
|
||||
putRestoreCipherAdmin: (id: string) => Promise<CipherResponse>;
|
||||
putRestoreManyCiphers: (
|
||||
request: CipherBulkRestoreRequest
|
||||
) => Promise<ListResponse<CipherResponse>>;
|
||||
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
* This method still exists for backward compatibility with old server versions.
|
||||
*/
|
||||
postCipherAttachmentLegacy: (id: string, data: FormData) => Promise<CipherResponse>;
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
* This method still exists for backward compatibility with old server versions.
|
||||
*/
|
||||
postCipherAttachmentAdminLegacy: (id: string, data: FormData) => Promise<CipherResponse>;
|
||||
postCipherAttachment: (
|
||||
id: string,
|
||||
request: AttachmentRequest
|
||||
) => Promise<AttachmentUploadDataResponse>;
|
||||
deleteCipherAttachment: (id: string, attachmentId: string) => Promise<any>;
|
||||
deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise<any>;
|
||||
postShareCipherAttachment: (
|
||||
id: string,
|
||||
attachmentId: string,
|
||||
data: FormData,
|
||||
organizationId: string
|
||||
) => Promise<any>;
|
||||
renewAttachmentUploadUrl: (
|
||||
id: string,
|
||||
attachmentId: string
|
||||
) => Promise<AttachmentUploadDataResponse>;
|
||||
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
|
||||
|
||||
getCollectionDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<CollectionGroupDetailsResponse>;
|
||||
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
|
||||
postCollection: (
|
||||
organizationId: string,
|
||||
request: CollectionRequest
|
||||
) => Promise<CollectionResponse>;
|
||||
putCollectionUsers: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: SelectionReadOnlyRequest[]
|
||||
) => Promise<any>;
|
||||
putCollection: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: CollectionRequest
|
||||
) => Promise<CollectionResponse>;
|
||||
deleteCollection: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteCollectionUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
organizationUserId: string
|
||||
) => Promise<any>;
|
||||
|
||||
getGroupDetails: (organizationId: string, id: string) => Promise<GroupDetailsResponse>;
|
||||
getGroups: (organizationId: string) => Promise<ListResponse<GroupResponse>>;
|
||||
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
||||
postGroup: (organizationId: string, request: GroupRequest) => Promise<GroupResponse>;
|
||||
putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise<GroupResponse>;
|
||||
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
||||
deleteGroup: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
||||
|
||||
getPolicy: (organizationId: string, type: PolicyType) => Promise<PolicyResponse>;
|
||||
getPolicies: (organizationId: string) => Promise<ListResponse<PolicyResponse>>;
|
||||
getPoliciesByToken: (
|
||||
organizationId: string,
|
||||
token: string,
|
||||
email: string,
|
||||
organizationUserId: string
|
||||
) => Promise<ListResponse<PolicyResponse>>;
|
||||
getPoliciesByInvitedUser: (
|
||||
organizationId: string,
|
||||
userId: string
|
||||
) => Promise<ListResponse<PolicyResponse>>;
|
||||
putPolicy: (
|
||||
organizationId: string,
|
||||
type: PolicyType,
|
||||
request: PolicyRequest
|
||||
) => Promise<PolicyResponse>;
|
||||
|
||||
getOrganizationUser: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<OrganizationUserDetailsResponse>;
|
||||
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
|
||||
getOrganizationUsers: (
|
||||
organizationId: string
|
||||
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||
getOrganizationUserResetPasswordDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<OrganizationUserResetPasswordDetailsReponse>;
|
||||
postOrganizationUserInvite: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserInviteRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUserReinvite: (organizationId: string, id: string) => Promise<any>;
|
||||
postManyOrganizationUserReinvite: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
postOrganizationUserAccept: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserAcceptRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUserConfirm: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserConfirmRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUsersPublicKey: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
|
||||
postOrganizationUserBulkConfirm: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkConfirmRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
putOrganizationUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserUpdateRequest
|
||||
) => Promise<any>;
|
||||
putOrganizationUserGroups: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserUpdateGroupsRequest
|
||||
) => Promise<any>;
|
||||
putOrganizationUserResetPasswordEnrollment: (
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
request: OrganizationUserResetPasswordEnrollmentRequest
|
||||
) => Promise<any>;
|
||||
putOrganizationUserResetPassword: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserResetPasswordRequest
|
||||
) => Promise<any>;
|
||||
deleteOrganizationUser: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteManyOrganizationUsers: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
getSync: () => Promise<SyncResponse>;
|
||||
postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise<any>;
|
||||
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;
|
||||
|
||||
getSettingsDomains: () => Promise<DomainsResponse>;
|
||||
putSettingsDomains: (request: UpdateDomainsRequest) => Promise<DomainsResponse>;
|
||||
|
||||
getTwoFactorProviders: () => Promise<ListResponse<TwoFactorProviderResponse>>;
|
||||
getTwoFactorOrganizationProviders: (
|
||||
organizationId: string
|
||||
) => Promise<ListResponse<TwoFactorProviderResponse>>;
|
||||
getTwoFactorAuthenticator: (
|
||||
request: SecretVerificationRequest
|
||||
) => Promise<TwoFactorAuthenticatorResponse>;
|
||||
getTwoFactorEmail: (request: SecretVerificationRequest) => Promise<TwoFactorEmailResponse>;
|
||||
getTwoFactorDuo: (request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
|
||||
getTwoFactorOrganizationDuo: (
|
||||
organizationId: string,
|
||||
request: SecretVerificationRequest
|
||||
) => Promise<TwoFactorDuoResponse>;
|
||||
getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
|
||||
getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
|
||||
getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise<ChallengeResponse>;
|
||||
getTwoFactorRecover: (request: SecretVerificationRequest) => Promise<TwoFactorRecoverResponse>;
|
||||
putTwoFactorAuthenticator: (
|
||||
request: UpdateTwoFactorAuthenticatorRequest
|
||||
) => Promise<TwoFactorAuthenticatorResponse>;
|
||||
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
|
||||
putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
|
||||
putTwoFactorOrganizationDuo: (
|
||||
organizationId: string,
|
||||
request: UpdateTwoFactorDuoRequest
|
||||
) => Promise<TwoFactorDuoResponse>;
|
||||
putTwoFactorYubiKey: (
|
||||
request: UpdateTwoFactorYubioOtpRequest
|
||||
) => Promise<TwoFactorYubiKeyResponse>;
|
||||
putTwoFactorWebAuthn: (
|
||||
request: UpdateTwoFactorWebAuthnRequest
|
||||
) => Promise<TwoFactorWebAuthnResponse>;
|
||||
deleteTwoFactorWebAuthn: (
|
||||
request: UpdateTwoFactorWebAuthnDeleteRequest
|
||||
) => Promise<TwoFactorWebAuthnResponse>;
|
||||
putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
|
||||
putTwoFactorOrganizationDisable: (
|
||||
organizationId: string,
|
||||
request: TwoFactorProviderRequest
|
||||
) => Promise<TwoFactorProviderResponse>;
|
||||
postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise<any>;
|
||||
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
|
||||
getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
|
||||
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;
|
||||
getEmergencyAccess: (id: string) => Promise<EmergencyAccessGranteeDetailsResponse>;
|
||||
getEmergencyGrantorPolicies: (id: string) => Promise<ListResponse<PolicyResponse>>;
|
||||
putEmergencyAccess: (id: string, request: EmergencyAccessUpdateRequest) => Promise<any>;
|
||||
deleteEmergencyAccess: (id: string) => Promise<any>;
|
||||
postEmergencyAccessInvite: (request: EmergencyAccessInviteRequest) => Promise<any>;
|
||||
postEmergencyAccessReinvite: (id: string) => Promise<any>;
|
||||
postEmergencyAccessAccept: (id: string, request: EmergencyAccessAcceptRequest) => Promise<any>;
|
||||
postEmergencyAccessConfirm: (id: string, request: EmergencyAccessConfirmRequest) => Promise<any>;
|
||||
postEmergencyAccessInitiate: (id: string) => Promise<any>;
|
||||
postEmergencyAccessApprove: (id: string) => Promise<any>;
|
||||
postEmergencyAccessReject: (id: string) => Promise<any>;
|
||||
postEmergencyAccessTakeover: (id: string) => Promise<EmergencyAccessTakeoverResponse>;
|
||||
postEmergencyAccessPassword: (
|
||||
id: string,
|
||||
request: EmergencyAccessPasswordRequest
|
||||
) => Promise<any>;
|
||||
postEmergencyAccessView: (id: string) => Promise<EmergencyAccessViewResponse>;
|
||||
|
||||
getOrganization: (id: string) => Promise<OrganizationResponse>;
|
||||
getOrganizationBilling: (id: string) => Promise<BillingResponse>;
|
||||
getOrganizationSubscription: (id: string) => Promise<OrganizationSubscriptionResponse>;
|
||||
getOrganizationLicense: (id: string, installationId: string) => Promise<any>;
|
||||
getOrganizationTaxInfo: (id: string) => Promise<TaxInfoResponse>;
|
||||
getOrganizationAutoEnrollStatus: (
|
||||
identifier: string
|
||||
) => Promise<OrganizationAutoEnrollStatusResponse>;
|
||||
getOrganizationSso: (id: string) => Promise<OrganizationSsoResponse>;
|
||||
postOrganization: (request: OrganizationCreateRequest) => Promise<OrganizationResponse>;
|
||||
putOrganization: (
|
||||
id: string,
|
||||
request: OrganizationUpdateRequest
|
||||
) => Promise<OrganizationResponse>;
|
||||
putOrganizationTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise<any>;
|
||||
postLeaveOrganization: (id: string) => Promise<any>;
|
||||
postOrganizationLicense: (data: FormData) => Promise<OrganizationResponse>;
|
||||
postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise<any>;
|
||||
postOrganizationApiKey: (
|
||||
id: string,
|
||||
request: SecretVerificationRequest
|
||||
) => Promise<ApiKeyResponse>;
|
||||
postOrganizationRotateApiKey: (
|
||||
id: string,
|
||||
request: SecretVerificationRequest
|
||||
) => Promise<ApiKeyResponse>;
|
||||
postOrganizationSso: (
|
||||
id: string,
|
||||
request: OrganizationSsoRequest
|
||||
) => Promise<OrganizationSsoResponse>;
|
||||
postOrganizationUpgrade: (
|
||||
id: string,
|
||||
request: OrganizationUpgradeRequest
|
||||
) => Promise<PaymentResponse>;
|
||||
postOrganizationUpdateSubscription: (
|
||||
id: string,
|
||||
request: OrganizationSubscriptionUpdateRequest
|
||||
) => Promise<void>;
|
||||
postOrganizationSeat: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
|
||||
postOrganizationStorage: (id: string, request: StorageRequest) => Promise<any>;
|
||||
postOrganizationPayment: (id: string, request: PaymentRequest) => Promise<any>;
|
||||
postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise<any>;
|
||||
postOrganizationCancel: (id: string) => Promise<any>;
|
||||
postOrganizationReinstate: (id: string) => Promise<any>;
|
||||
deleteOrganization: (id: string, request: SecretVerificationRequest) => Promise<any>;
|
||||
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
||||
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
|
||||
getOrganizationKeys: (id: string) => Promise<OrganizationKeysResponse>;
|
||||
postOrganizationKeys: (
|
||||
id: string,
|
||||
request: OrganizationKeysRequest
|
||||
) => Promise<OrganizationKeysResponse>;
|
||||
|
||||
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
|
||||
getProvider: (id: string) => Promise<ProviderResponse>;
|
||||
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
|
||||
|
||||
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
|
||||
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
|
||||
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
|
||||
postProviderUserReinvite: (providerId: string, id: string) => Promise<any>;
|
||||
postManyProviderUserReinvite: (
|
||||
providerId: string,
|
||||
request: ProviderUserBulkRequest
|
||||
) => Promise<ListResponse<ProviderUserBulkResponse>>;
|
||||
postProviderUserAccept: (
|
||||
providerId: string,
|
||||
id: string,
|
||||
request: ProviderUserAcceptRequest
|
||||
) => Promise<any>;
|
||||
postProviderUserConfirm: (
|
||||
providerId: string,
|
||||
id: string,
|
||||
request: ProviderUserConfirmRequest
|
||||
) => Promise<any>;
|
||||
postProviderUsersPublicKey: (
|
||||
providerId: string,
|
||||
request: ProviderUserBulkRequest
|
||||
) => Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
|
||||
postProviderUserBulkConfirm: (
|
||||
providerId: string,
|
||||
request: ProviderUserBulkConfirmRequest
|
||||
) => Promise<ListResponse<ProviderUserBulkResponse>>;
|
||||
putProviderUser: (
|
||||
providerId: string,
|
||||
id: string,
|
||||
request: ProviderUserUpdateRequest
|
||||
) => Promise<any>;
|
||||
deleteProviderUser: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteManyProviderUsers: (
|
||||
providerId: string,
|
||||
request: ProviderUserBulkRequest
|
||||
) => Promise<ListResponse<ProviderUserBulkResponse>>;
|
||||
getProviderClients: (
|
||||
providerId: string
|
||||
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
|
||||
postProviderAddOrganization: (
|
||||
providerId: string,
|
||||
request: ProviderAddOrganizationRequest
|
||||
) => Promise<any>;
|
||||
postProviderCreateOrganization: (
|
||||
providerId: string,
|
||||
request: ProviderOrganizationCreateRequest
|
||||
) => Promise<ProviderOrganizationResponse>;
|
||||
deleteProviderOrganization: (providerId: string, organizationId: string) => Promise<any>;
|
||||
|
||||
getEvents: (start: string, end: string, token: string) => Promise<ListResponse<EventResponse>>;
|
||||
getEventsCipher: (
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string
|
||||
) => Promise<ListResponse<EventResponse>>;
|
||||
getEventsOrganization: (
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string
|
||||
) => Promise<ListResponse<EventResponse>>;
|
||||
getEventsOrganizationUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string
|
||||
) => Promise<ListResponse<EventResponse>>;
|
||||
getEventsProvider: (
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string
|
||||
) => Promise<ListResponse<EventResponse>>;
|
||||
getEventsProviderUser: (
|
||||
providerId: string,
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string
|
||||
) => Promise<ListResponse<EventResponse>>;
|
||||
postEventsCollect: (request: EventRequest[]) => Promise<any>;
|
||||
|
||||
deleteSsoUser: (organizationId: string) => Promise<any>;
|
||||
getSsoUserIdentifier: () => Promise<string>;
|
||||
|
||||
getUserPublicKey: (id: string) => Promise<UserKeyResponse>;
|
||||
|
||||
getHibpBreach: (username: string) => Promise<BreachAccountResponse[]>;
|
||||
|
||||
postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise<string>;
|
||||
postSetupPayment: () => Promise<string>;
|
||||
|
||||
getActiveBearerToken: () => Promise<string>;
|
||||
fetch: (request: Request) => Promise<Response>;
|
||||
nativeFetch: (request: Request) => Promise<Response>;
|
||||
|
||||
preValidateSso: (identifier: string) => Promise<boolean>;
|
||||
|
||||
postCreateSponsorship: (
|
||||
sponsorshipOrgId: string,
|
||||
request: OrganizationSponsorshipCreateRequest
|
||||
) => Promise<void>;
|
||||
deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise<void>;
|
||||
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
|
||||
postPreValidateSponsorshipToken: (sponsorshipToken: string) => Promise<boolean>;
|
||||
postRedeemSponsorship: (
|
||||
sponsorshipToken: string,
|
||||
request: OrganizationSponsorshipRedeemRequest
|
||||
) => Promise<void>;
|
||||
postResendSponsorshipOffer: (sponsoringOrgId: string) => Promise<void>;
|
||||
|
||||
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
|
||||
postUserKeyToKeyConnector: (
|
||||
keyConnectorUrl: string,
|
||||
request: KeyConnectorUserKeyRequest
|
||||
) => Promise<void>;
|
||||
getKeyConnectorAlive: (keyConnectorUrl: string) => Promise<void>;
|
||||
}
|
||||
4
jslib/common/src/abstractions/appId.service.ts
Normal file
4
jslib/common/src/abstractions/appId.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export abstract class AppIdService {
|
||||
getAppId: () => Promise<string>;
|
||||
getAnonymousAppId: () => Promise<string>;
|
||||
}
|
||||
6
jslib/common/src/abstractions/audit.service.ts
Normal file
6
jslib/common/src/abstractions/audit.service.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
|
||||
|
||||
export abstract class AuditService {
|
||||
passwordLeaked: (password: string) => Promise<number>;
|
||||
breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
|
||||
}
|
||||
25
jslib/common/src/abstractions/auth.service.ts
Normal file
25
jslib/common/src/abstractions/auth.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AuthResult } from "../models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
||||
|
||||
export abstract class AuthService {
|
||||
masterPasswordHash: string;
|
||||
email: string;
|
||||
logIn: (
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
) => Promise<AuthResult>;
|
||||
logInTwoFactor: (
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string
|
||||
) => Promise<AuthResult>;
|
||||
logOut: (callback: () => void) => void;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
authingWithApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
authingWithPassword: () => boolean;
|
||||
}
|
||||
5
jslib/common/src/abstractions/broadcaster.service.ts
Normal file
5
jslib/common/src/abstractions/broadcaster.service.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export abstract class BroadcasterService {
|
||||
send: (message: any, id?: string) => void;
|
||||
subscribe: (id: string, messageCallback: (message: any) => any) => void;
|
||||
unsubscribe: (id: string) => void;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user