mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-16 08:14:01 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5900db10eb | ||
|
|
c9bd853032 | ||
|
|
976fcad098 | ||
|
|
5d8193c348 | ||
|
|
cc09b26818 | ||
|
|
b0247caa4c | ||
|
|
38316abeae | ||
|
|
0991dfa6bd | ||
|
|
88029663e9 | ||
|
|
3f5cb10db6 | ||
|
|
1c927722f0 | ||
|
|
c04354ba1c | ||
|
|
3335659c8d | ||
|
|
f0282b33f0 | ||
|
|
f69d461463 | ||
|
|
bcf16562fe | ||
|
|
d7412410f2 | ||
|
|
d5907477df | ||
|
|
9acfaff361 | ||
|
|
737274da59 | ||
|
|
7a0e0e6915 | ||
|
|
25847b9c83 | ||
|
|
07dd7bb93e | ||
|
|
bc9936bd92 | ||
|
|
e4428e3387 | ||
|
|
30ef0ce140 | ||
|
|
38c461ebe0 | ||
|
|
ead1776f90 | ||
|
|
0e944b2442 | ||
|
|
3bbd503308 | ||
|
|
a7b13b168e | ||
|
|
1cf8ae1bdf | ||
|
|
7da3c8e252 | ||
|
|
2edf3fb68d | ||
|
|
1e698ba81b | ||
|
|
e9034ea6fe |
29
README.md
29
README.md
@@ -18,7 +18,32 @@ The application is written using Electron with Angular and installs on Windows,
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
# Build/Run
|
## Command-line Interface
|
||||||
|
|
||||||
|
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
|
||||||
|
|
||||||
|
## CLI Documentation
|
||||||
|
|
||||||
|
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bwdc --help
|
||||||
|
```
|
||||||
|
|
||||||
|
This option will list all available commands that you can use with the Directory Connector CLI.
|
||||||
|
|
||||||
|
Additionally, you can run the `--help` option on a specific command to learn more about it:
|
||||||
|
|
||||||
|
```
|
||||||
|
bwdc test --help
|
||||||
|
bwdc config --help
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed Documentation**
|
||||||
|
|
||||||
|
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
|
||||||
|
|
||||||
|
## Build/Run
|
||||||
|
|
||||||
**Requirements**
|
**Requirements**
|
||||||
|
|
||||||
@@ -48,7 +73,7 @@ You can then run commands from the `./build-cli` folder:
|
|||||||
node ./build-cli/bwdc.js --help
|
node ./build-cli/bwdc.js --help
|
||||||
```
|
```
|
||||||
|
|
||||||
# Contribute
|
## Contribute
|
||||||
|
|
||||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
||||||
|
|
||||||
|
|||||||
18
appveyor.yml
18
appveyor.yml
@@ -24,21 +24,23 @@ init:
|
|||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
Install-Product node 10
|
Install-Product node 10
|
||||||
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
|
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
|
||||||
if(Test-Path -Path $env:WIN_PKG) {
|
|
||||||
$env:VER_INFO = "true"
|
|
||||||
}
|
}
|
||||||
|
if($env:APPVEYOR_REPO_TAG -eq "true") {
|
||||||
|
$env:RELEASE_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
|
||||||
}
|
}
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ps: |
|
- ps: |
|
||||||
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
|
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
|
||||||
$env:RELEASE_NAME = "Version ${tagName}"
|
|
||||||
$env:PROD_DEPLOY = "false"
|
$env:PROD_DEPLOY = "false"
|
||||||
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
|
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
|
||||||
$env:PROD_DEPLOY = "true"
|
$env:PROD_DEPLOY = "true"
|
||||||
echo "This is a production deployment."
|
echo "This is a production deployment."
|
||||||
}
|
}
|
||||||
if($isWindows) {
|
if($isWindows) {
|
||||||
|
if(Test-Path -Path $env:WIN_PKG) {
|
||||||
|
$env:VER_INFO = "true"
|
||||||
|
}
|
||||||
choco install reshack --no-progress
|
choco install reshack --no-progress
|
||||||
choco install cloc --no-progress
|
choco install cloc --no-progress
|
||||||
choco install checksum --no-progress
|
choco install checksum --no-progress
|
||||||
@@ -136,17 +138,17 @@ for:
|
|||||||
only:
|
only:
|
||||||
- image: Visual Studio 2017
|
- image: Visual Studio 2017
|
||||||
cache:
|
cache:
|
||||||
- '%LOCALAPPDATA%\electron -> appveyor.yml'
|
- '%LOCALAPPDATA%\electron'
|
||||||
- '%LOCALAPPDATA%\electron-builder -> appveyor.yml'
|
- '%LOCALAPPDATA%\electron-builder'
|
||||||
- 'C:\Users\appveyor\.pkg-cache\ -> package.json'
|
- 'C:\Users\appveyor\.pkg-cache\'
|
||||||
|
|
||||||
-
|
-
|
||||||
matrix:
|
matrix:
|
||||||
only:
|
only:
|
||||||
- image: Ubuntu1804
|
- image: Ubuntu1804
|
||||||
cache:
|
cache:
|
||||||
- '/home/appveyor/.cache/electron -> appveyor.yml'
|
- '/home/appveyor/.cache/electron'
|
||||||
- '/home/appveyor/.cache/electron-builder -> appveyor.yml'
|
- '/home/appveyor/.cache/electron-builder'
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
tag: $(APPVEYOR_REPO_TAG_NAME)
|
tag: $(APPVEYOR_REPO_TAG_NAME)
|
||||||
|
|||||||
2
jslib
2
jslib
Submodule jslib updated: 50e6f24679...741e060d99
1268
package-lock.json
generated
1268
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@
|
|||||||
"assets": "./build-cli/**/*"
|
"assets": "./build-cli/**/*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^7.2.1",
|
"@angular/compiler-cli": "^7.2.11",
|
||||||
"@microsoft/microsoft-graph-types": "^1.4.0",
|
"@microsoft/microsoft-graph-types": "^1.4.0",
|
||||||
"@ngtools/webpack": "^7.2.2",
|
"@ngtools/webpack": "^7.2.2",
|
||||||
"@types/commander": "^2.12.2",
|
"@types/commander": "^2.12.2",
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"node-abi": "^2.5.1",
|
"node-abi": "^2.5.1",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.11.0",
|
||||||
"pkg": "4.3.4",
|
"pkg": "4.3.4",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
"angular2-toaster": "6.1.0",
|
"angular2-toaster": "6.1.0",
|
||||||
"angulartics2": "6.3.0",
|
"angulartics2": "6.3.0",
|
||||||
"big-integer": "1.6.36",
|
"big-integer": "1.6.36",
|
||||||
"bootstrap": "4.1.3",
|
"bootstrap": "4.3.1",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
"commander": "2.18.0",
|
"commander": "2.18.0",
|
||||||
"core-js": "2.6.2",
|
"core-js": "2.6.2",
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
"googleapis": "33.0.0",
|
"googleapis": "33.0.0",
|
||||||
"inquirer": "6.2.0",
|
"inquirer": "6.2.0",
|
||||||
"keytar": "4.4.1",
|
"keytar": "4.4.1",
|
||||||
"ldapjs": "1.0.2",
|
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
"lunr": "2.3.3",
|
"lunr": "2.3.3",
|
||||||
"node-fetch": "2.2.0",
|
"node-fetch": "2.2.0",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { MoreComponent } from './tabs/more.component';
|
|||||||
import { SettingsComponent } from './tabs/settings.component';
|
import { SettingsComponent } from './tabs/settings.component';
|
||||||
import { TabsComponent } from './tabs/tabs.component';
|
import { TabsComponent } from './tabs/tabs.component';
|
||||||
|
|
||||||
|
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
|
||||||
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
|
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
|
||||||
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
|
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
|
||||||
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
|
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
|
||||||
@@ -53,6 +54,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||||||
ToasterModule.forRoot(),
|
ToasterModule.forRoot(),
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
A11yTitleDirective,
|
||||||
ApiActionDirective,
|
ApiActionDirective,
|
||||||
AppComponent,
|
AppComponent,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
|
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
|
||||||
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
|
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
|
||||||
</p>
|
</p>
|
||||||
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading">
|
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary"
|
||||||
|
[disabled]="startBtn.loading">
|
||||||
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
|
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
|
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
|
||||||
{{'startSync' | i18n}}
|
{{'startSync' | i18n}}
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
<i class="fa fa-stop fa-fw"></i>
|
<i class="fa fa-stop fa-fw"></i>
|
||||||
{{'stopSync' | i18n}}
|
{{'stopSync' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading">
|
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary"
|
||||||
|
[disabled]="syncBtn.loading">
|
||||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
|
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
|
||||||
{{'syncNow' | i18n}}
|
{{'syncNow' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
@@ -33,7 +35,8 @@
|
|||||||
<h3 class="card-header">{{'testing' | i18n}}</h3>
|
<h3 class="card-header">{{'testing' | i18n}}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>{{'testingDesc' | i18n}}</p>
|
<p>{{'testingDesc' | i18n}}</p>
|
||||||
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading">
|
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary"
|
||||||
|
[disabled]="simBtn.loading">
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
|
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
|
||||||
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
|
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
|
||||||
{{'testNow' | i18n}}
|
{{'testNow' | i18n}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { StateService } from 'jslib/abstractions/state.service';
|
|||||||
import { SyncService } from '../../services/sync.service';
|
import { SyncService } from '../../services/sync.service';
|
||||||
|
|
||||||
import { GroupEntry } from '../../models/groupEntry';
|
import { GroupEntry } from '../../models/groupEntry';
|
||||||
|
import { SimResult } from '../../models/simResult';
|
||||||
import { UserEntry } from '../../models/userEntry';
|
import { UserEntry } from '../../models/userEntry';
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
import { ConfigurationService } from '../../services/configuration.service';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
simEnabledUsers: UserEntry[] = [];
|
simEnabledUsers: UserEntry[] = [];
|
||||||
simDisabledUsers: UserEntry[] = [];
|
simDisabledUsers: UserEntry[] = [];
|
||||||
simDeletedUsers: UserEntry[] = [];
|
simDeletedUsers: UserEntry[] = [];
|
||||||
simPromise: Promise<any>;
|
simPromise: Promise<SimResult>;
|
||||||
simSinceLast: boolean = false;
|
simSinceLast: boolean = false;
|
||||||
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
||||||
startPromise: Promise<any>;
|
startPromise: Promise<any>;
|
||||||
@@ -101,9 +102,9 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.simDisabledUsers = [];
|
this.simDisabledUsers = [];
|
||||||
this.simDeletedUsers = [];
|
this.simDeletedUsers = [];
|
||||||
|
|
||||||
this.simPromise = new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
try {
|
||||||
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
|
this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
|
||||||
|
const result = await this.simPromise;
|
||||||
this.simGroups = result.groups;
|
this.simGroups = result.groups;
|
||||||
this.simUsers = result.users;
|
this.simUsers = result.users;
|
||||||
this.simEnabledUsers = result.enabledUsers;
|
this.simEnabledUsers = result.enabledUsers;
|
||||||
@@ -112,11 +113,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.simGroups = null;
|
this.simGroups = null;
|
||||||
this.simUsers = null;
|
this.simUsers = null;
|
||||||
reject(e);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateLastSync() {
|
private async updateLastSync() {
|
||||||
|
|||||||
@@ -190,6 +190,13 @@
|
|||||||
<label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
|
<label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="overwriteExisting"
|
||||||
|
[(ngModel)]="sync.overwriteExisting" name="OverwriteExisting">
|
||||||
|
<label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap">
|
<div [hidden]="directory != directoryType.Ldap">
|
||||||
<div [hidden]="ldap.ad">
|
<div [hidden]="ldap.ad">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
18
src/bwdc.ts
18
src/bwdc.ts
@@ -31,6 +31,7 @@ import { Program } from './program';
|
|||||||
const packageJson = require('./package.json');
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
export class Main {
|
export class Main {
|
||||||
|
dataFilePath: string;
|
||||||
logService: ConsoleLogService;
|
logService: ConsoleLogService;
|
||||||
messagingService: NoopMessagingService;
|
messagingService: NoopMessagingService;
|
||||||
storageService: LowdbStorageService;
|
storageService: LowdbStorageService;
|
||||||
@@ -53,21 +54,20 @@ export class Main {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const applicationName = 'Bitwarden Directory Connector';
|
const applicationName = 'Bitwarden Directory Connector';
|
||||||
let p = null;
|
|
||||||
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
||||||
p = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
||||||
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
|
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
|
||||||
p = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
|
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
|
||||||
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
|
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
|
||||||
p = path.join(__dirname, 'bitwarden-connector-appdata');
|
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === 'darwin') {
|
||||||
p = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
|
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
p = path.join(process.env.APPDATA, applicationName);
|
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
|
||||||
} else if (process.env.XDG_CONFIG_HOME) {
|
} else if (process.env.XDG_CONFIG_HOME) {
|
||||||
p = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
||||||
} else {
|
} else {
|
||||||
p = path.join(process.env.HOME, '.config/' + applicationName);
|
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.i18nService = new I18nService('en', './locales');
|
this.i18nService = new I18nService('en', './locales');
|
||||||
@@ -75,7 +75,7 @@ export class Main {
|
|||||||
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
|
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
|
||||||
(level) => process.env.BWCLI_DEBUG !== 'true' && level <= LogLevelType.Info);
|
(level) => process.env.BWCLI_DEBUG !== 'true' && level <= LogLevelType.Info);
|
||||||
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
this.storageService = new LowdbStorageService(null, p, true);
|
this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
|
||||||
this.secureStorageService = new KeytarSecureStorageService(applicationName);
|
this.secureStorageService = new KeytarSecureStorageService(applicationName);
|
||||||
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
|
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
|
||||||
this.cryptoFunctionService);
|
this.cryptoFunctionService);
|
||||||
|
|||||||
29
src/commands/lastSync.command.ts
Normal file
29
src/commands/lastSync.command.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as program from 'commander';
|
||||||
|
|
||||||
|
import { ConfigurationService } from '../services/configuration.service';
|
||||||
|
|
||||||
|
import { Response } from 'jslib/cli/models/response';
|
||||||
|
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||||
|
|
||||||
|
export class LastSyncCommand {
|
||||||
|
constructor(private configurationService: ConfigurationService) { }
|
||||||
|
|
||||||
|
async run(object: string, cmd: program.Command): Promise<Response> {
|
||||||
|
try {
|
||||||
|
switch (object.toLowerCase()) {
|
||||||
|
case 'groups':
|
||||||
|
const groupsDate = await this.configurationService.getLastGroupSyncDate();
|
||||||
|
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
|
||||||
|
return Response.success(groupsRes);
|
||||||
|
case 'users':
|
||||||
|
const usersDate = await this.configurationService.getLastUserSyncDate();
|
||||||
|
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
|
||||||
|
return Response.success(usersRes);
|
||||||
|
default:
|
||||||
|
return Response.badRequest('Unknown object.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"message": "Self-hosted Environment"
|
"message": "Self-hosted Environment"
|
||||||
},
|
},
|
||||||
"selfHostedEnvironmentFooter": {
|
"selfHostedEnvironmentFooter": {
|
||||||
"message": "Specify the base URL of your on-premise hosted Bitwarden installation."
|
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
|
||||||
},
|
},
|
||||||
"customEnvironment": {
|
"customEnvironment": {
|
||||||
"message": "Custom Environment"
|
"message": "Custom Environment"
|
||||||
@@ -577,5 +577,8 @@
|
|||||||
"example": "server"
|
"example": "server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"overwriteExisting": {
|
||||||
|
"message": "Overwrite existing organization users based on current sync settings."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class Main {
|
|||||||
this.i18nService = new I18nService('en', './locales/');
|
this.i18nService = new I18nService('en', './locales/');
|
||||||
this.storageService = new ElectronStorageService(app.getPath('userData'));
|
this.storageService = new ElectronStorageService(app.getPath('userData'));
|
||||||
|
|
||||||
this.windowMain = new WindowMain(this.storageService, 800, 600);
|
this.windowMain = new WindowMain(this.storageService, false, 800, 600);
|
||||||
this.menuMain = new MenuMain(this);
|
this.menuMain = new MenuMain(this);
|
||||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
|
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
|
||||||
this.messagingService.send('checkingForUpdate');
|
this.messagingService.send('checkingForUpdate');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export class SyncConfiguration {
|
|||||||
userFilter: string;
|
userFilter: string;
|
||||||
groupFilter: string;
|
groupFilter: string;
|
||||||
removeDisabled = false;
|
removeDisabled = false;
|
||||||
|
overwriteExisting = false;
|
||||||
// Ldap properties
|
// Ldap properties
|
||||||
groupObjectClass: string;
|
groupObjectClass: string;
|
||||||
userObjectClass: string;
|
userObjectClass: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "bitwarden-directory-connector",
|
"name": "bitwarden-directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2.5.0",
|
"version": "2.6.0",
|
||||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||||
"homepage": "https://bitwarden.com",
|
"homepage": "https://bitwarden.com",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import * as chk from 'chalk';
|
import * as chk from 'chalk';
|
||||||
import * as program from 'commander';
|
import * as program from 'commander';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
import { Main } from './bwdc';
|
import { Main } from './bwdc';
|
||||||
|
|
||||||
import { ClearCacheCommand } from './commands/clearCache.command';
|
import { ClearCacheCommand } from './commands/clearCache.command';
|
||||||
import { ConfigCommand } from './commands/config.command';
|
import { ConfigCommand } from './commands/config.command';
|
||||||
|
import { LastSyncCommand } from './commands/lastSync.command';
|
||||||
import { SyncCommand } from './commands/sync.command';
|
import { SyncCommand } from './commands/sync.command';
|
||||||
import { TestCommand } from './commands/test.command';
|
import { TestCommand } from './commands/test.command';
|
||||||
|
|
||||||
@@ -14,6 +16,9 @@ import { UpdateCommand } from 'jslib/cli/commands/update.command';
|
|||||||
|
|
||||||
import { BaseProgram } from 'jslib/cli/baseProgram';
|
import { BaseProgram } from 'jslib/cli/baseProgram';
|
||||||
|
|
||||||
|
import { Response } from 'jslib/cli/models/response';
|
||||||
|
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
|
||||||
|
|
||||||
const chalk = chk.default;
|
const chalk = chk.default;
|
||||||
const writeLn = (s: string, finalLine: boolean = false) => {
|
const writeLn = (s: string, finalLine: boolean = false) => {
|
||||||
if (finalLine && process.platform === 'win32') {
|
if (finalLine && process.platform === 'win32') {
|
||||||
@@ -61,8 +66,10 @@ export class Program extends BaseProgram {
|
|||||||
program.on('--help', () => {
|
program.on('--help', () => {
|
||||||
writeLn('\n Examples:');
|
writeLn('\n Examples:');
|
||||||
writeLn('');
|
writeLn('');
|
||||||
|
writeLn(' bwdc login');
|
||||||
writeLn(' bwdc test');
|
writeLn(' bwdc test');
|
||||||
writeLn(' bwdc sync');
|
writeLn(' bwdc sync');
|
||||||
|
writeLn(' bwdc last-sync');
|
||||||
writeLn(' bwdc config server https://bw.company.com');
|
writeLn(' bwdc config server https://bw.company.com');
|
||||||
writeLn(' bwdc update');
|
writeLn(' bwdc update');
|
||||||
writeLn('', true);
|
writeLn('', true);
|
||||||
@@ -143,6 +150,27 @@ export class Program extends BaseProgram {
|
|||||||
this.processResponse(response);
|
this.processResponse(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('last-sync <object>')
|
||||||
|
.description('Get the last successful sync date.')
|
||||||
|
.on('--help', () => {
|
||||||
|
writeLn('\n Notes:');
|
||||||
|
writeLn('');
|
||||||
|
writeLn(' Returns empty response if no sync has been performed for the given object.');
|
||||||
|
writeLn('');
|
||||||
|
writeLn(' Examples:');
|
||||||
|
writeLn('');
|
||||||
|
writeLn(' bwdc last-sync groups');
|
||||||
|
writeLn(' bwdc last-sync users');
|
||||||
|
writeLn('', true);
|
||||||
|
})
|
||||||
|
.action(async (object: string, cmd: program.Command) => {
|
||||||
|
await this.exitIfNotAuthed();
|
||||||
|
const command = new LastSyncCommand(this.main.configurationService);
|
||||||
|
const response = await command.run(object, cmd);
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('config <setting> <value>')
|
.command('config <setting> <value>')
|
||||||
.description('Configure settings.')
|
.description('Configure settings.')
|
||||||
@@ -174,6 +202,20 @@ export class Program extends BaseProgram {
|
|||||||
this.processResponse(response);
|
this.processResponse(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('data-file')
|
||||||
|
.description('Path to data.json database file.')
|
||||||
|
.on('--help', () => {
|
||||||
|
writeLn('\n Examples:');
|
||||||
|
writeLn('');
|
||||||
|
writeLn(' bwdc data-file');
|
||||||
|
writeLn('', true);
|
||||||
|
})
|
||||||
|
.action(() => {
|
||||||
|
this.processResponse(
|
||||||
|
Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json'))));
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('clear-cache')
|
.command('clear-cache')
|
||||||
.description('Clear the sync cache.')
|
.description('Clear the sync cache.')
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
|||||||
import { LogService } from 'jslib/abstractions/log.service';
|
import { LogService } from 'jslib/abstractions/log.service';
|
||||||
|
|
||||||
const NextLink = '@odata.nextLink';
|
const NextLink = '@odata.nextLink';
|
||||||
|
const DeltaLink = '@odata.deltaLink';
|
||||||
const ObjectType = '@odata.type';
|
const ObjectType = '@odata.type';
|
||||||
|
|
||||||
enum UserSetType {
|
enum UserSetType {
|
||||||
@@ -59,7 +60,9 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
|||||||
|
|
||||||
let users: UserEntry[];
|
let users: UserEntry[];
|
||||||
if (this.syncConfig.users) {
|
if (this.syncConfig.users) {
|
||||||
users = await this.getUsers();
|
users = await this.getCurrentUsers();
|
||||||
|
const deletedUsers = await this.getDeletedUsers(force, !test);
|
||||||
|
users = users.concat(deletedUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
let groups: GroupEntry[];
|
||||||
@@ -72,7 +75,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
|||||||
return [groups, users];
|
return [groups, users];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsers(): Promise<UserEntry[]> {
|
private async getCurrentUsers(): Promise<UserEntry[]> {
|
||||||
const entryIds = new Set<string>();
|
const entryIds = new Set<string>();
|
||||||
const entries: UserEntry[] = [];
|
const entries: UserEntry[] = [];
|
||||||
const userReq = this.client.api('/users');
|
const userReq = this.client.api('/users');
|
||||||
@@ -111,6 +114,61 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
||||||
|
const entryIds = new Set<string>();
|
||||||
|
const entries: UserEntry[] = [];
|
||||||
|
|
||||||
|
let res: any = null;
|
||||||
|
const token = await this.configurationService.getUserDeltaToken();
|
||||||
|
if (!force && token != null) {
|
||||||
|
try {
|
||||||
|
const deltaReq = this.client.api(token);
|
||||||
|
res = await deltaReq.get();
|
||||||
|
} catch {
|
||||||
|
res = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == null) {
|
||||||
|
const userReq = this.client.api('/users/delta');
|
||||||
|
res = await userReq.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||||
|
while (true) {
|
||||||
|
const users: graphType.User[] = res.value;
|
||||||
|
if (users != null) {
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.id == null || entryIds.has(user.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = this.buildUser(user);
|
||||||
|
if (!entry.disabled && !entry.deleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (await this.filterOutUserResult(setFilter, entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(entry);
|
||||||
|
entryIds.add(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res[NextLink] == null) {
|
||||||
|
if (res[DeltaLink] != null && saveDelta) {
|
||||||
|
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const nextReq = this.client.api(res[NextLink]);
|
||||||
|
res = await nextReq.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
|
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
|
||||||
if (filter == null || filter === '') {
|
if (filter == null || filter === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -67,21 +67,23 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
private async getUsers(): Promise<UserEntry[]> {
|
private async getUsers(): Promise<UserEntry[]> {
|
||||||
const entries: UserEntry[] = [];
|
const entries: UserEntry[] = [];
|
||||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
this.logService.info('Querying users.');
|
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||||
let p = Object.assign({ query: query }, this.authParams);
|
while (true) {
|
||||||
|
this.logService.info('Querying users - nextPageToken:' + nextPageToken);
|
||||||
|
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
||||||
const res = await this.service.users.list(p);
|
const res = await this.service.users.list(p);
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('User list API failed: ' + res.statusText);
|
throw new Error('User list API failed: ' + res.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
nextPageToken = res.data.nextPageToken;
|
||||||
if (res.data.users != null) {
|
if (res.data.users != null) {
|
||||||
for (const user of res.data.users) {
|
for (const user of res.data.users) {
|
||||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = this.buildUser(user, false);
|
const entry = this.buildUser(user, false);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
@@ -89,19 +91,26 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logService.info('Querying deleted users.');
|
if (nextPageToken == null) {
|
||||||
p = Object.assign({ showDeleted: true, query: query }, this.authParams);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = null;
|
||||||
|
while (true) {
|
||||||
|
this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
|
||||||
|
const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
|
||||||
const delRes = await this.service.users.list(p);
|
const delRes = await this.service.users.list(p);
|
||||||
if (delRes.status !== 200) {
|
if (delRes.status !== 200) {
|
||||||
throw new Error('Deleted user list API failed: ' + delRes.statusText);
|
throw new Error('Deleted user list API failed: ' + delRes.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPageToken = delRes.data.nextPageToken;
|
||||||
if (delRes.data.users != null) {
|
if (delRes.data.users != null) {
|
||||||
for (const user of delRes.data.users) {
|
for (const user of delRes.data.users) {
|
||||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = this.buildUser(user, true);
|
const entry = this.buildUser(user, true);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
@@ -109,6 +118,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +142,17 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
|
|
||||||
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||||
const entries: GroupEntry[] = [];
|
const entries: GroupEntry[] = [];
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
this.logService.info('Querying groups.');
|
while (true) {
|
||||||
const res = await this.service.groups.list(this.authParams);
|
this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
|
||||||
|
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
|
||||||
|
const res = await this.service.groups.list(p);
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Group list API failed: ' + res.statusText);
|
throw new Error('Group list API failed: ' + res.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPageToken = res.data.nextPageToken;
|
||||||
if (res.data.groups != null) {
|
if (res.data.groups != null) {
|
||||||
for (const group of res.data.groups) {
|
for (const group of res.data.groups) {
|
||||||
if (!this.filterOutResult(setFilter, group.name)) {
|
if (!this.filterOutResult(setFilter, group.name)) {
|
||||||
@@ -143,22 +162,31 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildGroup(group: admin_directory_v1.Schema$Group) {
|
private async buildGroup(group: admin_directory_v1.Schema$Group) {
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
const entry = new GroupEntry();
|
const entry = new GroupEntry();
|
||||||
entry.referenceId = group.id;
|
entry.referenceId = group.id;
|
||||||
entry.externalId = group.id;
|
entry.externalId = group.id;
|
||||||
entry.name = group.name;
|
entry.name = group.name;
|
||||||
|
|
||||||
const p = Object.assign({ groupKey: group.id }, this.authParams);
|
while (true) {
|
||||||
|
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
||||||
const memRes = await this.service.members.list(p);
|
const memRes = await this.service.members.list(p);
|
||||||
if (memRes.status !== 200) {
|
if (memRes.status !== 200) {
|
||||||
this.logService.warning('Group member list API failed: ' + memRes.statusText);
|
this.logService.warning('Group member list API failed: ' + memRes.statusText);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPageToken = memRes.data.nextPageToken;
|
||||||
if (memRes.data.members != null) {
|
if (memRes.data.members != null) {
|
||||||
for (const member of memRes.data.members) {
|
for (const member of memRes.data.members) {
|
||||||
if (member.type == null) {
|
if (member.type == null) {
|
||||||
@@ -180,6 +208,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class SyncService {
|
|||||||
|
|
||||||
this.messagingService.send('dirSyncStarted');
|
this.messagingService.send('dirSyncStarted');
|
||||||
try {
|
try {
|
||||||
const entries = await directoryService.getEntries(force, test);
|
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
|
||||||
let groups = entries[0];
|
let groups = entries[0];
|
||||||
let users = entries[1];
|
let users = entries[1];
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export class SyncService {
|
|||||||
return [groups, users];
|
return [groups, users];
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = this.buildRequest(groups, users, syncConfig.removeDisabled);
|
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting);
|
||||||
const reqJson = JSON.stringify(req);
|
const reqJson = JSON.stringify(req);
|
||||||
|
|
||||||
let hash: string = null;
|
let hash: string = null;
|
||||||
@@ -128,8 +128,10 @@ export class SyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean): ImportDirectoryRequest {
|
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean,
|
||||||
|
overwriteExisting: boolean): ImportDirectoryRequest {
|
||||||
const model = new ImportDirectoryRequest();
|
const model = new ImportDirectoryRequest();
|
||||||
|
model.overwriteExisting = overwriteExisting;
|
||||||
|
|
||||||
if (groups != null) {
|
if (groups != null) {
|
||||||
for (const g of groups) {
|
for (const g of groups) {
|
||||||
|
|||||||
@@ -49,10 +49,11 @@
|
|||||||
"jslib/src/abstractions/passwordGeneration.service.ts",
|
"jslib/src/abstractions/passwordGeneration.service.ts",
|
||||||
"jslib/src/angular/components/export.component.ts",
|
"jslib/src/angular/components/export.component.ts",
|
||||||
"jslib/src/angular/components/register.component.ts",
|
"jslib/src/angular/components/register.component.ts",
|
||||||
|
"jslib/src/angular/components/add-edit.component.ts",
|
||||||
"jslib/src/angular/components/password-generator.component.ts",
|
"jslib/src/angular/components/password-generator.component.ts",
|
||||||
"jslib/src/angular/components/password-generator-history.component.ts",
|
"jslib/src/angular/components/password-generator-history.component.ts",
|
||||||
"jslib/src/angular/pipes/color-password.pipe.ts",
|
"jslib/src/angular/pipes/color-password.pipe.ts",
|
||||||
"jslib/src/angular/directives/flex-copy.directive.ts",
|
"jslib/src/angular/directives/select-copy.directive.ts",
|
||||||
"jslib/src/importers",
|
"jslib/src/importers",
|
||||||
"dist",
|
"dist",
|
||||||
"dist-cli",
|
"dist-cli",
|
||||||
|
|||||||
Reference in New Issue
Block a user