1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-26 17:23:15 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Brandon
651867a2e9 wip fix test workflow 2026-02-26 12:20:59 -05:00
Brandon
2f713578a6 update workflow 2026-02-26 12:08:39 -05:00
Brandon
3af1e31168 restructure repo, update eslint rules 2026-02-26 11:29:30 -05:00
186 changed files with 4840 additions and 1091 deletions

View File

@@ -14,9 +14,9 @@ on:
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
- "libs/services/sync.service.ts" # core sync service used by all directory services
- "libs/services/directory-services/ldap-directory.service*" # LDAP directory service
- "libs/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
# Add directory services here as we add test coverage
pull_request:
paths:
@@ -24,9 +24,9 @@ on:
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
- "libs/services/sync.service.ts" # core sync service used by all directory services
- "libs/services/directory-services/ldap-directory.service*" # LDAP directory service
- "libs/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
# Add directory services here as we add test coverage
permissions:
contents: read
@@ -94,12 +94,12 @@ jobs:
- '.github/workflows/integration-test.yml'
- 'utils/**'
- 'package.json'
- 'src/services/sync.service.ts'
- 'libs/services/sync.service.ts'
ldap:
- 'docker-compose.yml'
- 'src/services/directory-services/ldap-directory.service*'
- 'libs/services/directory-services/ldap-directory.service*'
google:
- 'src/services/directory-services/gsuite-directory.service*'
- 'libs/services/directory-services/gsuite-directory.service*'
# LDAP
- name: Setup LDAP integration tests
@@ -109,6 +109,13 @@ jobs:
sudo apt-get -y install mkcert
npm run test:integration:setup
- name: Wait for LDAP container to be healthy
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
run: |
echo "Waiting for LDAP container to be healthy..."
timeout 60 bash -c 'until docker compose ps | grep open-ldap | grep -q "(healthy)"; do sleep 2; done'
echo "LDAP container is ready!"
- name: Run LDAP integration tests
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
env:

View File

@@ -14,7 +14,7 @@
}
},
"root": ".",
"sourceRoot": "src",
"sourceRoot": "src-gui",
"prefix": "app",
"architect": {
"build": {
@@ -23,12 +23,15 @@
"outputPath": {
"base": "dist"
},
"index": "src/index.html",
"index": "src-gui/index.html",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [],
"assets": [
{ "glob": "**/*", "input": "src-gui/images", "output": "images" },
{ "glob": "**/*", "input": "src-gui/locales", "output": "locales" }
],
"styles": ["src-gui/scss/styles.scss"],
"scripts": [],
"browser": "src/main.ts"
"browser": "src-gui/app/main.ts"
}
}
}

View File

@@ -16,3 +16,22 @@ services:
ports:
- "1389:1389"
- "1636:1636"
healthcheck:
test:
[
"CMD",
"ldapsearch",
"-x",
"-H",
"ldap://localhost:1389",
"-b",
"dc=bitwarden,dc=com",
"-D",
"cn=admin,dc=bitwarden,dc=com",
"-w",
"admin",
]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s

View File

@@ -87,14 +87,24 @@ export default [
"newlines-between": "always",
pathGroups: [
{
pattern: "@/jslib/**/*",
pattern: "@/libs/**",
group: "external",
position: "after",
},
{
pattern: "@/src/**/*",
group: "parent",
position: "before",
pattern: "@/jslib/**",
group: "external",
position: "after",
},
{
pattern: "@/src-gui/**",
group: "external",
position: "after",
},
{
pattern: "@/src-cli/**",
group: "external",
position: "after",
},
],
pathGroupsExcludedImportTypes: ["builtin"],

View File

View File

@@ -1,2 +1,2 @@
// Stub file - re-exports DC EnvironmentService
export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";
export { EnvironmentService, EnvironmentUrls } from "@/libs/abstractions/environment.service";

View File

@@ -1,2 +1,2 @@
// Stub file - re-exports DC StateService
export { StateService } from "@/src/abstractions/state.service";
export { StateService } from "@/libs/abstractions/state.service";

View File

@@ -1,2 +1,2 @@
// Stub file - re-exports DC TokenService
export { TokenService } from "@/src/abstractions/token.service";
export { TokenService } from "@/libs/abstractions/token.service";

View File

@@ -10,9 +10,9 @@ import {
Tray,
} from "electron";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { StateService } from "@/libs/abstractions/state.service";
import { StateService } from "@/src/abstractions/state.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { WindowMain } from "./window.main";

View File

@@ -3,9 +3,9 @@ import * as url from "url";
import { app, BrowserWindow, Rectangle, screen } from "electron";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "@/libs/abstractions/state.service";
import { StateService } from "@/src/abstractions/state.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { cleanUserAgent, isDev, isMacAppStore, isSnapStore } from "./utils";

View File

@@ -0,0 +1,6 @@
import { DirectoryType } from "@/libs/enums/directoryType";
import { IDirectoryService } from "@/libs/services/directory-services/directory.service";
export abstract class DirectoryFactoryService {
abstract createService(type: DirectoryType): IDirectoryService;
}

View File

@@ -1,7 +1,7 @@
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { GroupEntry } from "@/libs/models/groupEntry";
import { UserEntry } from "@/libs/models/userEntry";
import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
export interface RequestBuilderOptions {
removeDisabled: boolean;

View File

@@ -1,14 +1,14 @@
import { DirectoryType } from "@/libs/enums/directoryType";
import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/libs/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { OktaConfiguration } from "@/libs/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/libs/models/oneLoginConfiguration";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
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 StateService {
abstract getDirectory<IConfiguration>(type: DirectoryType): Promise<IConfiguration>;
abstract setDirectory(

View File

@@ -1,4 +1,4 @@
import { DecodedToken } from "@/src/utils/jwt.util";
import { DecodedToken } from "@/libs/utils/jwt.util";
export abstract class TokenService {
// Token storage

View File

@@ -1,4 +1,4 @@
import { DirectoryType } from "@/src/enums/directoryType";
import { DirectoryType } from "@/libs/enums/directoryType";
import { EntraIdConfiguration } from "./entraIdConfiguration";
import { GSuiteConfiguration } from "./gsuiteConfiguration";

View File

@@ -1,3 +1,5 @@
import { StateService } from "@/libs/abstractions/state.service";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
@@ -7,8 +9,6 @@ import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { StateService } from "../abstractions/state.service";
export class AuthService {
constructor(
private apiService: ApiService,

View File

@@ -1,5 +1,7 @@
import { mock } from "jest-mock-extended";
import { StateService } from "@/libs/abstractions/state.service";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
@@ -7,8 +9,6 @@ import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUt
import { Utils } from "@/jslib/common/src/misc/utils";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { StateService } from "../abstractions/state.service";
import { AuthService } from "./auth.service";
const clientId = "organization.CLIENT_ID";

View File

@@ -1,6 +1,6 @@
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { GroupEntry } from "@/libs/models/groupEntry";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { UserEntry } from "@/libs/models/userEntry";
export abstract class BaseDirectoryService {
protected createDirectoryQuery(filter: string) {

View File

@@ -1,10 +1,9 @@
import { RequestBuilder, RequestBuilderOptions } from "@/libs/abstractions/request-builder.service";
import { GroupEntry } from "@/libs/models/groupEntry";
import { UserEntry } from "@/libs/models/userEntry";
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
import { batchSize } from "./sync.service";
/**

View File

@@ -1,12 +1,12 @@
import { RequestBuilderOptions } from "@/libs/abstractions/request-builder.service";
import { UserEntry } from "@/libs/models/userEntry";
import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { BatchRequestBuilder } from "./batch-request-builder";
import { groupSimulator, userSimulator } from "@/utils/request-builder-helper";
describe("BatchRequestBuilder", () => {
let batchRequestBuilder: BatchRequestBuilder;

View File

@@ -1,10 +1,10 @@
import { DirectoryFactoryService } from "@/libs/abstractions/directory-factory.service";
import { StateService } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
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 { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service";
import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";

View File

@@ -1,7 +1,7 @@
import { config as dotenvConfig } from "dotenv";
import { mock, MockProxy } from "jest-mock-extended";
import { StateService } from "@/src/abstractions/state.service";
import { StateService } from "@/libs/abstractions/state.service";
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../../jslib/common/src/abstractions/log.service";

View File

@@ -1,11 +1,11 @@
import { JWT } from "google-auth-library";
import { admin_directory_v1, google } from "googleapis";
import { StateService } from "@/libs/abstractions/state.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "@/src/abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../../models/groupEntry";
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";

View File

@@ -1,7 +1,7 @@
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
import { EnvironmentService as IEnvironmentService } from "@/libs/abstractions/environment.service";
import { StateService } from "@/libs/abstractions/state.service";
import { EnvironmentService as IEnvironmentService } from "@/src/abstractions/environment.service";
import { StateService } from "@/src/abstractions/state.service";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
export class EnvironmentService implements IEnvironmentService {
private readonly DEFAULT_URLS = {

View File

@@ -1,12 +1,12 @@
import { RequestBuilderOptions } from "@/libs/abstractions/request-builder.service";
import { UserEntry } from "@/libs/models/userEntry";
import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { SingleRequestBuilder } from "./single-request-builder";
import { groupSimulator, userSimulator } from "@/utils/request-builder-helper";
describe("SingleRequestBuilder", () => {
let singleRequestBuilder: SingleRequestBuilder;

View File

@@ -1,10 +1,9 @@
import { RequestBuilder, RequestBuilderOptions } from "@/libs/abstractions/request-builder.service";
import { GroupEntry } from "@/libs/models/groupEntry";
import { UserEntry } from "@/libs/models/userEntry";
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
/**
* This class is responsible for building small (<2k users) syncs as a single
* request to the /import endpoint. This is done to be backwards compatible with

View File

@@ -1,21 +1,21 @@
import { mock, MockProxy } from "jest-mock-extended";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
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 { DirectoryType } from "@/libs/enums/directoryType";
import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/libs/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { OktaConfiguration } from "@/libs/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/libs/models/oneLoginConfiguration";
import {
SecureStorageKeysVNext as SecureStorageKeys,
StorageKeysVNext as StorageKeys,
StoredSecurely,
} from "@/src/models/state.model";
import { SyncConfiguration } from "@/src/models/syncConfiguration";
} from "@/libs/models/state.model";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
import { StateServiceImplementation } from "./state.service";
import { StateMigrationService } from "./stateMigration.service";

View File

@@ -1,22 +1,22 @@
import { LogService } from "@/jslib/common/src/abstractions/log.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 { StateService as StateServiceAbstraction } from "@/src/abstractions/state.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 { StateService as StateServiceAbstraction } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
import { IConfiguration } from "@/libs/models/IConfiguration";
import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/libs/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { OktaConfiguration } from "@/libs/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/libs/models/oneLoginConfiguration";
import {
SecureStorageKeysVNext as SecureStorageKeys,
StorageKeysVNext as StorageKeys,
StoredSecurely,
} from "@/src/models/state.model";
import { SyncConfiguration } from "@/src/models/syncConfiguration";
} from "@/libs/models/state.model";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { LogService } from "@/jslib/common/src/abstractions/log.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 { StateMigrationService } from "./stateMigration.service";
@@ -566,4 +566,4 @@ export class StateServiceImplementation implements StateServiceAbstraction {
}
// Re-export the abstraction for convenience
export { StateService } from "@/src/abstractions/state.service";
export { StateService } from "@/libs/abstractions/state.service";

View File

@@ -1,15 +1,10 @@
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { HtmlStorageLocation } from "@/jslib/common/src/enums/htmlStorageLocation";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
import { DirectoryType } from "@/src/enums/directoryType";
import { DirectoryConfigurations, DirectorySettings } from "@/src/models/account";
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 { DirectoryType } from "@/libs/enums/directoryType";
import { DirectoryConfigurations, DirectorySettings } from "@/libs/models/account";
import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/libs/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { OktaConfiguration } from "@/libs/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/libs/models/oneLoginConfiguration";
import {
MigrationClientKeys as ClientKeys,
MigrationKeys as Keys,
@@ -17,8 +12,13 @@ import {
SecureStorageKeysMigration as SecureStorageKeys,
SecureStorageKeysVNext,
StorageKeysVNext,
} from "@/src/models/state.model";
import { SyncConfiguration } from "@/src/models/syncConfiguration";
} from "@/libs/models/state.model";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { HtmlStorageLocation } from "@/jslib/common/src/enums/htmlStorageLocation";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
export class StateMigrationService {
constructor(

View File

@@ -1,15 +1,15 @@
import { mock, MockProxy } from "jest-mock-extended";
import { DirectoryFactoryService } from "@/libs/abstractions/directory-factory.service";
import { StateService } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../jslib/common/src/abstractions/log.service";
import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
@@ -17,6 +17,7 @@ import { SingleRequestBuilder } from "./single-request-builder";
import { SyncService } from "./sync.service";
import * as constants from "./sync.service";
import { getLdapConfiguration, getSyncConfiguration } from "@/utils/openldap/config-fixtures";
import { groupFixtures } from "@/utils/openldap/group-fixtures";
import { userFixtures } from "@/utils/openldap/user-fixtures";

View File

@@ -1,15 +1,14 @@
import { mock, MockProxy } from "jest-mock-extended";
import { DirectoryFactoryService } from "@/libs/abstractions/directory-factory.service";
import { StateService } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { ApiService } from "@/jslib/common/src/services/api.service";
import { getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { I18nService } from "./i18n.service";
@@ -17,6 +16,7 @@ import { SingleRequestBuilder } from "./single-request-builder";
import { SyncService } from "./sync.service";
import * as constants from "./sync.service";
import { getSyncConfiguration } from "@/utils/openldap/config-fixtures";
import { groupFixtures } from "@/utils/openldap/group-fixtures";
import { userFixtures } from "@/utils/openldap/user-fixtures";

View File

@@ -1,3 +1,10 @@
import { DirectoryFactoryService } from "@/libs/abstractions/directory-factory.service";
import { StateService } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
import { GroupEntry } from "@/libs/models/groupEntry";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { UserEntry } from "@/libs/models/userEntry";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
@@ -5,13 +12,6 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { Utils } from "@/jslib/common/src/misc/utils";
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { BatchRequestBuilder } from "./batch-request-builder";
import { SingleRequestBuilder } from "./single-request-builder";

View File

@@ -1,11 +1,11 @@
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as ITokenService } from "@/src/abstractions/token.service";
import { TokenService as ITokenService } from "@/libs/abstractions/token.service";
import {
DecodedToken,
decodeJwt,
tokenNeedsRefresh as checkTokenNeedsRefresh,
} from "@/src/utils/jwt.util";
} from "@/libs/utils/jwt.util";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
export class TokenService implements ITokenService {
// Storage keys

View File

@@ -1,11 +1,11 @@
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Entry } from "@/libs/models/entry";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { SimResult } from "@/libs/models/simResult";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { UserEntry } from "@/libs/models/userEntry";
import { SyncService } from "@/libs/services/sync.service";
import { Entry } from "./models/entry";
import { LdapConfiguration } from "./models/ldapConfiguration";
import { SimResult } from "./models/simResult";
import { SyncConfiguration } from "./models/syncConfiguration";
import { UserEntry } from "./models/userEntry";
import { SyncService } from "./services/sync.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
export class ConnectorUtils {
static async simulate(

View File

@@ -3,35 +3,37 @@ import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import * as path from "path";
import { DirectoryFactoryService } from "@/libs/abstractions/directory-factory.service";
import { EnvironmentService } from "@/libs/abstractions/environment.service";
import { StateService } from "@/libs/abstractions/state.service";
import { TokenService } from "@/libs/abstractions/token.service";
import { AuthService } from "@/libs/services/auth.service";
import { BatchRequestBuilder } from "@/libs/services/batch-request-builder";
import { DefaultDirectoryFactoryService } from "@/libs/services/directory-factory.service";
import { EnvironmentService as EnvironmentServiceImplementation } from "@/libs/services/environment/environment.service";
import { I18nService } from "@/libs/services/i18n.service";
import { KeytarSecureStorageService } from "@/libs/services/keytarSecureStorage.service";
import { LowdbStorageService } from "@/libs/services/lowdbStorage.service";
import { SingleRequestBuilder } from "@/libs/services/single-request-builder";
import { StateServiceImplementation } from "@/libs/services/state-service/state.service";
import { StateMigrationService } from "@/libs/services/state-service/stateMigration.service";
import { SyncService } from "@/libs/services/sync.service";
import { TokenService as TokenServiceImplementation } from "@/libs/services/token/token.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
import { CliPlatformUtilsService } from "@/src-cli/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/src-cli/cli/services/consoleLog.service";
import { NodeApiService } from "@/src-cli/services/node/nodeApi.service";
import { NodeCryptoFunctionService } from "@/src-cli/services/node/nodeCryptoFunction.service";
import packageJson from "../package.json";
import { DirectoryFactoryService } from "./abstractions/directory-factory.service";
import { EnvironmentService } from "./abstractions/environment.service";
import { StateService } from "./abstractions/state.service";
import { TokenService } from "./abstractions/token.service";
import { Program } from "./program";
import { AuthService } from "./services/auth.service";
import { BatchRequestBuilder } from "./services/batch-request-builder";
import { DefaultDirectoryFactoryService } from "./services/directory-factory.service";
import { EnvironmentService as EnvironmentServiceImplementation } from "./services/environment/environment.service";
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 { StateServiceImplementation } from "./services/state-service/state.service";
import { StateMigrationService } from "./services/state-service/stateMigration.service";
import { SyncService } from "./services/sync.service";
import { TokenService as TokenServiceImplementation } from "./services/token/token.service";
// ESM __dirname polyfill for Node 20

116
src-cli/cli/baseProgram.ts Normal file
View File

@@ -0,0 +1,116 @@
import * as chalk from "chalk";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { Response } from "@/src-cli/cli/models/response";
import { ListResponse } from "@/src-cli/cli/models/response/listResponse";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
import { StringResponse } from "@/src-cli/cli/models/response/stringResponse";
export abstract class BaseProgram {
constructor(
protected stateService: StateService,
private writeLn: (s: string, finalLine: boolean, error: boolean) => void,
) {}
protected processResponse(
response: Response,
exitImmediately = false,
dataProcessor: () => string = null,
) {
if (!response.success) {
if (process.env.BW_QUIET !== "true") {
if (process.env.BW_RESPONSE === "true") {
this.writeLn(this.getJson(response), true, false);
} else {
this.writeLn(chalk.redBright(response.message), true, true);
}
}
const exitCode = process.env.BW_CLEANEXIT ? 0 : 1;
if (exitImmediately) {
process.exit(exitCode);
} else {
process.exitCode = exitCode;
}
return;
}
if (process.env.BW_RESPONSE === "true") {
this.writeLn(this.getJson(response), true, false);
} else if (response.data != null) {
let out: string = dataProcessor != null ? dataProcessor() : null;
if (out == null) {
if (response.data.object === "string") {
const data = (response.data as StringResponse).data;
if (data != null) {
out = data;
}
} else if (response.data.object === "list") {
out = this.getJson((response.data as ListResponse).data);
} else if (response.data.object === "message") {
out = this.getMessage(response);
} else {
out = this.getJson(response.data);
}
}
if (out != null && process.env.BW_QUIET !== "true") {
this.writeLn(out, true, false);
}
}
if (exitImmediately) {
process.exit(0);
} else {
process.exitCode = 0;
}
}
protected getJson(obj: any): string {
if (process.env.BW_PRETTY === "true") {
return JSON.stringify(obj, null, " ");
} else {
return JSON.stringify(obj);
}
}
protected getMessage(response: Response): string {
const message = response.data as MessageResponse;
if (process.env.BW_RAW === "true") {
return message.raw;
}
let out = "";
if (message.title != null) {
if (message.noColor) {
out = message.title;
} else {
out = chalk.greenBright(message.title);
}
}
if (message.message != null) {
if (message.title != null) {
out += "\n";
}
out += message.message;
}
return out.trim() === "" ? null : out;
}
protected async exitIfAuthed() {
const authed = await this.stateService.getIsAuthenticated();
if (authed) {
const organizationId = await this.stateService.getEntityId();
this.processResponse(
Response.error("You are already logged in to" + organizationId + "."),
true,
);
}
}
protected async exitIfNotAuthed() {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
this.processResponse(Response.error("You are not logged in."), true);
}
}
}

View File

@@ -0,0 +1,104 @@
import * as fetch from "node-fetch";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
export class UpdateCommand {
inPkg = false;
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private repoName: string,
private executableName: string,
private showExtendedMessage: boolean,
) {
this.inPkg = !!(process as any).pkg;
}
async run(): Promise<Response> {
const currentVersion = await this.platformUtilsService.getApplicationVersion();
const response = await fetch.default(
"https://api.github.com/repos/bitwarden/" + this.repoName + "/releases/latest",
);
if (response.status === 200) {
const responseJson = await response.json();
const res = new MessageResponse(null, null);
const tagName: string = responseJson.tag_name;
if (tagName === "v" + currentVersion) {
res.title = "No update available.";
res.noColor = true;
return Response.success(res);
}
let downloadUrl: string = null;
if (responseJson.assets != null) {
for (const a of responseJson.assets) {
const download: string = a.browser_download_url;
if (download == null) {
continue;
}
if (download.indexOf(".zip") === -1) {
continue;
}
if (
process.platform === "win32" &&
download.indexOf(this.executableName + "-windows") > -1
) {
downloadUrl = download;
break;
} else if (
process.platform === "darwin" &&
download.indexOf(this.executableName + "-macos") > -1
) {
downloadUrl = download;
break;
} else if (
process.platform === "linux" &&
download.indexOf(this.executableName + "-linux") > -1
) {
downloadUrl = download;
break;
}
}
}
res.title = "A new version is available: " + tagName;
if (downloadUrl == null) {
downloadUrl = "https://github.com/bitwarden/" + this.repoName + "/releases";
} else {
res.raw = downloadUrl;
}
res.message = "";
if (responseJson.body != null && responseJson.body !== "") {
res.message = responseJson.body + "\n\n";
}
res.message += "You can download this update at " + downloadUrl;
if (this.showExtendedMessage) {
if (this.inPkg) {
res.message +=
"\n\nIf you installed this CLI through a package manager " +
"you should probably update using its update command instead.";
} else {
res.message +=
"\n\nIf you installed this CLI through NPM " +
"you should update using `npm install -g @bitwarden/" +
this.repoName +
"`";
}
}
return Response.success(res);
} else {
return Response.error("Error contacting update API: " + response.status);
}
}
}

View File

@@ -0,0 +1,50 @@
import { BaseResponse } from "./response/baseResponse";
export class Response {
static error(error: any, data?: any): Response {
const res = new Response();
res.success = false;
if (typeof error === "string") {
res.message = error;
} else {
res.message =
error.message != null
? error.message
: error.toString() === "[object Object]"
? JSON.stringify(error)
: error.toString();
}
res.data = data;
return res;
}
static notFound(): Response {
return Response.error("Not found.");
}
static badRequest(message: string): Response {
return Response.error(message);
}
static multipleResults(ids: string[]): Response {
let msg =
"More than one result was found. Try getting a specific object by `id` instead. " +
"The following objects were found:";
ids.forEach((id) => {
msg += "\n" + id;
});
return Response.error(msg, ids);
}
static success(data?: BaseResponse): Response {
const res = new Response();
res.success = true;
res.data = data;
return res;
}
success: boolean;
message: string;
errorCode: number;
data: BaseResponse;
}

View File

@@ -0,0 +1,3 @@
export interface BaseResponse {
object: string;
}

View File

@@ -0,0 +1,11 @@
import { BaseResponse } from "./baseResponse";
export class ListResponse implements BaseResponse {
object: string;
data: BaseResponse[];
constructor(data: BaseResponse[]) {
this.object = "list";
this.data = data;
}
}

View File

@@ -0,0 +1,15 @@
import { BaseResponse } from "./baseResponse";
export class MessageResponse implements BaseResponse {
object: string;
title: string;
message: string;
raw: string;
noColor = false;
constructor(title: string, message: string) {
this.object = "message";
this.title = title;
this.message = message;
}
}

View File

@@ -0,0 +1,11 @@
import { BaseResponse } from "./baseResponse";
export class StringResponse implements BaseResponse {
object: string;
data: string;
constructor(data: string) {
this.object = "string";
this.data = data;
}
}

View File

@@ -0,0 +1,160 @@
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
import { ThemeType } from "@/jslib/common/src/enums/themeType";
export class CliPlatformUtilsService implements PlatformUtilsService {
clientType: ClientType;
private deviceCache: DeviceType = null;
constructor(
clientType: ClientType,
private packageJson: any,
) {
this.clientType = clientType;
}
getDevice(): DeviceType {
if (!this.deviceCache) {
switch (process.platform) {
case "win32":
this.deviceCache = DeviceType.WindowsDesktop;
break;
case "darwin":
this.deviceCache = DeviceType.MacOsDesktop;
break;
case "linux":
default:
this.deviceCache = DeviceType.LinuxDesktop;
break;
}
}
return this.deviceCache;
}
getDeviceString(): string {
const device = DeviceType[this.getDevice()].toLowerCase();
return device.replace("desktop", "");
}
getClientType() {
return this.clientType;
}
isFirefox() {
return false;
}
isChrome() {
return false;
}
isEdge() {
return false;
}
isOpera() {
return false;
}
isVivaldi() {
return false;
}
isSafari() {
return false;
}
isMacAppStore() {
return false;
}
isViewOpen() {
return Promise.resolve(false);
}
launchUri(_uri: string, _options?: any): void {
throw new Error("Not implemented.");
}
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
throw new Error("Not implemented.");
}
getApplicationVersion(): Promise<string> {
return Promise.resolve(this.packageJson.version);
}
getApplicationVersionSync(): string {
return this.packageJson.version;
}
supportsWebAuthn(win: Window) {
return false;
}
supportsDuo(): boolean {
return false;
}
showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
throw new Error("Not implemented.");
}
showDialog(
text: string,
title?: string,
confirmText?: string,
cancelText?: string,
type?: string,
): Promise<boolean> {
throw new Error("Not implemented.");
}
isDev(): boolean {
return process.env.BWCLI_ENV === "development";
}
isSelfHost(): boolean {
return false;
}
copyToClipboard(text: string, options?: any): void {
throw new Error("Not implemented.");
}
readFromClipboard(options?: any): Promise<string> {
throw new Error("Not implemented.");
}
supportsBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
authenticateBiometric(): Promise<boolean> {
return Promise.resolve(false);
}
getDefaultSystemTheme() {
return Promise.resolve(ThemeType.Light as ThemeType.Light | ThemeType.Dark);
}
onDefaultSystemThemeChange() {
/* noop */
}
getEffectiveTheme() {
return Promise.resolve(ThemeType.Light);
}
supportsSecureStorage(): boolean {
return false;
}
}

View File

@@ -0,0 +1,22 @@
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
import { ConsoleLogService as BaseConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
export class ConsoleLogService extends BaseConsoleLogService {
constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) {
super(isDev, filter);
}
write(level: LogLevelType, message: string) {
if (this.filter != null && this.filter(level)) {
return;
}
if (process.env.BW_RESPONSE === "true") {
// eslint-disable-next-line
console.error(message);
return;
}
super.write(level, message);
}
}

View File

@@ -1,10 +1,11 @@
import * as program from "commander";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { StateService } from "@/libs/abstractions/state.service";
import { StateService } from "../abstractions/state.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
export class ClearCacheCommand {
constructor(

View File

@@ -1,20 +1,21 @@
import * as program from "commander";
import { StateService } from "@/libs/abstractions/state.service";
import { DirectoryType } from "@/libs/enums/directoryType";
import { EntraIdConfiguration } from "@/libs/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/libs/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/libs/models/ldapConfiguration";
import { OktaConfiguration } from "@/libs/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/libs/models/oneLoginConfiguration";
import { SyncConfiguration } from "@/libs/models/syncConfiguration";
import { ConnectorUtils } from "@/libs/utils";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { EntraIdConfiguration } from "../models/entraIdConfiguration";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { ConnectorUtils } from "../utils";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
export class ConfigCommand {
private directory: DirectoryType;

View File

@@ -1,7 +1,7 @@
import { Response } from "@/jslib/node/src/cli/models/response";
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
import { StateService } from "@/libs/abstractions/state.service";
import { StateService } from "../abstractions/state.service";
import { Response } from "@/src-cli/cli/models/response";
import { StringResponse } from "@/src-cli/cli/models/response/stringResponse";
export class LastSyncCommand {
constructor(private stateService: StateService) {}

View File

@@ -1,6 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "../abstractions/auth.service";
import { AuthService } from "@/libs/abstractions/auth.service";
import { LoginCommand } from "./login.command";

View File

@@ -1,10 +1,11 @@
import * as inquirer from "inquirer";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { AuthService } from "@/libs/abstractions/auth.service";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
import { Utils } from "../../jslib/common/src/misc/utils";
import { AuthService } from "../abstractions/auth.service";
export class LoginCommand {
private canInteract: boolean;

View File

@@ -1,7 +1,7 @@
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { AuthService } from "@/libs/abstractions/auth.service";
import { AuthService } from "../abstractions/auth.service";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
export class LogoutCommand {
constructor(

View File

@@ -1,8 +1,9 @@
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { SyncService } from "@/libs/services/sync.service";
import { SyncService } from "../services/sync.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/src-cli/cli/models/response";
import { MessageResponse } from "@/src-cli/cli/models/response/messageResponse";
export class SyncCommand {
constructor(

View File

@@ -1,11 +1,12 @@
import * as program from "commander";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/jslib/node/src/cli/models/response";
import { TestResponse } from "@/libs/models/response/testResponse";
import { SyncService } from "@/libs/services/sync.service";
import { ConnectorUtils } from "@/libs/utils";
import { TestResponse } from "../models/response/testResponse";
import { SyncService } from "../services/sync.service";
import { ConnectorUtils } from "../utils";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/src-cli/cli/models/response";
export class TestCommand {
constructor(

View File

@@ -4,10 +4,11 @@ import * as chalk from "chalk";
import { Command, OptionValues } from "commander";
import { Utils } from "@/jslib/common/src/misc/utils";
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
import { Response } from "@/jslib/node/src/cli/models/response";
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
import { BaseProgram } from "@/src-cli/cli/baseProgram";
import { UpdateCommand } from "@/src-cli/cli/commands/update.command";
import { Response } from "@/src-cli/cli/models/response";
import { StringResponse } from "@/src-cli/cli/models/response/stringResponse";
import { Main } from "./bwdc";
import { ClearCacheCommand } from "./commands/clearCache.command";

View File

@@ -0,0 +1,148 @@
import * as fs from "fs";
import * as path from "path";
import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
import { Utils } from "@/jslib/common/src/misc/utils";
export class LowdbStorageService implements StorageService {
protected dataFilePath: string;
private db: lowdb.LowdbSync<any>;
private defaults: any;
private ready = false;
constructor(
protected logService: LogService,
defaults?: any,
private dir?: string,
private allowCache = false,
) {
this.defaults = defaults;
}
@sequentialize(() => "lowdbStorageInit")
async init() {
if (this.ready) {
return;
}
this.logService.info("Initializing lowdb storage service.");
let adapter: lowdb.AdapterSync<any>;
if (Utils.isNode && this.dir != null) {
if (!fs.existsSync(this.dir)) {
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
NodeUtils.mkdirpSync(this.dir, "700");
this.logService.info(`Created dir "${this.dir}".`);
}
this.dataFilePath = path.join(this.dir, "data.json");
if (!fs.existsSync(this.dataFilePath)) {
this.logService.warning(
`Could not find data file, "${this.dataFilePath}"; creating it instead.`,
);
fs.writeFileSync(this.dataFilePath, "", { mode: 0o600 });
fs.chmodSync(this.dataFilePath, 0o600);
this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`);
} else {
this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`);
}
await this.lockDbFile(() => {
adapter = new FileSync(this.dataFilePath);
});
}
try {
this.logService.info("Attempting to create lowdb storage adapter.");
this.db = lowdb(adapter);
this.logService.info("Successfully created lowdb storage adapter.");
} catch (e) {
if (e instanceof SyntaxError) {
this.logService.warning(
`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`,
);
if (fs.existsSync(this.dataFilePath)) {
const backupPath = this.dataFilePath + ".bak";
this.logService.warning(`Writing backup of data file to ${backupPath}`);
await fs.copyFile(this.dataFilePath, backupPath, () => {
this.logService.warning(
`Error while creating data file backup, "${e.message}". No backup may have been created.`,
);
});
}
adapter.write({});
this.db = lowdb(adapter);
} else {
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
throw e;
}
}
if (this.defaults != null) {
this.lockDbFile(() => {
this.logService.info("Writing defaults.");
this.readForNoCache();
this.db.defaults(this.defaults).write();
this.logService.info("Successfully wrote defaults to db.");
});
}
this.ready = true;
}
async get<T>(key: string): Promise<T> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
const val = this.db.get(key).value();
this.logService.debug(`Successfully read ${key} from db`);
if (val == null) {
return null;
}
return val as T;
});
}
has(key: string): Promise<boolean> {
return this.get(key).then((v) => v != null);
}
async save(key: string, obj: any): Promise<any> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.set(key, obj).write();
this.logService.debug(`Successfully wrote ${key} to db`);
return;
});
}
async remove(key: string): Promise<any> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.unset(key).write();
this.logService.debug(`Successfully removed ${key} from db`);
return;
});
}
protected async lockDbFile<T>(action: () => T): Promise<T> {
// Lock methods implemented in clients
return Promise.resolve(action());
}
private readForNoCache() {
if (!this.allowCache) {
this.db.read();
}
}
private async waitForReady() {
if (!this.ready) {
await this.init();
}
}
}

View File

@@ -0,0 +1,43 @@
import * as FormData from "form-data";
import { HttpsProxyAgent } from "https-proxy-agent";
import * as fe from "node-fetch";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { ApiService } from "@/jslib/common/src/services/api.service";
(global as any).fetch = fe.default;
(global as any).Request = fe.Request;
(global as any).Response = fe.Response;
(global as any).Headers = fe.Headers;
(global as any).FormData = FormData;
export class NodeApiService extends ApiService {
constructor(
tokenService: TokenService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
appIdService: AppIdService,
logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null,
) {
super(
tokenService,
platformUtilsService,
environmentService,
appIdService,
logoutCallback,
customUserAgent,
);
}
nativeFetch(request: Request): Promise<Response> {
const proxy = process.env.http_proxy || process.env.https_proxy;
if (proxy) {
(request as any).agent = new HttpsProxyAgent(proxy);
}
return fetch(request);
}
}

View File

@@ -0,0 +1,301 @@
import * as crypto from "crypto";
import * as forge from "node-forge";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { DecryptParameters } from "@/jslib/common/src/models/domain/decryptParameters";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
export class NodeCryptoFunctionService implements CryptoFunctionService {
pbkdf2(
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
algorithm: "sha256" | "sha512",
iterations: number,
): Promise<ArrayBuffer> {
const len = algorithm === "sha256" ? 32 : 64;
const nodePassword = this.toNodeValue(password);
const nodeSalt = this.toNodeValue(salt);
return new Promise<ArrayBuffer>((resolve, reject) => {
crypto.pbkdf2(nodePassword, nodeSalt, iterations, len, algorithm, (error, key) => {
if (error != null) {
reject(error);
} else {
resolve(this.toArrayBuffer(key));
}
});
});
}
// ref: https://tools.ietf.org/html/rfc5869
async hkdf(
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512",
): Promise<ArrayBuffer> {
const saltBuf = this.toArrayBuffer(salt);
const prk = await this.hmac(ikm, saltBuf, algorithm);
return this.hkdfExpand(prk, info, outputByteSize, algorithm);
}
// ref: https://tools.ietf.org/html/rfc5869
async hkdfExpand(
prk: ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512",
): Promise<ArrayBuffer> {
const hashLen = algorithm === "sha256" ? 32 : 64;
if (outputByteSize > 255 * hashLen) {
throw new Error("outputByteSize is too large.");
}
const prkArr = new Uint8Array(prk);
if (prkArr.length < hashLen) {
throw new Error("prk is too small.");
}
const infoBuf = this.toArrayBuffer(info);
const infoArr = new Uint8Array(infoBuf);
let runningOkmLength = 0;
let previousT = new Uint8Array(0);
const n = Math.ceil(outputByteSize / hashLen);
const okm = new Uint8Array(n * hashLen);
for (let i = 0; i < n; i++) {
const t = new Uint8Array(previousT.length + infoArr.length + 1);
t.set(previousT);
t.set(infoArr, previousT.length);
t.set([i + 1], t.length - 1);
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
okm.set(previousT, runningOkmLength);
runningOkmLength += previousT.length;
if (runningOkmLength >= outputByteSize) {
break;
}
}
return okm.slice(0, outputByteSize).buffer;
}
hash(
value: string | ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512" | "md5",
): Promise<ArrayBuffer> {
const nodeValue = this.toNodeValue(value);
const hash = crypto.createHash(algorithm);
hash.update(nodeValue);
return Promise.resolve(this.toArrayBuffer(hash.digest()));
}
hmac(
value: ArrayBuffer,
key: ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512",
): Promise<ArrayBuffer> {
const nodeValue = this.toNodeBuffer(value);
const nodeKey = this.toNodeBuffer(key);
const hmac = crypto.createHmac(algorithm, nodeKey);
hmac.update(nodeValue);
return Promise.resolve(this.toArrayBuffer(hmac.digest()));
}
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
const key = await this.randomBytes(32);
const mac1 = await this.hmac(a, key, "sha256");
const mac2 = await this.hmac(b, key, "sha256");
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
const arr1 = new Uint8Array(mac1);
const arr2 = new Uint8Array(mac2);
for (let i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
hmacFast(
value: ArrayBuffer,
key: ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512",
): Promise<ArrayBuffer> {
return this.hmac(value, key, algorithm);
}
compareFast(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
return this.compare(a, b);
}
aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const nodeData = this.toNodeBuffer(data);
const nodeIv = this.toNodeBuffer(iv);
const nodeKey = this.toNodeBuffer(key);
const cipher = crypto.createCipheriv("aes-256-cbc", nodeKey, nodeIv);
const encBuf = Buffer.concat([cipher.update(nodeData), cipher.final()]);
return Promise.resolve(this.toArrayBuffer(encBuf));
}
aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey,
): DecryptParameters<ArrayBuffer> {
const p = new DecryptParameters<ArrayBuffer>();
p.encKey = key.encKey;
p.data = Utils.fromB64ToArray(data).buffer;
p.iv = Utils.fromB64ToArray(iv).buffer;
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
macData.set(new Uint8Array(p.iv), 0);
macData.set(new Uint8Array(p.data), p.iv.byteLength);
p.macData = macData.buffer;
if (key.macKey != null) {
p.macKey = key.macKey;
}
if (mac != null) {
p.mac = Utils.fromB64ToArray(mac).buffer;
}
return p;
}
async aesDecryptFast(parameters: DecryptParameters<ArrayBuffer>): Promise<string> {
const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey);
return Utils.fromBufferToUtf8(decBuf);
}
aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const nodeData = this.toNodeBuffer(data);
const nodeIv = this.toNodeBuffer(iv);
const nodeKey = this.toNodeBuffer(key);
const decipher = crypto.createDecipheriv("aes-256-cbc", nodeKey, nodeIv);
const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]);
return Promise.resolve(this.toArrayBuffer(decBuf));
}
rsaEncrypt(
data: ArrayBuffer,
publicKey: ArrayBuffer,
algorithm: "sha1" | "sha256",
): Promise<ArrayBuffer> {
if (algorithm === "sha256") {
throw new Error("Node crypto does not support RSA-OAEP SHA-256");
}
const pem = this.toPemPublicKey(publicKey);
const decipher = crypto.publicEncrypt(pem, this.toNodeBuffer(data));
return Promise.resolve(this.toArrayBuffer(decipher));
}
rsaDecrypt(
data: ArrayBuffer,
privateKey: ArrayBuffer,
algorithm: "sha1" | "sha256",
): Promise<ArrayBuffer> {
if (algorithm === "sha256") {
throw new Error("Node crypto does not support RSA-OAEP SHA-256");
}
const pem = this.toPemPrivateKey(privateKey);
const decipher = crypto.privateDecrypt(pem, this.toNodeBuffer(data));
return Promise.resolve(this.toArrayBuffer(decipher));
}
rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
const privateKeyByteString = Utils.fromBufferToByteString(privateKey);
const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString);
const forgePrivateKey: any = forge.pki.privateKeyFromAsn1(privateKeyAsn1);
const forgePublicKey = (forge.pki as any).setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e);
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey);
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data;
const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString);
return Promise.resolve(publicKeyArray.buffer);
}
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
return new Promise<[ArrayBuffer, ArrayBuffer]>((resolve, reject) => {
forge.pki.rsa.generateKeyPair(
{
bits: length,
workers: -1,
e: 0x10001, // 65537
},
(error, keyPair) => {
if (error != null) {
reject(error);
return;
}
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(keyPair.publicKey);
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).getBytes();
const publicKey = Utils.fromByteStringToArray(publicKeyByteString);
const privateKeyAsn1 = forge.pki.privateKeyToAsn1(keyPair.privateKey);
const privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1);
const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes();
const privateKey = Utils.fromByteStringToArray(privateKeyByteString);
resolve([publicKey.buffer, privateKey.buffer]);
},
);
});
}
randomBytes(length: number): Promise<ArrayBuffer> {
return new Promise<ArrayBuffer>((resolve, reject) => {
crypto.randomBytes(length, (error, bytes) => {
if (error != null) {
reject(error);
} else {
resolve(this.toArrayBuffer(bytes));
}
});
});
}
private toNodeValue(value: string | ArrayBuffer): string | Buffer {
let nodeValue: string | Buffer;
if (typeof value === "string") {
nodeValue = value;
} else {
nodeValue = this.toNodeBuffer(value);
}
return nodeValue;
}
private toNodeBuffer(value: ArrayBuffer): Buffer {
return Buffer.from(new Uint8Array(value) as any);
}
private toArrayBuffer(value: Buffer | string | ArrayBuffer): ArrayBuffer {
let buf: ArrayBuffer;
if (typeof value === "string") {
buf = Utils.fromUtf8ToArray(value).buffer;
} else {
buf = new Uint8Array(value).buffer;
}
return buf;
}
private toPemPrivateKey(key: ArrayBuffer): string {
const byteString = Utils.fromBufferToByteString(key);
const asn1 = forge.asn1.fromDer(byteString);
const privateKey = forge.pki.privateKeyFromAsn1(asn1);
const rsaPrivateKey = forge.pki.privateKeyToAsn1(privateKey);
const privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);
return forge.pki.privateKeyInfoToPem(privateKeyInfo);
}
private toPemPublicKey(key: ArrayBuffer): string {
const byteString = Utils.fromBufferToByteString(key);
const asn1 = forge.asn1.fromDer(byteString);
const publicKey = forge.pki.publicKeyFromAsn1(asn1);
return forge.pki.publicKeyToPem(publicKey);
}
}

View File

@@ -0,0 +1,79 @@
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from "@angular/cdk/a11y";
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
OnDestroy,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ModalService } from "@/src-gui/angular/services/modal.service";
import { ModalRef } from "./modal.ref";
@Component({
selector: "app-modal",
template: "<ng-template #modalContent></ng-template>",
})
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
componentRef: ComponentRef<any>;
@ViewChild("modalContent", { read: ViewContainerRef, static: true })
modalContentRef: ViewContainerRef;
childComponentType: Type<any>;
setComponentParameters: (component: any) => void;
private focusTrap: ConfigurableFocusTrap;
constructor(
private modalService: ModalService,
private cd: ChangeDetectorRef,
private el: ElementRef<HTMLElement>,
private focusTrapFactory: ConfigurableFocusTrapFactory,
public modalRef: ModalRef,
) {}
ngAfterViewInit() {
this.loadChildComponent(this.childComponentType);
if (this.setComponentParameters != null) {
this.setComponentParameters(this.componentRef.instance);
}
this.cd.detectChanges();
this.modalRef.created(this.el.nativeElement);
this.focusTrap = this.focusTrapFactory.create(
this.el.nativeElement.querySelector(".modal-dialog"),
);
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
this.focusTrap.focusFirstTabbableElementWhenReady();
}
}
loadChildComponent(componentType: Type<any>) {
const componentFactory = this.modalService.resolveComponentFactory(componentType);
this.modalContentRef.clear();
this.componentRef = this.modalContentRef.createComponent(componentFactory);
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
}
this.focusTrap.destroy();
}
close() {
this.modalRef.close();
}
getFocus() {
const autoFocusEl = this.el.nativeElement.querySelector("[appAutoFocus]") as HTMLElement;
autoFocusEl?.focus();
}
}

View File

@@ -0,0 +1,20 @@
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(
private _parentInjector: Injector,
private _additionalTokens: WeakMap<any, any>,
) {}
get<T>(
token: ProviderToken<T>,
notFoundValue: undefined,
options: InjectOptions & { optional?: false },
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
}
}

View File

@@ -0,0 +1,49 @@
import { lastValueFrom, Observable, Subject } from "rxjs";
export class ModalRef {
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
onClose: Observable<any>; // Initiated close.
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
onShow: Observable<void>; // Start showing modal
onShown: Observable<void>; // Modal is fully visible
private readonly _onCreated = new Subject<HTMLElement>();
private readonly _onClose = new Subject<any>();
private readonly _onClosed = new Subject<any>();
private readonly _onShow = new Subject<void>();
private readonly _onShown = new Subject<void>();
private lastResult: any;
constructor() {
this.onCreated = this._onCreated.asObservable();
this.onClose = this._onClose.asObservable();
this.onClosed = this._onClosed.asObservable();
this.onShow = this._onShow.asObservable();
this.onShown = this._onShow.asObservable();
}
show() {
this._onShow.next();
}
shown() {
this._onShown.next();
}
close(result?: any) {
this.lastResult = result;
this._onClose.next(result);
}
closed() {
this._onClosed.next(this.lastResult);
}
created(el: HTMLElement) {
this._onCreated.next(el);
}
onClosedPromise(): Promise<any> {
return lastValueFrom(this.onClosed);
}
}

View File

@@ -0,0 +1,101 @@
import { CommonModule } from "@angular/common";
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
@if (options().closeButton) {
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
}
<div class="icon">
<i></i>
</div>
<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>
}
`,
styles: `
:host {
&.toast-in {
animation: toast-animation var(--animation-duration) var(--animation-easing);
}
&.toast-out {
animation: toast-animation var(--animation-duration) var(--animation-easing) reverse
forwards;
}
}
@keyframes toast-animation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`,
preserveWhitespaces: false,
standalone: false,
})
export class BitwardenToast extends Toast {}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,
toastComponent: BitwardenToast,
};
@NgModule({
imports: [CommonModule],
declarations: [BitwardenToast],
exports: [BitwardenToast],
})
export class BitwardenToastModule {
static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> {
return {
ngModule: BitwardenToastModule,
providers: [
{
provide: TOAST_CONFIG,
useValue: {
default: BitwardenToastGlobalConfig,
config: config,
},
},
],
};
}
}

View File

@@ -0,0 +1,27 @@
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
@Directive({
selector: "[appA11yTitle]",
standalone: false,
})
export class A11yTitleDirective {
@Input() set appA11yTitle(title: string) {
this.title = title;
}
private title: string;
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {}
ngOnInit() {
if (!this.el.nativeElement.hasAttribute("title")) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
}
if (!this.el.nativeElement.hasAttribute("aria-label")) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
}
}
}

View File

@@ -0,0 +1,50 @@
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
import { ValidationService } from "@/src-gui/angular/services/validation.service";
/**
* Provides error handling, in particular for any error returned by the server in an api call.
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
* e.g. <form [appApiAction]="this.formPromise">
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
*/
@Directive({
selector: "[appApiAction]",
standalone: false,
})
export class ApiActionDirective implements OnChanges {
@Input() appApiAction: Promise<any>;
constructor(
private el: ElementRef,
private validationService: ValidationService,
private logService: LogService,
) {}
ngOnChanges(changes: any) {
if (this.appApiAction == null || this.appApiAction.then == null) {
return;
}
this.el.nativeElement.loading = true;
this.appApiAction.then(
(response: any) => {
this.el.nativeElement.loading = false;
},
(e: any) => {
this.el.nativeElement.loading = false;
if ((e as ErrorResponse).captchaRequired) {
this.logService.error("Captcha required error response: " + e.getSingleMessage());
return;
}
this.logService?.error(`Received API exception: ${e}`);
this.validationService.showError(e);
},
);
}
}

View File

@@ -0,0 +1,31 @@
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
import { take } from "rxjs";
import { Utils } from "@/jslib/common/src/misc/utils";
@Directive({
selector: "[appAutofocus]",
standalone: false,
})
export class AutofocusDirective {
@Input() set appAutofocus(condition: boolean | string) {
this.autofocus = condition === "" || condition === true;
}
private autofocus: boolean;
constructor(
private el: ElementRef,
private ngZone: NgZone,
) {}
ngOnInit() {
if (!Utils.isMobileBrowser && this.autofocus) {
if (this.ngZone.isStable) {
this.el.nativeElement.focus();
} else {
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus());
}
}
}
}

View File

@@ -0,0 +1,13 @@
import { Directive, ElementRef, HostListener } from "@angular/core";
@Directive({
selector: "[appBlurClick]",
standalone: false,
})
export class BlurClickDirective {
constructor(private el: ElementRef) {}
@HostListener("click") onClick() {
this.el.nativeElement.blur();
}
}

View File

@@ -0,0 +1,60 @@
import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
@Directive({
selector: "[appBoxRow]",
standalone: false,
})
export class BoxRowDirective implements OnInit {
el: HTMLElement = null;
formEls: Element[];
constructor(elRef: ElementRef) {
this.el = elRef.nativeElement;
}
ngOnInit(): void {
this.formEls = Array.from(
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea'),
);
this.formEls.forEach((formEl) => {
formEl.addEventListener(
"focus",
() => {
this.el.classList.add("active");
},
false,
);
formEl.addEventListener(
"blur",
() => {
this.el.classList.remove("active");
},
false,
);
});
}
@HostListener("click", ["$event"]) onClick(event: Event) {
const target = event.target as HTMLElement;
if (
target !== this.el &&
!target.classList.contains("progress") &&
!target.classList.contains("progress-bar")
) {
return;
}
if (this.formEls.length > 0) {
const formEl = this.formEls[0] as HTMLElement;
if (formEl.tagName.toLowerCase() === "input") {
const inputEl = formEl as HTMLInputElement;
if (inputEl.type != null && inputEl.type.toLowerCase() === "checkbox") {
inputEl.click();
return;
}
}
formEl.focus();
}
}
}

View File

@@ -0,0 +1,15 @@
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({
selector: "[appFallbackSrc]",
standalone: false,
})
export class FallbackSrcDirective {
@Input("appFallbackSrc") appFallbackSrc: string;
constructor(private el: ElementRef) {}
@HostListener("error") onError() {
this.el.nativeElement.src = this.appFallbackSrc;
}
}

View File

@@ -0,0 +1,11 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopClick]",
standalone: false,
})
export class StopClickDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.preventDefault();
}
}

View File

@@ -0,0 +1,11 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopProp]",
standalone: false,
})
export class StopPropDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.stopPropagation();
}
}

View File

@@ -0,0 +1,15 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
@Pipe({
name: "i18n",
standalone: false,
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Binary file not shown.

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