mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-16 08:34:24 +00:00
Compare commits
26 Commits
v2025.12.0
...
state-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e74948bd | ||
|
|
9f8018e8f8 | ||
|
|
0bff38c459 | ||
|
|
94ff20f69f | ||
|
|
7381857296 | ||
|
|
ba17d5b438 | ||
|
|
b5d31e693b | ||
|
|
2854a2eba1 | ||
|
|
4485ecab3c | ||
|
|
9e3b2d2d95 | ||
|
|
b2997358dc | ||
|
|
db258f0191 | ||
|
|
19d7884933 | ||
|
|
21ce02f431 | ||
|
|
1af8fc1067 | ||
|
|
6c2f54bad5 | ||
|
|
bb9a6a61ee | ||
|
|
f0a19b6267 | ||
|
|
220d6c02c7 | ||
|
|
321db6e771 | ||
|
|
554e14d7a8 | ||
|
|
f195e27938 | ||
|
|
d1ac1e667e | ||
|
|
b9867b131f | ||
|
|
bb165441ee | ||
|
|
b8964aa382 |
@@ -1,27 +0,0 @@
|
||||
Please review this pull request with a focus on:
|
||||
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide a comprehensive review including:
|
||||
|
||||
- Summary of changes since last review
|
||||
- Critical issues found (be thorough)
|
||||
- Suggested improvements (be thorough)
|
||||
- Good practices observed (be concise - list only the most notable items without elaboration)
|
||||
- Action items for the author
|
||||
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code
|
||||
snippets to enhance human readability
|
||||
|
||||
When reviewing subsequent commits:
|
||||
|
||||
- Track status of previously identified issues (fixed/unfixed/reopened)
|
||||
- Identify NEW problems introduced since last review
|
||||
- Note if fixes introduced new issues
|
||||
|
||||
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note
|
||||
what was done well without explaining why or praising excessively.
|
||||
@@ -1,10 +0,0 @@
|
||||
dist
|
||||
build
|
||||
build-cli
|
||||
webpack.cli.js
|
||||
webpack.main.js
|
||||
webpack.renderer.js
|
||||
|
||||
**/node_modules
|
||||
|
||||
**/jest.config.js
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"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"]
|
||||
},
|
||||
"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",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/jslib/**/*",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "@/src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -51,12 +51,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Linux Zip to GitHub
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
@@ -129,12 +129,12 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Mac Zip to GitHub
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
choco install checksum --no-progress
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Upload Windows Zip to GitHub
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
@@ -279,12 +279,12 @@ jobs:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -338,28 +338,28 @@ jobs:
|
||||
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
|
||||
|
||||
- name: Upload Portable Executable to GitHub
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: latest.yml
|
||||
path: ./dist/latest.yml
|
||||
@@ -379,12 +379,12 @@ jobs:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -411,14 +411,14 @@ jobs:
|
||||
run: npm run dist:lin
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: latest-linux.yml
|
||||
path: ./dist/latest-linux.yml
|
||||
@@ -439,12 +439,12 @@ jobs:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -542,28 +542,28 @@ jobs:
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
- name: Upload .zip artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: latest-mac.yml
|
||||
path: ./dist/latest-mac.yml
|
||||
|
||||
12
.github/workflows/integration-test.yml
vendored
12
.github/workflows/integration-test.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
|
||||
- name: Report test results
|
||||
id: report
|
||||
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
||||
# PRs from the repository and all other events are OK.
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
||||
@@ -140,7 +140,9 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
report_type: test_results
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
||||
env:
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/review-code.yml
vendored
2
.github/workflows/review-code.yml
vendored
@@ -2,7 +2,7 @@ name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
run: npm run test --coverage
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
||||
# PRs from the repository and all other events are OK.
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
||||
@@ -64,7 +64,9 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
report_type: test_results
|
||||
|
||||
4
.github/workflows/version-bump.yml
vendored
4
.github/workflows/version-bump.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
permission-contents: write
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
|
||||
10
angular.json
10
angular.json
@@ -18,15 +18,17 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"outputPath": {
|
||||
"base": "dist"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.json",
|
||||
"assets": [],
|
||||
"styles": [],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"browser": "src/main.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
},
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"appId": "com.bitwarden.directory-connector",
|
||||
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
149
eslint.config.mjs
Normal file
149
eslint.config.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
// @ts-check
|
||||
import eslint from "@eslint/js";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import prettierConfig from "eslint-config-prettier";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import rxjsX from "eslint-plugin-rxjs-x";
|
||||
import rxjsAngularX from "eslint-plugin-rxjs-angular-x";
|
||||
import angularEslint from "@angular-eslint/eslint-plugin-template";
|
||||
import angularParser from "@angular-eslint/template-parser";
|
||||
import globals from "globals";
|
||||
|
||||
export default [
|
||||
// Global ignores (replaces .eslintignore)
|
||||
{
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"dist-cli/**",
|
||||
"build/**",
|
||||
"build-cli/**",
|
||||
"coverage/**",
|
||||
"**/*.cjs",
|
||||
"eslint.config.mjs",
|
||||
"scripts/**/*.js",
|
||||
"**/node_modules/**",
|
||||
],
|
||||
},
|
||||
|
||||
// Base config for all JavaScript/TypeScript files
|
||||
{
|
||||
files: ["**/*.ts", "**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.eslint.json"],
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin,
|
||||
import: importPlugin,
|
||||
"rxjs-x": rxjsX,
|
||||
"rxjs-angular-x": rxjsAngularX,
|
||||
},
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ESLint recommended rules
|
||||
...eslint.configs.recommended.rules,
|
||||
|
||||
// TypeScript ESLint recommended rules
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
|
||||
// Import plugin recommended rules
|
||||
...importPlugin.flatConfigs.recommended.rules,
|
||||
|
||||
// RxJS recommended rules
|
||||
...rxjsX.configs.recommended.rules,
|
||||
|
||||
// Custom project 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 on once each package is an actual package.
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
},
|
||||
"newlines-between": "always",
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "@/jslib/**/*",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/src/**/*",
|
||||
group: "parent",
|
||||
position: "before",
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin"],
|
||||
},
|
||||
],
|
||||
"rxjs-angular-x/prefer-takeuntil": "error",
|
||||
"rxjs-x/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/**/*"] }],
|
||||
},
|
||||
},
|
||||
|
||||
// Jest test files (includes any test-related files)
|
||||
{
|
||||
files: ["**/*.spec.ts", "**/test.setup.ts", "**/spec/**/*.ts", "**/utils/**/*fixtures*.ts"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Angular HTML templates
|
||||
{
|
||||
files: ["**/*.html"],
|
||||
languageOptions: {
|
||||
parser: angularParser,
|
||||
},
|
||||
plugins: {
|
||||
"@angular-eslint/template": angularEslint,
|
||||
},
|
||||
rules: {
|
||||
"@angular-eslint/template/button-has-type": "error",
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier config (must be last to override other configs)
|
||||
prettierConfig,
|
||||
];
|
||||
@@ -26,7 +26,6 @@ module.exports = {
|
||||
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
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
import { lastValueFrom, Observable, Subject } from "rxjs";
|
||||
|
||||
export class ModalRef {
|
||||
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
|
||||
@@ -45,6 +44,6 @@ export class ModalRef {
|
||||
}
|
||||
|
||||
onClosedPromise(): Promise<any> {
|
||||
return this.onClosed.pipe(first()).toPromise();
|
||||
return lastValueFrom(this.onClosed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,42 +13,47 @@ import {
|
||||
@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>
|
||||
@if (options.closeButton) {
|
||||
<button (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>
|
||||
@if (title) {
|
||||
<div [class]="options.titleClass" [attr.aria-label]="title">
|
||||
{{ title }}
|
||||
@if (duplicatesCount) {
|
||||
[{{ duplicatesCount + 1 }}]
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (message && options.enableHtml) {
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options.messageClass"
|
||||
[innerHTML]="message"
|
||||
></div>
|
||||
}
|
||||
@if (message && !options.enableHtml) {
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options.messageClass"
|
||||
[attr.aria-label]="message"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (options.progressBar) {
|
||||
<div>
|
||||
<div class="toast-progress" [style.width]="width + '%'"></div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
animations: [
|
||||
trigger("flyInOut", [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
|
||||
import { take } from "rxjs/operators";
|
||||
import { take } from "rxjs";
|
||||
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { first } from "rxjs/operators";
|
||||
import { first, firstValueFrom } from "rxjs";
|
||||
|
||||
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
|
||||
import { ModalInjector } from "../components/modal/modal-injector";
|
||||
@@ -58,7 +58,7 @@ export class ModalService {
|
||||
|
||||
viewContainerRef.insert(modalComponentRef.hostView);
|
||||
|
||||
await modalRef.onCreated.pipe(first()).toPromise();
|
||||
await firstValueFrom(modalRef.onCreated);
|
||||
|
||||
return [modalRef, modalComponentRef.instance.componentRef.instance];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
new SymmetricCryptoKey(null);
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Must provide key");
|
||||
expect(t).toThrow("Must provide key");
|
||||
});
|
||||
|
||||
describe("guesses encKey from key length", () => {
|
||||
@@ -63,7 +63,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
new SymmetricCryptoKey(makeStaticByteArray(30));
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Unable to determine encType.");
|
||||
expect(t).toThrow("Unable to determine encType.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,15 +8,12 @@ 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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,5 +33,5 @@ export function makeStaticByteArray(length: number, start = 0) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = start + i;
|
||||
}
|
||||
return arr;
|
||||
return arr.buffer;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ export enum StateVersion {
|
||||
Two = 2, // Move to a typed State object
|
||||
Three = 3, // Fix migration of users' premium status
|
||||
Four = 4, // Fix 'Never Lock' option by removing stale data
|
||||
Latest = Four,
|
||||
Five = 5, // New state service implementation
|
||||
Latest = Five,
|
||||
}
|
||||
|
||||
@@ -26,9 +26,4 @@ export class NodeUtils {
|
||||
.on("error", (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/31394257
|
||||
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import * as url from "url";
|
||||
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
|
||||
import * as tldjs from "tldjs";
|
||||
|
||||
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
||||
const nodeURL = typeof window === "undefined" ? url : null;
|
||||
|
||||
export class Utils {
|
||||
static inited = false;
|
||||
@@ -34,7 +36,7 @@ export class Utils {
|
||||
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
|
||||
}
|
||||
|
||||
static fromB64ToArray(str: string): Uint8Array {
|
||||
static fromB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "base64"));
|
||||
} else {
|
||||
@@ -47,11 +49,11 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromUrlB64ToArray(str: string): Uint8Array {
|
||||
static fromUrlB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
|
||||
}
|
||||
|
||||
static fromHexToArray(str: string): Uint8Array {
|
||||
static fromHexToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "hex"));
|
||||
} else {
|
||||
@@ -63,7 +65,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromUtf8ToArray(str: string): Uint8Array {
|
||||
static fromUtf8ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "utf8"));
|
||||
} else {
|
||||
@@ -76,7 +78,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromByteStringToArray(str: string): Uint8Array {
|
||||
static fromByteStringToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
const arr = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
arr[i] = str.charCodeAt(i);
|
||||
@@ -97,8 +99,8 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||
static fromBufferToUrlB64(buffer: Uint8Array<ArrayBuffer>): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer.buffer));
|
||||
}
|
||||
|
||||
static fromB64toUrlB64(b64Str: string) {
|
||||
@@ -247,7 +249,7 @@ export class Utils {
|
||||
const urlDomain =
|
||||
tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null;
|
||||
return urlDomain != null ? urlDomain : url.hostname;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Invalid domain, try another approach below.
|
||||
}
|
||||
}
|
||||
@@ -395,7 +397,7 @@ export class Utils {
|
||||
anchor.href = uriString;
|
||||
return anchor as any;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export class EncString {
|
||||
try {
|
||||
this.encryptionType = parseInt(headerPieces[0], null);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -114,7 +114,7 @@ export class EncString {
|
||||
key = await cryptoService.getOrgKey(orgId);
|
||||
}
|
||||
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.decryptedValue = "[error: cannot decrypt]";
|
||||
}
|
||||
return this.decryptedValue;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ClientType } from "../../../enums/clientType";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
|
||||
import { DeviceRequest } from "../deviceRequest";
|
||||
|
||||
@@ -30,5 +29,4 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export abstract class TokenRequest {
|
||||
this.device = device != null ? device : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
// Implemented in subclass if required
|
||||
}
|
||||
|
||||
@@ -335,9 +335,11 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async clearStoredKey(keySuffix: KeySuffixOptions) {
|
||||
keySuffix === KeySuffixOptions.Auto
|
||||
? await this.stateService.setCryptoMasterKeyAuto(null)
|
||||
: await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null);
|
||||
} else {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||
}
|
||||
}
|
||||
|
||||
async clearKeyHash(userId?: string): Promise<any> {
|
||||
@@ -634,9 +636,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array = null;
|
||||
let ivBytes: Uint8Array = null;
|
||||
let macBytes: Uint8Array = null;
|
||||
let ctBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let ivBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let macBytes: Uint8Array<ArrayBuffer> = null;
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
@@ -717,7 +719,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
|
||||
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ const partialKeys = {
|
||||
export class StateService<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> implements StateServiceAbstraction<TAccount>
|
||||
{
|
||||
> implements StateServiceAbstraction<TAccount> {
|
||||
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import * as path from "path";
|
||||
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
NativeImage,
|
||||
nativeImage,
|
||||
Tray,
|
||||
} from "electron";
|
||||
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
@@ -12,8 +20,8 @@ export class TrayMain {
|
||||
|
||||
private appName: string;
|
||||
private tray: Tray;
|
||||
private icon: string | Electron.NativeImage;
|
||||
private pressedIcon: Electron.NativeImage;
|
||||
private icon: string | NativeImage;
|
||||
private pressedIcon: NativeImage;
|
||||
|
||||
constructor(
|
||||
private windowMain: WindowMain,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
import { app, BrowserWindow, screen } from "electron";
|
||||
import { app, BrowserWindow, Rectangle, screen } from "electron";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
@@ -14,7 +14,7 @@ export class WindowMain {
|
||||
win: BrowserWindow;
|
||||
isQuitting = false;
|
||||
|
||||
private windowStateChangeTimer: NodeJS.Timeout;
|
||||
private windowStateChangeTimer: ReturnType<typeof setTimeout>;
|
||||
private windowStates: { [key: string]: any } = {};
|
||||
private enableAlwaysOnTop = false;
|
||||
|
||||
@@ -37,7 +37,6 @@ export class WindowMain {
|
||||
app.quit();
|
||||
return;
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
app.on("second-instance", (event, argv, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (this.win != null) {
|
||||
@@ -241,7 +240,7 @@ export class WindowMain {
|
||||
const state = await this.stateService.getWindow();
|
||||
|
||||
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
||||
let displayBounds: Electron.Rectangle = null;
|
||||
let displayBounds: Rectangle = null;
|
||||
if (!isValid) {
|
||||
state.width = defaultWidth;
|
||||
state.height = defaultHeight;
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("NodeCrypto Function Service", () => {
|
||||
it("should fail with prk too small", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk16Byte),
|
||||
Utils.fromB64ToArray(prk16Byte).buffer,
|
||||
"info",
|
||||
32,
|
||||
"sha256",
|
||||
@@ -105,7 +105,7 @@ describe("NodeCrypto Function Service", () => {
|
||||
it("should fail with outputByteSize is too large", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk32Byte),
|
||||
Utils.fromB64ToArray(prk32Byte).buffer,
|
||||
"info",
|
||||
8161,
|
||||
"sha256",
|
||||
@@ -341,7 +341,7 @@ function testHkdf(
|
||||
utf8Key: string,
|
||||
unicodeKey: string,
|
||||
) {
|
||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==");
|
||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer;
|
||||
|
||||
const regularSalt = "salt";
|
||||
const utf8Salt = "üser_salt";
|
||||
@@ -393,7 +393,7 @@ function testHkdfExpand(
|
||||
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const okm = await cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(b64prk),
|
||||
Utils.fromB64ToArray(b64prk).buffer,
|
||||
info,
|
||||
outputByteSize,
|
||||
algorithm,
|
||||
|
||||
11932
package-lock.json
generated
11932
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2026.1.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -31,14 +31,14 @@
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:main": "webpack --config webpack.main.js",
|
||||
"build:renderer": "webpack --config webpack.renderer.js",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
|
||||
"build:main": "webpack --config webpack.main.cjs",
|
||||
"build:renderer": "webpack --config webpack.renderer.cjs",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.cjs --watch",
|
||||
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
||||
"build:cli": "webpack --config webpack.cli.js",
|
||||
"build:cli:watch": "webpack --config webpack.cli.js --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
|
||||
"build:cli": "webpack --config webpack.cli.cjs",
|
||||
"build:cli:watch": "webpack --config webpack.cli.cjs --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs --watch",
|
||||
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"electron:ignore": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 --ignore-certificate-errors ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"clean:dist": "rimraf --glob ./dist/*",
|
||||
@@ -73,27 +73,30 @@
|
||||
"test:types": "npx tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "20.3.3",
|
||||
"@angular-eslint/eslint-plugin-template": "20.6.0",
|
||||
"@angular-eslint/template-parser": "20.6.0",
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@angular-eslint/eslint-plugin-template": "21.1.0",
|
||||
"@angular-eslint/template-parser": "21.1.0",
|
||||
"@angular/build": "21.0.5",
|
||||
"@angular/compiler-cli": "21.0.8",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "4.0.1",
|
||||
"@fluffy-spoon/substitute": "1.208.0",
|
||||
"@microsoft/microsoft-graph-types": "2.43.1",
|
||||
"@ngtools/webpack": "20.3.3",
|
||||
"@ngtools/webpack": "21.0.5",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lowdb": "1.0.15",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/node": "22.19.2",
|
||||
"@types/node-fetch": "2.6.12",
|
||||
"@types/node-forge": "1.3.11",
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/tldjs": "2.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"@yao-pkg/pkg": "5.16.1",
|
||||
"babel-loader": "9.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"jest-environment-jsdom": "30.2.0",
|
||||
"concurrently": "9.2.0",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -104,22 +107,22 @@
|
||||
"electron-log": "5.4.1",
|
||||
"electron-reload": "2.0.0-alpha.1",
|
||||
"electron-store": "8.2.0",
|
||||
"electron-updater": "6.6.2",
|
||||
"eslint": "8.57.1",
|
||||
"electron-updater": "6.7.3",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-rxjs": "5.0.3",
|
||||
"eslint-plugin-rxjs-angular": "2.0.1",
|
||||
"eslint-plugin-rxjs-angular-x": "0.1.0",
|
||||
"eslint-plugin-rxjs-x": "0.8.3",
|
||||
"form-data": "4.0.4",
|
||||
"glob": "13.0.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"husky": "9.1.7",
|
||||
"jest": "29.7.0",
|
||||
"jest": "30.2.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"jest-mock-extended": "4.0.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jest-preset-angular": "16.0.0",
|
||||
"lint-staged": "16.2.6",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimatch": "5.1.2",
|
||||
@@ -128,30 +131,30 @@
|
||||
"prettier": "3.7.4",
|
||||
"rimraf": "6.1.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sass": "1.94.2",
|
||||
"sass": "1.97.1",
|
||||
"sass-loader": "16.0.5",
|
||||
"ts-jest": "29.4.1",
|
||||
"ts-loader": "9.5.2",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "5.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"webpack": "5.103.0",
|
||||
"typescript": "5.9.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-node-externals": "3.0.0",
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/cli": "20.3.3",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/forms": "20.3.15",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@angular/animations": "21.0.8",
|
||||
"@angular/cdk": "21.0.6",
|
||||
"@angular/cli": "21.0.5",
|
||||
"@angular/common": "21.0.8",
|
||||
"@angular/compiler": "21.0.8",
|
||||
"@angular/core": "21.0.8",
|
||||
"@angular/forms": "21.0.8",
|
||||
"@angular/platform-browser": "21.0.8",
|
||||
"@angular/platform-browser-dynamic": "21.0.8",
|
||||
"@angular/router": "21.0.8",
|
||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||
"big-integer": "1.6.52",
|
||||
"bootstrap": "5.3.7",
|
||||
@@ -163,7 +166,7 @@
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"keytar": "7.9.0",
|
||||
"ldapts": "8.0.1",
|
||||
"ldapts": "8.1.3",
|
||||
"lowdb": "1.0.0",
|
||||
"ngx-toastr": "19.1.0",
|
||||
"node-fetch": "2.7.0",
|
||||
|
||||
54
src/abstractions/state-vNext.service.ts
Normal file
54
src/abstractions/state-vNext.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
|
||||
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
export abstract class StateServiceVNext {
|
||||
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||
setDirectory: (
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
| GSuiteConfiguration
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
) => Promise<any>;
|
||||
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getEntraConfiguration: (options?: StorageOptions) => Promise<EntraIdConfiguration>;
|
||||
setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||
setOneLoginConfiguration: (
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { EnvironmentComponent } from "./environment.component";
|
||||
// The only subscription in this component is closed from a child component, confusing eslint.
|
||||
// https://github.com/cartant/eslint-plugin-rxjs-angular/blob/main/docs/rules/prefer-takeuntil.md
|
||||
//
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
|
||||
export class ApiKeyComponent {
|
||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||
environmentModal: ViewContainerRef;
|
||||
@@ -100,7 +100,7 @@ export class ApiKeyComponent {
|
||||
this.environmentModal,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
|
||||
childComponent.onSaved.pipe(takeUntil(modalRef.onClosed)).subscribe(() => {
|
||||
modalRef.close();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import { isDev } from "@/jslib/electron/src/utils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require("../scss/styles.scss");
|
||||
import "../scss/styles.scss";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
@@ -12,4 +11,7 @@ if (!isDev()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, {
|
||||
applicationProviders: [provideZoneChangeDetection()],
|
||||
preserveWhitespaces: true,
|
||||
});
|
||||
|
||||
@@ -31,12 +31,14 @@ import { DefaultDirectoryFactoryService } from "@/src/services/directory-factory
|
||||
import { SingleRequestBuilder } from "@/src/services/single-request-builder";
|
||||
|
||||
import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
|
||||
import { StateServiceVNext } from "../../abstractions/state-vNext.service";
|
||||
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||
import { Account } from "../../models/account";
|
||||
import { AuthService } from "../../services/auth.service";
|
||||
import { I18nService } from "../../services/i18n.service";
|
||||
import { StateService } from "../../services/state.service";
|
||||
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||
import { StateServiceVNextImplementation } from "../../services/state-service/state-vNext.service";
|
||||
import { StateService } from "../../services/state-service/state.service";
|
||||
import { StateMigrationService } from "../../services/state-service/stateMigration.service";
|
||||
import { SyncService } from "../../services/sync.service";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
@@ -222,6 +224,29 @@ export function initFactory(
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
// Use new StateServiceVNext with flat key-value structure (new interface)
|
||||
safeProvider({
|
||||
provide: StateServiceVNext,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
logService: LogServiceAbstraction,
|
||||
stateMigrationService: StateMigrationServiceAbstraction,
|
||||
) =>
|
||||
new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
true,
|
||||
),
|
||||
deps: [
|
||||
StorageServiceAbstraction,
|
||||
SECURE_STORAGE,
|
||||
LogServiceAbstraction,
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SingleRequestBuilder,
|
||||
deps: [],
|
||||
@@ -233,7 +258,12 @@ export function initFactory(
|
||||
safeProvider({
|
||||
provide: DirectoryFactoryService,
|
||||
useClass: DefaultDirectoryFactoryService,
|
||||
deps: [LogServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction],
|
||||
deps: [
|
||||
LogServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
StateServiceVNext,
|
||||
],
|
||||
}),
|
||||
] satisfies SafeProvider[],
|
||||
})
|
||||
|
||||
@@ -3,17 +3,25 @@
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{ "lastGroupSync" | i18n }}:
|
||||
<span *ngIf="!lastGroupSync">-</span>
|
||||
@if (!lastGroupSync) {
|
||||
<span>-</span>
|
||||
}
|
||||
{{ lastGroupSync | date: "medium" }}
|
||||
<br />
|
||||
{{ "lastUserSync" | i18n }}:
|
||||
<span *ngIf="!lastUserSync">-</span>
|
||||
@if (!lastUserSync) {
|
||||
<span>-</span>
|
||||
}
|
||||
{{ lastUserSync | date: "medium" }}
|
||||
</p>
|
||||
<p>
|
||||
{{ "syncStatus" | i18n }}:
|
||||
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
|
||||
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
|
||||
@if (syncRunning) {
|
||||
<strong class="text-success">{{ "running" | i18n }}</strong>
|
||||
}
|
||||
@if (!syncRunning) {
|
||||
<strong class="text-danger">{{ "stopped" | i18n }}</strong>
|
||||
}
|
||||
</p>
|
||||
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
||||
<button
|
||||
@@ -60,57 +68,85 @@
|
||||
/>
|
||||
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
||||
@if (!simForm.loading && (simUsers || simGroups)) {
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>{{ "users" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
||||
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
@if (simEnabledUsers && simEnabledUsers.length) {
|
||||
<ul class="bwi-ul testing-list">
|
||||
@for (u of simEnabledUsers; track u) {
|
||||
<li title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (!simEnabledUsers || !simEnabledUsers.length) {
|
||||
<p>
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
}
|
||||
<h4>{{ "disabledUsers" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
||||
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
@if (simDisabledUsers && simDisabledUsers.length) {
|
||||
<ul class="bwi-ul testing-list">
|
||||
@for (u of simDisabledUsers; track u) {
|
||||
<li title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (!simDisabledUsers || !simDisabledUsers.length) {
|
||||
<p>
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
}
|
||||
<h4>{{ "deletedUsers" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
||||
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
@if (simDeletedUsers && simDeletedUsers.length) {
|
||||
<ul class="bwi-ul testing-list">
|
||||
@for (u of simDeletedUsers; track u) {
|
||||
<li title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (!simDeletedUsers || !simDeletedUsers.length) {
|
||||
<p>
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<h4>{{ "groups" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
|
||||
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-sitemap"></i>
|
||||
{{ g.displayName }}
|
||||
<ul class="small" *ngIf="g.users && g.users.length">
|
||||
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
|
||||
{{ u.displayName }}
|
||||
@if (simGroups && simGroups.length) {
|
||||
<ul class="bwi-ul testing-list">
|
||||
@for (g of simGroups; track g) {
|
||||
<li title="{{ g.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-sitemap"></i>
|
||||
{{ g.displayName }}
|
||||
@if (g.users && g.users.length) {
|
||||
<ul class="small">
|
||||
@for (u of g.users; track u) {
|
||||
<li title="{{ u.referenceId }}">
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (!simGroups || !simGroups.length) {
|
||||
<p>{{ "noGroups" | i18n }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
<div class="mb-3">
|
||||
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
|
||||
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
|
||||
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
@for (o of directoryOptions; track o) {
|
||||
<option [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap">
|
||||
@@ -51,20 +53,22 @@
|
||||
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" *ngIf="!ldap.ad">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="pagedSearch"
|
||||
[(ngModel)]="ldap.pagedSearch"
|
||||
name="PagedSearch"
|
||||
/>
|
||||
<label class="form-check-label" for="pagedSearch">{{
|
||||
"ldapPagedResults" | i18n
|
||||
}}</label>
|
||||
@if (!ldap.ad) {
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="pagedSearch"
|
||||
[(ngModel)]="ldap.pagedSearch"
|
||||
name="PagedSearch"
|
||||
/>
|
||||
<label class="form-check-label" for="pagedSearch">{{
|
||||
"ldapPagedResults" | i18n
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
@@ -79,116 +83,122 @@
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-4" *ngIf="ldap.ssl">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
[value]="false"
|
||||
id="ssl"
|
||||
[(ngModel)]="ldap.startTls"
|
||||
name="SSL"
|
||||
/>
|
||||
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
||||
@if (ldap.ssl) {
|
||||
<div class="ms-4">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
[value]="false"
|
||||
id="ssl"
|
||||
[(ngModel)]="ldap.startTls"
|
||||
name="SSL"
|
||||
/>
|
||||
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
[value]="true"
|
||||
id="startTls"
|
||||
[(ngModel)]="ldap.startTls"
|
||||
name="StartTLS"
|
||||
/>
|
||||
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
[value]="true"
|
||||
id="startTls"
|
||||
[(ngModel)]="ldap.startTls"
|
||||
name="StartTLS"
|
||||
/>
|
||||
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
||||
@if (ldap.startTls) {
|
||||
<div class="ms-4">
|
||||
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
||||
<div class="mb-3">
|
||||
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="tlsCaPath_file"
|
||||
(change)="setSslPath('tlsCaPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="tlsCaPath"
|
||||
name="TLSCaPath"
|
||||
[(ngModel)]="ldap.tlsCaPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!ldap.startTls) {
|
||||
<div class="ms-4">
|
||||
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
||||
<div class="mb-3">
|
||||
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslCertPath_file"
|
||||
(change)="setSslPath('sslCertPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslCertPath"
|
||||
name="SSLCertPath"
|
||||
[(ngModel)]="ldap.sslCertPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslKeyPath_file"
|
||||
(change)="setSslPath('sslKeyPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslKeyPath"
|
||||
name="SSLKeyPath"
|
||||
[(ngModel)]="ldap.sslKeyPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslCaPath_file"
|
||||
(change)="setSslPath('sslCaPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslCaPath"
|
||||
name="SSLCaPath"
|
||||
[(ngModel)]="ldap.sslCaPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="certDoNotVerify"
|
||||
[(ngModel)]="ldap.sslAllowUnauthorized"
|
||||
name="CertDoNoVerify"
|
||||
/>
|
||||
<label class="form-check-label" for="certDoNotVerify">{{
|
||||
"ldapCertDoNotVerify" | i18n
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-4" *ngIf="ldap.startTls">
|
||||
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
||||
<div class="mb-3">
|
||||
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="tlsCaPath_file"
|
||||
(change)="setSslPath('tlsCaPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="tlsCaPath"
|
||||
name="TLSCaPath"
|
||||
[(ngModel)]="ldap.tlsCaPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-4" *ngIf="!ldap.startTls">
|
||||
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
||||
<div class="mb-3">
|
||||
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslCertPath_file"
|
||||
(change)="setSslPath('sslCertPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslCertPath"
|
||||
name="SSLCertPath"
|
||||
[(ngModel)]="ldap.sslCertPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslKeyPath_file"
|
||||
(change)="setSslPath('sslKeyPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslKeyPath"
|
||||
name="SSLKeyPath"
|
||||
[(ngModel)]="ldap.sslKeyPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control mb-2"
|
||||
id="sslCaPath_file"
|
||||
(change)="setSslPath('sslCaPath')"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sslCaPath"
|
||||
name="SSLCaPath"
|
||||
[(ngModel)]="ldap.sslCaPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="certDoNotVerify"
|
||||
[(ngModel)]="ldap.sslAllowUnauthorized"
|
||||
name="CertDoNoVerify"
|
||||
/>
|
||||
<label class="form-check-label" for="certDoNotVerify">{{
|
||||
"ldapCertDoNotVerify" | i18n
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="mb-3" [hidden]="true">
|
||||
<div class="form-check">
|
||||
<input
|
||||
@@ -211,10 +221,12 @@
|
||||
name="Username"
|
||||
[(ngModel)]="ldap.username"
|
||||
/>
|
||||
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
|
||||
<div class="form-text" *ngIf="!ldap.ad">
|
||||
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
|
||||
</div>
|
||||
@if (ldap.ad) {
|
||||
<div class="form-text">{{ "ex" | i18n }} company\admin</div>
|
||||
}
|
||||
@if (!ldap.ad) {
|
||||
<div class="form-text">{{ "ex" | i18n }} cn=admin,dc=company,dc=com</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">{{ "password" | i18n }}</label>
|
||||
@@ -604,18 +616,24 @@
|
||||
name="UserFilter"
|
||||
[(ngModel)]="sync.userFilter"
|
||||
></textarea>
|
||||
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||
{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.EntraID">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | orgUnitPath=/Engineering
|
||||
</div>
|
||||
@if (directory === directoryType.Ldap) {
|
||||
<div class="form-text">
|
||||
{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))
|
||||
</div>
|
||||
}
|
||||
@if (directory === directoryType.EntraID) {
|
||||
<div class="form-text">{{ "ex" | i18n }} exclude:joe@company.com</div>
|
||||
}
|
||||
@if (directory === directoryType.Okta) {
|
||||
<div class="form-text">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
||||
</div>
|
||||
}
|
||||
@if (directory === directoryType.GSuite) {
|
||||
<div class="form-text">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | orgUnitPath=/Engineering
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
|
||||
@@ -681,18 +699,20 @@
|
||||
name="GroupFilter"
|
||||
[(ngModel)]="sync.groupFilter"
|
||||
></textarea>
|
||||
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||
{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.EntraID">
|
||||
{{ "ex" | i18n }} include:Sales,IT
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||
{{ "ex" | i18n }} include:Sales,IT
|
||||
</div>
|
||||
@if (directory === directoryType.Ldap) {
|
||||
<div class="form-text">
|
||||
{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
|
||||
</div>
|
||||
}
|
||||
@if (directory === directoryType.EntraID) {
|
||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
|
||||
}
|
||||
@if (directory === directoryType.Okta) {
|
||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</div>
|
||||
}
|
||||
@if (directory === directoryType.GSuite) {
|
||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
|
||||
@@ -703,8 +723,12 @@
|
||||
name="GroupPath"
|
||||
[(ngModel)]="sync.groupPath"
|
||||
/>
|
||||
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
|
||||
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
|
||||
@if (!ldap.ad) {
|
||||
<div class="form-text">{{ "ex" | i18n }} CN=Groups</div>
|
||||
}
|
||||
@if (ldap.ad) {
|
||||
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
|
||||
}
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||
<div class="mb-3">
|
||||
|
||||
16
src/bwdc.ts
16
src/bwdc.ts
@@ -18,6 +18,7 @@ import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
||||
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
|
||||
|
||||
import { DirectoryFactoryService } from "./abstractions/directory-factory.service";
|
||||
import { StateServiceVNext } from "./abstractions/state-vNext.service";
|
||||
import { Account } from "./models/account";
|
||||
import { Program } from "./program";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
@@ -27,8 +28,9 @@ import { I18nService } from "./services/i18n.service";
|
||||
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
|
||||
import { LowdbStorageService } from "./services/lowdbStorage.service";
|
||||
import { SingleRequestBuilder } from "./services/single-request-builder";
|
||||
import { StateService } from "./services/state.service";
|
||||
import { StateMigrationService } from "./services/stateMigration.service";
|
||||
import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service";
|
||||
import { StateService } from "./services/state-service/state.service";
|
||||
import { StateMigrationService } from "./services/state-service/stateMigration.service";
|
||||
import { SyncService } from "./services/sync.service";
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -53,6 +55,7 @@ export class Main {
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
syncService: SyncService;
|
||||
stateServiceVNext: StateServiceVNext;
|
||||
stateService: StateService;
|
||||
stateMigrationService: StateMigrationService;
|
||||
directoryFactoryService: DirectoryFactoryService;
|
||||
@@ -116,6 +119,14 @@ export class Main {
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
// Use new StateServiceVNext with flat key-value structure
|
||||
this.stateServiceVNext = new StateServiceVNextImplementation(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.logService,
|
||||
this.stateMigrationService,
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
);
|
||||
|
||||
this.cryptoService = new CryptoService(
|
||||
this.cryptoFunctionService,
|
||||
@@ -157,6 +168,7 @@ export class Main {
|
||||
this.logService,
|
||||
this.i18nService,
|
||||
this.stateService,
|
||||
this.stateServiceVNext,
|
||||
);
|
||||
|
||||
this.batchRequestBuilder = new BatchRequestBuilder();
|
||||
|
||||
13
src/main.ts
13
src/main.ts
@@ -11,12 +11,14 @@ import { TrayMain } from "@/jslib/electron/src/tray.main";
|
||||
import { UpdaterMain } from "@/jslib/electron/src/updater.main";
|
||||
import { WindowMain } from "@/jslib/electron/src/window.main";
|
||||
|
||||
import { StateServiceVNext } from "./abstractions/state-vNext.service";
|
||||
import { DCCredentialStorageListener } from "./main/credential-storage-listener";
|
||||
import { MenuMain } from "./main/menu.main";
|
||||
import { MessagingMain } from "./main/messaging.main";
|
||||
import { Account } from "./models/account";
|
||||
import { I18nService } from "./services/i18n.service";
|
||||
import { StateService } from "./services/state.service";
|
||||
import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service";
|
||||
import { StateService } from "./services/state-service/state.service";
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
@@ -24,6 +26,7 @@ export class Main {
|
||||
storageService: ElectronStorageService;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
credentialStorageListener: DCCredentialStorageListener;
|
||||
stateServiceVNext: StateServiceVNext;
|
||||
stateService: StateService;
|
||||
|
||||
windowMain: WindowMain;
|
||||
@@ -66,6 +69,14 @@ export class Main {
|
||||
true,
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
// Use new StateServiceVNext with flat key-value structure
|
||||
this.stateServiceVNext = new StateServiceVNextImplementation(
|
||||
this.storageService,
|
||||
null,
|
||||
this.logService,
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
this.windowMain = new WindowMain(
|
||||
this.stateService,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MenuMain } from "./menu.main";
|
||||
const SyncCheckInterval = 60 * 1000; // 1 minute
|
||||
|
||||
export class MessagingMain {
|
||||
private syncTimeout: NodeJS.Timeout;
|
||||
private syncTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(
|
||||
private windowMain: WindowMain,
|
||||
|
||||
108
src/models/state.model.ts
Normal file
108
src/models/state.model.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// ===================================================================
|
||||
// vNext Storage Keys (Flat key-value structure)
|
||||
// ===================================================================
|
||||
|
||||
export const StorageKeysVNext = {
|
||||
stateVersion: "stateVersion",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
directory_ldap: "directory_ldap",
|
||||
directory_gsuite: "directory_gsuite",
|
||||
directory_entra: "directory_entra",
|
||||
directory_okta: "directory_okta",
|
||||
directory_onelogin: "directory_onelogin",
|
||||
sync: "sync",
|
||||
syncingDir: "syncingDir",
|
||||
};
|
||||
|
||||
export const SecureStorageKeysVNext: { [key: string]: any } = {
|
||||
ldap: "secret_ldap",
|
||||
gsuite: "secret_gsuite",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "secret_azure",
|
||||
entra: "secret_entra",
|
||||
okta: "secret_okta",
|
||||
oneLogin: "secret_oneLogin",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Legacy Storage Keys (Account-based hierarchy)
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysLegacy = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "azureKey",
|
||||
entra: "entraKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
export const TempKeys = {
|
||||
tempAccountSettings: "tempAccountSettings",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Migration Storage Keys
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysMigration: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
export const MigrationKeys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
export const MigrationStateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
export const MigrationClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Shared Constants
|
||||
// ===================================================================
|
||||
|
||||
export const StoredSecurely = "[STORED SECURELY]";
|
||||
2
src/scss/bootstrap.scss
vendored
2
src/scss/bootstrap.scss
vendored
@@ -28,4 +28,4 @@ $danger: map_get($theme-colors, "danger");
|
||||
$secondary: map_get($theme-colors, "secondary");
|
||||
$secondary-alt: map_get($theme-colors, "secondary-alt");
|
||||
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
@import "bootstrap/scss/bootstrap.scss";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
@import "bootstrap/scss/_variables.scss";
|
||||
|
||||
html.os_windows {
|
||||
body {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
@import "bootstrap/scss/_variables.scss";
|
||||
|
||||
body {
|
||||
padding: 10px 0 20px 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "~ngx-toastr/toastr";
|
||||
@import "ngx-toastr/toastr";
|
||||
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
@import "bootstrap/scss/_variables.scss";
|
||||
|
||||
.toast-container {
|
||||
.toast-close-button {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { MessagingService } from "../../jslib/common/src/abstractions/messaging.
|
||||
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||
|
||||
import { AuthService } from "./auth.service";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
|
||||
const clientId = "organization.CLIENT_ID";
|
||||
const clientSecret = "CLIENT_SECRET";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
|
||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
||||
import { StateServiceVNext } from "../abstractions/state-vNext.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
|
||||
@@ -16,12 +17,18 @@ export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
private stateServiceVNext: StateServiceVNext,
|
||||
) {}
|
||||
|
||||
createService(directoryType: DirectoryType) {
|
||||
switch (directoryType) {
|
||||
case DirectoryType.GSuite:
|
||||
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||
return new GSuiteDirectoryService(
|
||||
this.logService,
|
||||
this.i18nService,
|
||||
this.stateService,
|
||||
this.stateServiceVNext,
|
||||
);
|
||||
case DirectoryType.EntraID:
|
||||
return new EntraIdDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||
case DirectoryType.Ldap:
|
||||
|
||||
@@ -132,7 +132,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
||||
}
|
||||
|
||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
const users: graphType.User[] = res.value;
|
||||
if (users != null) {
|
||||
@@ -211,7 +211,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
||||
let auMembers = await this.client
|
||||
.api(`${this.getGraphApiEndpoint()}/v1.0/directory/administrativeUnits/${p}/members`)
|
||||
.get();
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
for (const auMember of auMembers.value) {
|
||||
const groupId = auMember.id;
|
||||
@@ -328,7 +328,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
||||
const entries: GroupEntry[] = [];
|
||||
const groupsReq = this.client.api("/groups");
|
||||
let res = await groupsReq.get();
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
const groups: graphType.Group[] = res.value;
|
||||
if (groups != null) {
|
||||
@@ -421,7 +421,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
||||
|
||||
const memReq = this.client.api("/groups/" + group.id + "/members");
|
||||
let memRes = await memReq.get();
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
const members: any = memRes.value;
|
||||
if (members != null) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateServiceVNext } from "@/src/abstractions/state-vNext.service";
|
||||
|
||||
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "../../../jslib/common/src/abstractions/log.service";
|
||||
import {
|
||||
@@ -10,7 +12,7 @@ import {
|
||||
import { groupFixtures } from "../../../utils/google-workspace/group-fixtures";
|
||||
import { userFixtures } from "../../../utils/google-workspace/user-fixtures";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { StateService } from "../state.service";
|
||||
import { StateService } from "../state-service/state.service";
|
||||
|
||||
import { GSuiteDirectoryService } from "./gsuite-directory.service";
|
||||
|
||||
@@ -35,6 +37,7 @@ describe("gsuiteDirectoryService", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateServiceVNext: MockProxy<StateServiceVNext>;
|
||||
|
||||
let directoryService: GSuiteDirectoryService;
|
||||
|
||||
@@ -42,23 +45,31 @@ describe("gsuiteDirectoryService", () => {
|
||||
logService = mock();
|
||||
i18nService = mock();
|
||||
stateService = mock();
|
||||
stateServiceVNext = mock();
|
||||
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.GSuite);
|
||||
stateServiceVNext.getDirectoryType.mockResolvedValue(DirectoryType.GSuite);
|
||||
stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date
|
||||
i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages
|
||||
|
||||
directoryService = new GSuiteDirectoryService(logService, i18nService, stateService);
|
||||
directoryService = new GSuiteDirectoryService(
|
||||
logService,
|
||||
i18nService,
|
||||
stateService,
|
||||
stateServiceVNext,
|
||||
);
|
||||
});
|
||||
|
||||
it("syncs without using filters (includes test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
stateServiceVNext.getDirectory
|
||||
.calledWith(DirectoryType.GSuite)
|
||||
.mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: true,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
stateServiceVNext.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
@@ -68,7 +79,9 @@ describe("gsuiteDirectoryService", () => {
|
||||
|
||||
it("syncs using user and group filters (exact match for test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
stateServiceVNext.getDirectory
|
||||
.calledWith(DirectoryType.GSuite)
|
||||
.mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
@@ -76,7 +89,7 @@ describe("gsuiteDirectoryService", () => {
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
groupFilter: INTEGRATION_GROUP_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
stateServiceVNext.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { admin_directory_v1, google } from "googleapis";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
|
||||
import { StateServiceVNext } from "@/src/abstractions/state-vNext.service";
|
||||
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { GroupEntry } from "../../models/groupEntry";
|
||||
@@ -25,25 +27,26 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
private stateServiceVNext: StateServiceVNext,
|
||||
) {
|
||||
super();
|
||||
this.service = google.admin("directory_v1");
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.stateService.getDirectoryType();
|
||||
const type = await this.stateServiceVNext.getDirectoryType();
|
||||
if (type !== DirectoryType.GSuite) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
|
||||
this.dirConfig = await this.stateServiceVNext.getDirectory<GSuiteConfiguration>(
|
||||
DirectoryType.GSuite,
|
||||
);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.stateService.getSync();
|
||||
this.syncConfig = await this.stateServiceVNext.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
@@ -71,7 +74,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
let nextPageToken: string = null;
|
||||
|
||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
|
||||
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
||||
@@ -99,7 +102,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
}
|
||||
|
||||
nextPageToken = null;
|
||||
// eslint-disable-next-line
|
||||
|
||||
while (true) {
|
||||
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
|
||||
const p = Object.assign(
|
||||
@@ -154,7 +157,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
|
||||
let nextPageToken: string = null;
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
|
||||
let p = null;
|
||||
@@ -194,7 +196,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
entry.externalId = group.id;
|
||||
entry.name = group.name;
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
||||
const memRes = await this.service.members.list(p);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { groupFixtures } from "../../../utils/openldap/group-fixtures";
|
||||
import { userFixtures } from "../../../utils/openldap/user-fixtures";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { StateService } from "../state.service";
|
||||
import { StateService } from "../state-service/state.service";
|
||||
|
||||
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||
|
||||
|
||||
488
src/services/state-service/state-vNext.service.spec.ts
Normal file
488
src/services/state-service/state-vNext.service.spec.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
|
||||
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import { StorageKeysVNext as StorageKeys, StoredSecurely } from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
import { StateServiceVNextImplementation } from "./state-vNext.service";
|
||||
|
||||
describe("StateServiceVNextImplementation", () => {
|
||||
let storageService: MockProxy<StorageService>;
|
||||
let secureStorageService: MockProxy<StorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateMigrationService: MockProxy<StateMigrationService>;
|
||||
let stateService: StateServiceVNextImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = mock<StorageService>();
|
||||
secureStorageService = mock<StorageService>();
|
||||
logService = mock<LogService>();
|
||||
stateMigrationService = mock<StateMigrationService>();
|
||||
|
||||
stateService = new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
true, // useSecureStorageForSecrets
|
||||
);
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("should run migration if needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(true);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run migration if not needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(false);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clean", () => {
|
||||
it("should clear all directory settings and configurations", async () => {
|
||||
await stateService.clean();
|
||||
|
||||
// Verify all directory types are cleared
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.directoryType, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Directory Type", () => {
|
||||
it("should store and retrieve directory type", async () => {
|
||||
storageService.get.mockResolvedValue(DirectoryType.Ldap);
|
||||
|
||||
await stateService.setDirectoryType(DirectoryType.Ldap);
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(
|
||||
StorageKeys.directoryType,
|
||||
DirectoryType.Ldap,
|
||||
);
|
||||
expect(result).toBe(DirectoryType.Ldap);
|
||||
});
|
||||
|
||||
it("should return null when directory type is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organization Id", () => {
|
||||
it("should store and retrieve organization ID", async () => {
|
||||
const orgId = "test-org-123";
|
||||
|
||||
storageService.get.mockResolvedValue(orgId);
|
||||
|
||||
await stateService.setOrganizationId(orgId);
|
||||
const result = await stateService.getOrganizationId();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, orgId);
|
||||
expect(result).toBe(orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LDAP Configuration", () => {
|
||||
it("should store and retrieve LDAP configuration with secrets in secure storage", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-password");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
password: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
const result = await stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Verify password is stored in secure storage
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify configuration is stored
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify retrieved config has real password from secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
});
|
||||
|
||||
it("should return null when LDAP configuration is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLdapConfiguration();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null password in LDAP configuration", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: null,
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Null passwords should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GSuite Configuration", () => {
|
||||
it("should store and retrieve GSuite configuration with privateKey in secure storage", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: "private-key-content",
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("private-key-content");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
privateKey: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
const result = await stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.privateKey).toBe("private-key-content");
|
||||
});
|
||||
|
||||
it("should handle null privateKey in GSuite configuration", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: null,
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
|
||||
// Null privateKey should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Entra ID Configuration", () => {
|
||||
it("should store and retrieve Entra ID configuration with key in secure storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: "secret-key",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-key");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
key: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.EntraID, config);
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("secret-key");
|
||||
});
|
||||
|
||||
it("should maintain backwards compatibility with Azure key storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: StoredSecurely,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
secureStorageService.get.mockResolvedValueOnce(null); // entra key not found
|
||||
secureStorageService.get.mockResolvedValueOnce("azure-secret-key"); // fallback to azure key
|
||||
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.get).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("azure-secret-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Okta Configuration", () => {
|
||||
it("should store and retrieve Okta configuration with token in secure storage", async () => {
|
||||
const config: OktaConfiguration = {
|
||||
orgUrl: "https://example.okta.com",
|
||||
token: "okta-token",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("okta-token");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
token: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Okta, config);
|
||||
const result = await stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.token).toBe("okta-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OneLogin Configuration", () => {
|
||||
it("should store and retrieve OneLogin configuration with clientSecret in secure storage", async () => {
|
||||
const config: OneLoginConfiguration = {
|
||||
region: "us",
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("client-secret");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
clientSecret: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.OneLogin, config);
|
||||
const result = await stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.clientSecret).toBe("client-secret");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Configuration", () => {
|
||||
it("should store and retrieve sync configuration", async () => {
|
||||
const syncConfig: SyncConfiguration = {
|
||||
users: true,
|
||||
groups: true,
|
||||
interval: 5,
|
||||
userFilter: null,
|
||||
groupFilter: null,
|
||||
removeDisabled: true,
|
||||
overwriteExisting: false,
|
||||
largeImport: false,
|
||||
groupObjectClass: null,
|
||||
userObjectClass: null,
|
||||
groupPath: null,
|
||||
userPath: null,
|
||||
groupNameAttribute: null,
|
||||
userEmailAttribute: null,
|
||||
memberAttribute: "member",
|
||||
creationDateAttribute: "whenCreated",
|
||||
revisionDateAttribute: "whenChanged",
|
||||
useEmailPrefixSuffix: false,
|
||||
emailPrefixAttribute: null,
|
||||
emailSuffix: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(syncConfig);
|
||||
|
||||
await stateService.setSync(syncConfig);
|
||||
const result = await stateService.getSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, syncConfig);
|
||||
expect(result).toEqual(syncConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Settings", () => {
|
||||
it("should clear sync settings when clearSyncSettings is called", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clear lastSyncHash when hashToo is true", async () => {
|
||||
await stateService.clearSyncSettings(true);
|
||||
|
||||
// Should set all values including lastSyncHash to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not clear lastSyncHash when hashToo is false", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values but not lastSyncHash
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Hash", () => {
|
||||
it("should store and retrieve last sync hash", async () => {
|
||||
const hash = "hash";
|
||||
|
||||
storageService.get.mockResolvedValue(hash);
|
||||
|
||||
await stateService.setLastSyncHash(hash);
|
||||
const result = await stateService.getLastSyncHash();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Delta Tokens", () => {
|
||||
it("should store and retrieve user delta token", async () => {
|
||||
const token = "user-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setUserDelta(token);
|
||||
const result = await stateService.getUserDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
|
||||
it("should store and retrieve group delta token", async () => {
|
||||
const token = "group-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setGroupDelta(token);
|
||||
const result = await stateService.getGroupDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Timestamps", () => {
|
||||
it("should store and retrieve last user sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastUserSync(timestamp);
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should store and retrieve last group sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastGroupSync(timestamp);
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should return null when last user sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when last group sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Secure Storage Flag", () => {
|
||||
it("should not separate secrets when useSecureStorageForSecrets is false", async () => {
|
||||
const insecureStateService = new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
false, // useSecureStorageForSecrets = false
|
||||
);
|
||||
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
|
||||
// When useSecureStorageForSecrets is false, setDirectory doesn't process secrets
|
||||
await insecureStateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Retrieve config - should return password as-is from storage (not from secure storage)
|
||||
const result = await insecureStateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Password should be retrieved directly from storage, not secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
expect(secureStorageService.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
409
src/services/state-service/state-vNext.service.ts
Normal file
409
src/services/state-service/state-vNext.service.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { StateServiceVNext as StateServiceVNextAbstraction } from "@/src/abstractions/state-vNext.service";
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { IConfiguration } from "@/src/models/IConfiguration";
|
||||
import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
|
||||
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import {
|
||||
SecureStorageKeysVNext as SecureStorageKeys,
|
||||
StorageKeysVNext as StorageKeys,
|
||||
StoredSecurely,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
export class StateServiceVNextImplementation implements StateServiceVNextAbstraction {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected logService: LogService,
|
||||
protected stateMigrationService: StateMigrationService,
|
||||
private useSecureStorageForSecrets = true,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (await this.stateMigrationService.needsMigration()) {
|
||||
await this.stateMigrationService.migrate();
|
||||
}
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
// Clear all directory settings and configurations
|
||||
// but preserve version and environment settings
|
||||
await this.setDirectoryType(null);
|
||||
await this.setOrganizationId(null);
|
||||
await this.setSync(null);
|
||||
await this.setLdapConfiguration(null);
|
||||
await this.setGsuiteConfiguration(null);
|
||||
await this.setEntraConfiguration(null);
|
||||
await this.setOktaConfiguration(null);
|
||||
await this.setOneLoginConfiguration(null);
|
||||
await this.clearSyncSettings(true);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory Configuration Methods
|
||||
// ===================================================================
|
||||
|
||||
async getDirectory<T extends IConfiguration>(type: DirectoryType): Promise<T> {
|
||||
const config = await this.getConfiguration(type);
|
||||
if (config == null) {
|
||||
return config as T;
|
||||
}
|
||||
|
||||
if (this.useSecureStorageForSecrets) {
|
||||
// Create a copy to avoid modifying the cached config
|
||||
const configWithSecrets = Object.assign({}, config);
|
||||
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
(configWithSecrets as any).password = await this.getLdapSecret();
|
||||
break;
|
||||
case DirectoryType.EntraID:
|
||||
(configWithSecrets as any).key = await this.getEntraSecret();
|
||||
break;
|
||||
case DirectoryType.Okta:
|
||||
(configWithSecrets as any).token = await this.getOktaSecret();
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
(configWithSecrets as any).privateKey = await this.getGsuiteSecret();
|
||||
break;
|
||||
case DirectoryType.OneLogin:
|
||||
(configWithSecrets as any).clientSecret = await this.getOneLoginSecret();
|
||||
break;
|
||||
}
|
||||
|
||||
return configWithSecrets as T;
|
||||
}
|
||||
|
||||
return config as T;
|
||||
}
|
||||
|
||||
async setDirectory(
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
| GSuiteConfiguration
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
): Promise<any> {
|
||||
if (this.useSecureStorageForSecrets) {
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap: {
|
||||
const ldapConfig = config as LdapConfiguration;
|
||||
await this.setLdapSecret(ldapConfig.password);
|
||||
ldapConfig.password = StoredSecurely;
|
||||
await this.setLdapConfiguration(ldapConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.EntraID: {
|
||||
const entraConfig = config as EntraIdConfiguration;
|
||||
await this.setEntraSecret(entraConfig.key);
|
||||
entraConfig.key = StoredSecurely;
|
||||
await this.setEntraConfiguration(entraConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.Okta: {
|
||||
const oktaConfig = config as OktaConfiguration;
|
||||
await this.setOktaSecret(oktaConfig.token);
|
||||
oktaConfig.token = StoredSecurely;
|
||||
await this.setOktaConfiguration(oktaConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.GSuite: {
|
||||
const gsuiteConfig = config as GSuiteConfiguration;
|
||||
if (gsuiteConfig.privateKey == null) {
|
||||
await this.setGsuiteSecret(null);
|
||||
} else {
|
||||
const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n");
|
||||
await this.setGsuiteSecret(normalizedPrivateKey);
|
||||
gsuiteConfig.privateKey = StoredSecurely;
|
||||
}
|
||||
await this.setGsuiteConfiguration(gsuiteConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.OneLogin: {
|
||||
const oneLoginConfig = config as OneLoginConfiguration;
|
||||
await this.setOneLoginSecret(oneLoginConfig.clientSecret);
|
||||
oneLoginConfig.clientSecret = StoredSecurely;
|
||||
await this.setOneLoginConfiguration(oneLoginConfig);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getConfiguration(type: DirectoryType): Promise<IConfiguration> {
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
return await this.getLdapConfiguration();
|
||||
case DirectoryType.GSuite:
|
||||
return await this.getGsuiteConfiguration();
|
||||
case DirectoryType.EntraID:
|
||||
return await this.getEntraConfiguration();
|
||||
case DirectoryType.Okta:
|
||||
return await this.getOktaConfiguration();
|
||||
case DirectoryType.OneLogin:
|
||||
return await this.getOneLoginConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Secret Storage Methods (Secure Storage)
|
||||
// ===================================================================
|
||||
|
||||
private async getLdapSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.ldap);
|
||||
}
|
||||
|
||||
private async setLdapSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.ldap);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.ldap, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getGsuiteSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.gsuite);
|
||||
}
|
||||
|
||||
private async setGsuiteSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.gsuite);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.gsuite, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntraSecret(): Promise<string> {
|
||||
// Try new key first, fall back to old azure key for backwards compatibility
|
||||
const entraKey = await this.secureStorageService.get<string>(SecureStorageKeys.entra);
|
||||
if (entraKey != null) {
|
||||
return entraKey;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.azure);
|
||||
}
|
||||
|
||||
private async setEntraSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.entra);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.azure);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.entra, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOktaSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.okta);
|
||||
}
|
||||
|
||||
private async setOktaSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.okta);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.okta, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOneLoginSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.oneLogin);
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.oneLogin);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.oneLogin, value);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory-Specific Configuration Methods
|
||||
// ===================================================================
|
||||
|
||||
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
|
||||
return await this.storageService.get<LdapConfiguration>(StorageKeys.directory_ldap);
|
||||
}
|
||||
|
||||
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_ldap, value);
|
||||
}
|
||||
|
||||
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
|
||||
return await this.storageService.get<GSuiteConfiguration>(StorageKeys.directory_gsuite);
|
||||
}
|
||||
|
||||
async setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_gsuite, value);
|
||||
}
|
||||
|
||||
async getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration> {
|
||||
return await this.storageService.get<EntraIdConfiguration>(StorageKeys.directory_entra);
|
||||
}
|
||||
|
||||
async setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_entra, value);
|
||||
}
|
||||
|
||||
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
|
||||
return await this.storageService.get<OktaConfiguration>(StorageKeys.directory_okta);
|
||||
}
|
||||
|
||||
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_okta, value);
|
||||
}
|
||||
|
||||
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
|
||||
return await this.storageService.get<OneLoginConfiguration>(StorageKeys.directory_onelogin);
|
||||
}
|
||||
|
||||
async setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_onelogin, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory Settings Methods
|
||||
// ===================================================================
|
||||
|
||||
async getOrganizationId(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(StorageKeys.organizationId);
|
||||
}
|
||||
|
||||
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||
const currentId = await this.getOrganizationId();
|
||||
if (currentId !== value) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.organizationId, value);
|
||||
}
|
||||
|
||||
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
|
||||
return await this.storageService.get<SyncConfiguration>(StorageKeys.sync);
|
||||
}
|
||||
|
||||
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.sync, value);
|
||||
}
|
||||
|
||||
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
|
||||
return await this.storageService.get<DirectoryType>(StorageKeys.directoryType);
|
||||
}
|
||||
|
||||
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||
const currentType = await this.getDirectoryType();
|
||||
if (value !== currentType) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.directoryType, value);
|
||||
}
|
||||
|
||||
async getLastUserSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastUserSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastUserSync, value);
|
||||
}
|
||||
|
||||
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastGroupSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastGroupSync, value);
|
||||
}
|
||||
|
||||
async getLastSyncHash(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.lastSyncHash);
|
||||
}
|
||||
|
||||
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastSyncHash, value);
|
||||
}
|
||||
|
||||
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
|
||||
return await this.storageService.get<boolean>(StorageKeys.syncingDir);
|
||||
}
|
||||
|
||||
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.syncingDir, value);
|
||||
}
|
||||
|
||||
async getUserDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.userDelta);
|
||||
}
|
||||
|
||||
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.userDelta, value);
|
||||
}
|
||||
|
||||
async getGroupDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.groupDelta);
|
||||
}
|
||||
|
||||
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.groupDelta, value);
|
||||
}
|
||||
|
||||
async clearSyncSettings(hashToo = false): Promise<void> {
|
||||
await this.setUserDelta(null);
|
||||
await this.setGroupDelta(null);
|
||||
await this.setLastGroupSync(null);
|
||||
await this.setLastUserSync(null);
|
||||
if (hashToo) {
|
||||
await this.setLastSyncHash(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Environment URLs (inherited from base, simplified implementation)
|
||||
// ===================================================================
|
||||
|
||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
||||
return await this.storageService.get<EnvironmentUrls>("environmentUrls");
|
||||
}
|
||||
|
||||
async setEnvironmentUrls(value: EnvironmentUrls): Promise<void> {
|
||||
await this.storageService.save("environmentUrls", value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Additional State Methods
|
||||
// ===================================================================
|
||||
|
||||
async getLocale(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("locale");
|
||||
}
|
||||
|
||||
async setLocale(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("locale", value);
|
||||
}
|
||||
|
||||
async getInstalledVersion(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("installedVersion");
|
||||
}
|
||||
|
||||
async setInstalledVersion(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("installedVersion", value);
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,13 @@ import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import {
|
||||
SecureStorageKeysLegacy as SecureStorageKeys,
|
||||
StoredSecurely,
|
||||
TempKeys as keys,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
const SecureStorageKeys = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "azureKey",
|
||||
entra: "entraKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
const keys = {
|
||||
tempAccountSettings: "tempAccountSettings",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
const StoredSecurely = "[STORED SECURELY]";
|
||||
|
||||
export class StateService
|
||||
extends BaseStateService<GlobalState, Account>
|
||||
implements StateServiceAbstraction
|
||||
@@ -8,48 +8,14 @@ import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import {
|
||||
MigrationClientKeys as ClientKeys,
|
||||
MigrationKeys as Keys,
|
||||
MigrationStateKeys as StateKeys,
|
||||
SecureStorageKeysMigration as SecureStorageKeys,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
const SecureStorageKeys: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
const Keys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
const StateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
const ClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
export class StateMigrationService extends BaseStateMigrationService {
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
@@ -61,6 +27,13 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
break;
|
||||
case StateVersion.Three:
|
||||
await this.migrateStateFrom3To4();
|
||||
break;
|
||||
case StateVersion.Four:
|
||||
await this.migrateStateFrom4To5();
|
||||
break;
|
||||
}
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
@@ -198,4 +171,124 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
globals.stateVersion = StateVersion.Three;
|
||||
await this.set(StateKeys.global, globals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from State v4 (Account-based hierarchy) to v5 (flat key-value structure)
|
||||
*
|
||||
* This is a clean break from the Account-based structure. Data is extracted from
|
||||
* the account and saved into flat keys for simpler access.
|
||||
*
|
||||
* Old structure: authenticatedAccounts -> userId -> account.directorySettings/directoryConfigurations
|
||||
* New structure: flat keys like "directoryType", "organizationId", "directory_ldap", etc.
|
||||
*
|
||||
* Secrets migrate from: {userId}_{secretKey} -> secret_{secretKey}
|
||||
*/
|
||||
protected async migrateStateFrom4To5(useSecureStorageForSecrets = true): Promise<void> {
|
||||
// Get the authenticated user IDs from v3 structure
|
||||
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
|
||||
|
||||
if (
|
||||
!authenticatedUserIds ||
|
||||
!Array.isArray(authenticatedUserIds) ||
|
||||
authenticatedUserIds.length === 0
|
||||
) {
|
||||
// No accounts to migrate, just update version
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
return;
|
||||
}
|
||||
|
||||
// DC is single-user, so we take the first (and likely only) account
|
||||
const userId = authenticatedUserIds[0];
|
||||
const account = await this.get<Account>(userId);
|
||||
|
||||
if (!account) {
|
||||
// No account data found, just update version
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate directory configurations to flat structure
|
||||
if (account.directoryConfigurations) {
|
||||
if (account.directoryConfigurations.ldap) {
|
||||
await this.set("directory_ldap", account.directoryConfigurations.ldap);
|
||||
}
|
||||
if (account.directoryConfigurations.gsuite) {
|
||||
await this.set("directory_gsuite", account.directoryConfigurations.gsuite);
|
||||
}
|
||||
if (account.directoryConfigurations.entra) {
|
||||
await this.set("directory_entra", account.directoryConfigurations.entra);
|
||||
} else if (account.directoryConfigurations.azure) {
|
||||
// Backwards compatibility: migrate azure to entra
|
||||
await this.set("directory_entra", account.directoryConfigurations.azure);
|
||||
}
|
||||
if (account.directoryConfigurations.okta) {
|
||||
await this.set("directory_okta", account.directoryConfigurations.okta);
|
||||
}
|
||||
if (account.directoryConfigurations.oneLogin) {
|
||||
await this.set("directory_onelogin", account.directoryConfigurations.oneLogin);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate directory settings to flat structure
|
||||
if (account.directorySettings) {
|
||||
if (account.directorySettings.organizationId) {
|
||||
await this.set("organizationId", account.directorySettings.organizationId);
|
||||
}
|
||||
if (account.directorySettings.directoryType != null) {
|
||||
await this.set("directoryType", account.directorySettings.directoryType);
|
||||
}
|
||||
if (account.directorySettings.sync) {
|
||||
await this.set("sync", account.directorySettings.sync);
|
||||
}
|
||||
if (account.directorySettings.lastUserSync) {
|
||||
await this.set("lastUserSync", account.directorySettings.lastUserSync);
|
||||
}
|
||||
if (account.directorySettings.lastGroupSync) {
|
||||
await this.set("lastGroupSync", account.directorySettings.lastGroupSync);
|
||||
}
|
||||
if (account.directorySettings.lastSyncHash) {
|
||||
await this.set("lastSyncHash", account.directorySettings.lastSyncHash);
|
||||
}
|
||||
if (account.directorySettings.userDelta) {
|
||||
await this.set("userDelta", account.directorySettings.userDelta);
|
||||
}
|
||||
if (account.directorySettings.groupDelta) {
|
||||
await this.set("groupDelta", account.directorySettings.groupDelta);
|
||||
}
|
||||
if (account.directorySettings.syncingDir != null) {
|
||||
await this.set("syncingDir", account.directorySettings.syncingDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate secrets from {userId}_* to secret_* pattern
|
||||
if (useSecureStorageForSecrets) {
|
||||
const oldSecretKeys = [
|
||||
{ old: `${userId}_${SecureStorageKeys.ldap}`, new: "secret_ldap" },
|
||||
{ old: `${userId}_${SecureStorageKeys.gsuite}`, new: "secret_gsuite" },
|
||||
{ old: `${userId}_${SecureStorageKeys.azure}`, new: "secret_azure" },
|
||||
{ old: `${userId}_${SecureStorageKeys.entra}`, new: "secret_entra" },
|
||||
{ old: `${userId}_${SecureStorageKeys.okta}`, new: "secret_okta" },
|
||||
{ old: `${userId}_${SecureStorageKeys.oneLogin}`, new: "secret_onelogin" },
|
||||
];
|
||||
|
||||
for (const { old: oldKey, new: newKey } of oldSecretKeys) {
|
||||
if (await this.secureStorageService.has(oldKey)) {
|
||||
const value = await this.secureStorageService.get(oldKey);
|
||||
if (value) {
|
||||
await this.secureStorageService.save(newKey, value);
|
||||
}
|
||||
// @TODO Keep old key for now - will remove in future release
|
||||
// await this.secureStorageService.remove(oldKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { DirectoryType } from "../enums/directoryType";
|
||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
@@ -116,6 +116,7 @@ describe("SyncService", () => {
|
||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||
|
||||
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
||||
// eslint-disable-next-line no-import-assign
|
||||
constants.batchSize = 4;
|
||||
|
||||
const syncResult = await syncService.sync(false, false);
|
||||
@@ -130,6 +131,7 @@ describe("SyncService", () => {
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(7);
|
||||
|
||||
// @ts-expect-error Reset batch size to original state.
|
||||
// eslint-disable-next-line no-import-assign
|
||||
constants.batchSize = originalBatchSize;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
||||
import { I18nService } from "./i18n.service";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
@@ -97,6 +97,7 @@ describe("SyncService", () => {
|
||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||
|
||||
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
||||
// eslint-disable-next-line no-import-assign
|
||||
constants.batchSize = 4;
|
||||
|
||||
const mockRequests = new Array(6).fill({
|
||||
@@ -119,6 +120,7 @@ describe("SyncService", () => {
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
|
||||
|
||||
// @ts-expect-error Reset batch size back to original value.
|
||||
// eslint-disable-next-line no-import-assign
|
||||
constants.batchSize = originalBatchSize;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { webcrypto } from "crypto";
|
||||
import { TextEncoder, TextDecoder } from "util";
|
||||
|
||||
import "jest-preset-angular/setup-jest";
|
||||
|
||||
Object.assign(globalThis, { TextEncoder, TextDecoder });
|
||||
Object.defineProperty(window, "CSS", { value: null });
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"compilerOptions": {
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"noImplicitAny": true,
|
||||
"target": "ES2016",
|
||||
"module": "ES2020",
|
||||
|
||||
Reference in New Issue
Block a user