mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-20 19:33:31 +00:00
Compare commits
2 Commits
rc
...
context-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22fe0bc482 | ||
|
|
5b1dd63c49 |
@@ -1,203 +1,706 @@
|
|||||||
# Bitwarden Directory Connector
|
# Bitwarden Directory Connector - Claude Code Configuration
|
||||||
|
|
||||||
## Project Overview
|
Sync users and groups from enterprise directory services (LDAP, Entra ID, Google Workspace, Okta, OneLogin) to Bitwarden organizations. Available as both a desktop GUI (Electron + Angular) and a CLI tool (`bwdc`).
|
||||||
|
|
||||||
Directory Connector is a TypeScript application that synchronizes users and groups from directory services to Bitwarden organizations. It provides both a desktop GUI (built with Angular and Electron) and a CLI tool (bwdc).
|
## Overview
|
||||||
|
|
||||||
**Supported Directory Services:**
|
### What This Project Does
|
||||||
|
|
||||||
- LDAP (Lightweight Directory Access Protocol) - includes Active Directory and general LDAP servers
|
- Connects to enterprise identity providers and retrieves user/group membership data
|
||||||
- Microsoft Entra ID (formerly Azure Active Directory)
|
- Syncs that data to Bitwarden organizations via the Directory Connector API
|
||||||
- Google Workspace
|
- Provides both a desktop GUI application (Electron) and a command-line interface (`bwdc`)
|
||||||
- Okta
|
|
||||||
- OneLogin
|
|
||||||
|
|
||||||
**Technologies:**
|
### Key Concepts
|
||||||
|
|
||||||
- TypeScript
|
- **Directory Service**: An identity provider (LDAP, Entra ID, GSuite, Okta, OneLogin) that stores users and groups
|
||||||
- Angular (GUI)
|
- **Sync**: The process of fetching entries from a directory and importing them to Bitwarden
|
||||||
- Electron (Desktop wrapper)
|
- **Delta Sync**: Incremental synchronization that only fetches changes since the last sync
|
||||||
- Node
|
- **Entry**: Base class for `UserEntry` and `GroupEntry` - the core data models
|
||||||
- Jest for testing
|
- **Force Sync**: Ignores delta tokens and fetches all entries fresh
|
||||||
|
- **Test Mode**: Simulates sync without making API calls or updating state
|
||||||
|
|
||||||
## Code Architecture & Structure
|
---
|
||||||
|
|
||||||
### Directory Organization
|
## Architecture & Patterns
|
||||||
|
|
||||||
|
### System Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
User Request (GUI/CLI)
|
||||||
├── abstractions/ # Interface definitions (e.g., IDirectoryService)
|
↓
|
||||||
├── services/ # Business logic implementations for directory services, sync, auth
|
┌───────────────────────────────────┐
|
||||||
├── models/ # Data models (UserEntry, GroupEntry, etc.)
|
│ Entry Points │
|
||||||
├── commands/ # CLI command implementations
|
│ main.ts (GUI) │ bwdc.ts (CLI) │
|
||||||
├── app/ # Angular GUI components
|
└───────────────────────────────────┘
|
||||||
└── utils/ # Test utilities and fixtures
|
↓
|
||||||
|
┌───────────────────────────────────┐
|
||||||
src-cli/ # CLI-specific code (imports common code from src/)
|
│ SyncService │
|
||||||
|
│ Orchestrates the sync flow │
|
||||||
jslib/ # Legacy folder structure (mix of deprecated/unused and current code - new code should not be added here)
|
└───────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────────┐
|
||||||
|
│ DirectoryFactoryService │
|
||||||
|
│ Creates appropriate IDirectory │
|
||||||
|
└───────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Directory Services │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
|
||||||
|
│ │ LDAP │ │ EntraID │ │ GSuite │ │ Okta/1Login │ │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────────┐
|
||||||
|
│ [GroupEntry[], UserEntry[]]│
|
||||||
|
└───────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────────┐
|
||||||
|
│ RequestBuilder (Batched) │
|
||||||
|
│ SingleRequestBuilder (<2000) │
|
||||||
|
│ BatchRequestBuilder (>2000) │
|
||||||
|
└───────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────────┐
|
||||||
|
│ Bitwarden API │
|
||||||
|
│ POST /import endpoint │
|
||||||
|
└───────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Architectural Patterns
|
|
||||||
|
|
||||||
1. **Abstractions = Interfaces**: All interfaces are defined in `/abstractions`
|
|
||||||
2. **Services = Business Logic**: Implementations live in `/services`
|
|
||||||
3. **Directory Service Pattern**: Each directory provider implements `IDirectoryService` interface
|
|
||||||
4. **Separation of Concerns**: GUI (Angular app) and CLI (commands) share the same service layer
|
|
||||||
|
|
||||||
## Development Conventions
|
|
||||||
|
|
||||||
### Code Organization
|
### Code Organization
|
||||||
|
|
||||||
**File Naming:**
|
```
|
||||||
|
src/
|
||||||
|
├── abstractions/ # Interface definitions (IDirectoryService, etc.)
|
||||||
|
├── app/ # Angular GUI components
|
||||||
|
│ ├── tabs/ # Tab-based navigation (Dashboard, Settings, More)
|
||||||
|
│ └── services/ # Angular service providers
|
||||||
|
├── commands/ # CLI command implementations
|
||||||
|
├── enums/ # TypeScript enums (DirectoryType, etc.)
|
||||||
|
├── models/ # Data models (Entry, UserEntry, GroupEntry)
|
||||||
|
├── services/ # Business logic implementations
|
||||||
|
│ └── directory-services/ # One service per directory provider
|
||||||
|
├── bwdc.ts # CLI entry point
|
||||||
|
├── main.ts # Electron main process entry point
|
||||||
|
└── program.ts # CLI command routing (Commander.js)
|
||||||
|
|
||||||
- kebab-case for files: `ldap-directory.service.ts`
|
jslib/ # Legacy shared libraries (do not add new code here)
|
||||||
- Descriptive names that reflect purpose
|
utils/ # Integration test fixtures
|
||||||
|
└── openldap/ # Docker configs, test data, certificates
|
||||||
|
```
|
||||||
|
|
||||||
**Class/Function Naming:**
|
### Key Principles
|
||||||
|
|
||||||
- PascalCase for classes and interfaces
|
1. **Shared Service Layer**: GUI (Angular) and CLI share identical service implementations
|
||||||
- camelCase for functions and variables
|
2. **Factory Pattern**: `DirectoryFactoryService` instantiates the correct `IDirectoryService` based on `DirectoryType`
|
||||||
- Descriptive names that indicate purpose
|
3. **Secure Storage**: Credentials stored in system keychain via `KeytarSecureStorageService`
|
||||||
|
4. **Delta Tracking**: Incremental sync via delta tokens to minimize API calls
|
||||||
|
|
||||||
**File Structure:**
|
### Core Patterns
|
||||||
|
|
||||||
- Keep files focused on single responsibility
|
#### Directory Service Pattern
|
||||||
- Create new service files for distinct directory integrations
|
|
||||||
- Separate models into individual files when complex
|
|
||||||
|
|
||||||
### TypeScript Conventions
|
**Purpose**: Abstract different identity providers behind a common interface
|
||||||
|
|
||||||
**Import Patterns:**
|
**Interface** (`src/abstractions/directory.service.ts`):
|
||||||
|
|
||||||
- Use path aliases (`@/`) for project imports
|
```typescript
|
||||||
- `@/` - project root
|
export interface IDirectoryService {
|
||||||
- `@/jslib/` - jslib folder
|
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||||
- ESLint enforces alphabetized import ordering with newlines between groups
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Type Safety:**
|
**Implementations** in `src/services/directory-services/`:
|
||||||
|
|
||||||
- Avoid `any` types - use proper typing or `unknown` with type guards
|
- `ldap-directory.service.ts` - LDAP/Active Directory
|
||||||
- Prefer interfaces for contracts, types for unions/intersections
|
- `entra-id-directory.service.ts` - Microsoft Entra ID (Azure AD)
|
||||||
- Use strict null checks - handle `null` and `undefined` explicitly
|
- `gsuite-directory.service.ts` - Google Workspace
|
||||||
- Leverage TypeScript's type inference where appropriate
|
- `okta-directory.service.ts` - Okta
|
||||||
|
- `onelogin-directory.service.ts` - OneLogin
|
||||||
|
|
||||||
**Configuration:**
|
**Factory** (`src/services/directory-factory.service.ts`):
|
||||||
|
|
||||||
- Use configuration files or environment variables
|
```typescript
|
||||||
- Never hardcode URLs or configuration values
|
createService(type: DirectoryType): IDirectoryService
|
||||||
|
```
|
||||||
|
|
||||||
## Security Best Practices
|
#### State Service Pattern
|
||||||
|
|
||||||
**Credential Handling:**
|
**Purpose**: Manage persistent state and credential storage
|
||||||
|
|
||||||
- Never log directory service credentials, API keys, or tokens
|
**Implementation** (`src/services/state.service.ts`):
|
||||||
- Use secure storage mechanisms for sensitive data
|
|
||||||
- Credentials should never be hardcoded
|
|
||||||
- Store credentials encrypted, never in plain text
|
|
||||||
|
|
||||||
**Sensitive Data:**
|
- Configuration and sync settings stored in LowDB (JSON file)
|
||||||
|
- Sensitive data (passwords, API keys) stored in system keychain
|
||||||
|
- File locking via `proper-lockfile` to prevent concurrent access corruption
|
||||||
|
- Platform-specific app data directories:
|
||||||
|
- macOS: `~/Library/Application Support/Bitwarden Directory Connector`
|
||||||
|
- Windows: `%APPDATA%/Bitwarden Directory Connector`
|
||||||
|
- Linux: `~/.config/Bitwarden Directory Connector` or `$XDG_CONFIG_HOME`
|
||||||
|
|
||||||
- User and group data from directories should be handled securely
|
---
|
||||||
- Avoid exposing sensitive information in error messages
|
|
||||||
- Sanitize data before logging
|
|
||||||
- Be cautious with data persistence
|
|
||||||
|
|
||||||
**Input Validation:**
|
## Development Guide
|
||||||
|
|
||||||
- Validate and sanitize data from external directory services
|
### Adding a New Directory Service
|
||||||
- Check for injection vulnerabilities (LDAP injection, etc.)
|
|
||||||
- Validate configuration inputs from users
|
|
||||||
|
|
||||||
**API Security:**
|
**1. Create the enum value** (`src/enums/directoryType.ts`)
|
||||||
|
|
||||||
- Ensure authentication flows are implemented correctly
|
```typescript
|
||||||
- Verify SSL/TLS is used for all external connections
|
export enum DirectoryType {
|
||||||
- Check for secure token storage and refresh mechanisms
|
Ldap = 0,
|
||||||
|
EntraID = 1,
|
||||||
|
GSuite = 2,
|
||||||
|
Okta = 3,
|
||||||
|
OneLogin = 4,
|
||||||
|
NewProvider = 5, // Add here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Error Handling
|
**2. Create the configuration model** (`src/models/newProviderConfiguration.ts`)
|
||||||
|
|
||||||
**Best Practices:**
|
```typescript
|
||||||
|
export class NewProviderConfiguration {
|
||||||
|
apiUrl: string;
|
||||||
|
apiToken: string;
|
||||||
|
// Provider-specific settings
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
1. **Try-catch for async operations** - Always wrap external API calls
|
**3. Implement the directory service** (`src/services/directory-services/newprovider-directory.service.ts`)
|
||||||
2. **Meaningful error messages** - Provide context for debugging
|
|
||||||
3. **Error propagation** - Don't swallow errors silently
|
|
||||||
4. **User-facing errors** - Separate user messages from developer logs
|
|
||||||
|
|
||||||
## Performance Best Practices
|
```typescript
|
||||||
|
import { IDirectoryService } from "@/src/abstractions/directory.service";
|
||||||
|
import { GroupEntry } from "@/src/models/groupEntry";
|
||||||
|
import { UserEntry } from "@/src/models/userEntry";
|
||||||
|
import { BaseDirectoryService } from "./base-directory.service";
|
||||||
|
|
||||||
**Large Dataset Handling:**
|
export class NewProviderDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
|
constructor(
|
||||||
|
private logService: LogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private stateService: StateService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
- Use pagination for large user/group lists
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
- Avoid loading entire datasets into memory at once
|
const config = await this.stateService.getDirectory<NewProviderConfiguration>(
|
||||||
- Consider streaming or batch processing for large operations
|
DirectoryType.NewProvider,
|
||||||
|
);
|
||||||
|
const syncConfig = await this.stateService.getSync();
|
||||||
|
|
||||||
**API Rate Limiting:**
|
const groups: GroupEntry[] = [];
|
||||||
|
const users: UserEntry[] = [];
|
||||||
|
|
||||||
- Respect rate limits for Microsoft Graph API, Google Admin SDK, etc.
|
// Fetch from provider API
|
||||||
- Consider batching large API calls where necessary
|
// Apply filters using inherited filter methods
|
||||||
|
|
||||||
**Memory Management:**
|
return [groups, users];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Close connections and clean up resources
|
**4. Register in the factory** (`src/services/directory-factory.service.ts`)
|
||||||
- Remove event listeners when components are destroyed
|
|
||||||
- Be cautious with caching large datasets
|
```typescript
|
||||||
|
case DirectoryType.NewProvider:
|
||||||
|
return new NewProviderDirectoryService(
|
||||||
|
this.logService,
|
||||||
|
this.i18nService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Add state service support** (`src/services/state.service.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add to secure storage keys if credentials involved
|
||||||
|
// Add configuration getter/setter methods
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. Write tests** (`src/services/directory-services/newprovider-directory.service.spec.ts`)
|
||||||
|
|
||||||
|
### Common Patterns
|
||||||
|
|
||||||
|
#### Error Handling with State Rollback
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
|
// Store initial state for rollback
|
||||||
|
const startingUserDelta = await this.stateService.getUserDelta();
|
||||||
|
const startingGroupDelta = await this.stateService.getGroupDelta();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform sync operations
|
||||||
|
const [groups, users] = await this.directoryService.getEntries(force, test);
|
||||||
|
// ... process and submit
|
||||||
|
return [groups, users];
|
||||||
|
} catch (e) {
|
||||||
|
if (!test) {
|
||||||
|
// Rollback deltas on failure
|
||||||
|
await this.stateService.setUserDelta(startingUserDelta);
|
||||||
|
await this.stateService.setGroupDelta(startingGroupDelta);
|
||||||
|
}
|
||||||
|
this.messagingService.send("dirSyncCompleted", { successfully: false });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Filter Processing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In BaseDirectoryService
|
||||||
|
protected buildIncludeSet(filter: string): Set<string> {
|
||||||
|
// Parse filter like "include:user1@example.com,user2@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildExcludeSet(filter: string): Set<string> {
|
||||||
|
// Parse filter like "exclude:user1@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldIncludeUser(user: UserEntry, include: Set<string>, exclude: Set<string>): boolean {
|
||||||
|
if (exclude.has(user.email)) return false;
|
||||||
|
if (include.size === 0) return true;
|
||||||
|
return include.has(user.email);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Desktop GUI (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run rebuild # Rebuild native modules (keytar)
|
||||||
|
npm run electron # Run GUI with hot reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the CLI (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build:cli:watch # Build CLI with watch mode
|
||||||
|
node ./build-cli/bwdc.js --help # Run CLI commands
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Core Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Base entry class (src/models/entry.ts)
|
||||||
|
abstract class Entry {
|
||||||
|
referenceId: string; // Unique ID within the directory (e.g., DN for LDAP)
|
||||||
|
externalId: string; // ID used for Bitwarden import
|
||||||
|
}
|
||||||
|
|
||||||
|
// User entry (src/models/userEntry.ts)
|
||||||
|
class UserEntry extends Entry {
|
||||||
|
email: string;
|
||||||
|
disabled: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group entry (src/models/groupEntry.ts)
|
||||||
|
class GroupEntry extends Entry {
|
||||||
|
name: string;
|
||||||
|
userMemberExternalIds: Set<string>; // External IDs of member users
|
||||||
|
groupMemberReferenceIds: Set<string>; // Reference IDs of nested groups
|
||||||
|
users: UserEntry[]; // Populated for display/simulation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directory Type Enum
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/enums/directoryType.ts
|
||||||
|
enum DirectoryType {
|
||||||
|
Ldap = 0,
|
||||||
|
EntraID = 1,
|
||||||
|
GSuite = 2,
|
||||||
|
Okta = 3,
|
||||||
|
OneLogin = 4,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Models
|
||||||
|
|
||||||
|
Each directory provider has a configuration class in `src/models/`:
|
||||||
|
|
||||||
|
- `LdapConfiguration` - hostname, port, SSL/TLS, bind credentials, auth mode
|
||||||
|
- `EntraIdConfiguration` - tenant, client ID, secret key
|
||||||
|
- `GSuiteConfiguration` - domain, admin user, client email, private key
|
||||||
|
- `OktaConfiguration` - organization URL, API token
|
||||||
|
- `OneLoginConfiguration` - client ID, client secret, region
|
||||||
|
|
||||||
|
### Sync Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/models/syncConfiguration.ts
|
||||||
|
interface SyncConfiguration {
|
||||||
|
users: boolean; // Sync users
|
||||||
|
groups: boolean; // Sync groups
|
||||||
|
interval: number; // Minutes between syncs (minimum 5)
|
||||||
|
userFilter: string; // Include/exclude filter
|
||||||
|
groupFilter: string; // Include/exclude filter
|
||||||
|
removeDisabled: boolean; // Remove disabled users from org
|
||||||
|
overwriteExisting: boolean; // Overwrite existing entries
|
||||||
|
largeImport: boolean; // Enable for >2000 entries
|
||||||
|
// LDAP-specific
|
||||||
|
groupObjectClass: string;
|
||||||
|
userObjectClass: string;
|
||||||
|
groupPath: string;
|
||||||
|
userPath: string;
|
||||||
|
// ... additional LDAP attributes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Configuration
|
||||||
|
|
||||||
|
### Security Rules
|
||||||
|
|
||||||
|
**MANDATORY - These rules have no exceptions:**
|
||||||
|
|
||||||
|
1. **Never log credentials**: API keys, passwords, tokens, and secrets must never appear in logs
|
||||||
|
2. **Never hardcode secrets**: All URLs, credentials, and sensitive data must come from configuration
|
||||||
|
3. **Use KeytarSecureStorageService**: All credentials must be stored in the system keychain
|
||||||
|
4. **Validate external data**: Sanitize all data received from directory services
|
||||||
|
5. **LDAP injection prevention**: Be cautious with user-provided LDAP filters
|
||||||
|
|
||||||
|
### Secure Storage Keys
|
||||||
|
|
||||||
|
The following are stored in the system keychain (not plain JSON):
|
||||||
|
|
||||||
|
- `ldapPassword` - LDAP bind password
|
||||||
|
- `gsuitePrivateKey` - Google Workspace private key
|
||||||
|
- `entraKey` - Microsoft Entra ID client secret
|
||||||
|
- `oktaToken` - Okta API token
|
||||||
|
- `oneLoginClientSecret` - OneLogin client secret
|
||||||
|
- User/group delta tokens
|
||||||
|
- Sync hashes
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description | Example |
|
||||||
|
| ------------------------------------------ | -------- | ---------------------------------------- | -------------------- |
|
||||||
|
| `BITWARDENCLI_CONNECTOR_APPDATA_DIR` | No | CLI app data directory override | `/custom/path` |
|
||||||
|
| `BITWARDEN_CONNECTOR_APPDATA_DIR` | No | GUI app data directory override | `/custom/path` |
|
||||||
|
| `BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS` | No | Store secrets in plain text (debug only) | `true` |
|
||||||
|
| `BITWARDENCLI_CONNECTOR_DEBUG` | No | Enable debug logging | `true` |
|
||||||
|
| `BW_CLIENTID` | No | CLI login client ID | `organization.xxxxx` |
|
||||||
|
| `BW_CLIENTSECRET` | No | CLI login client secret | `xxxxx` |
|
||||||
|
| `BW_NOINTERACTION` | No | Disable interactive prompts | `true` |
|
||||||
|
| `BW_PRETTY` | No | Pretty-print JSON output | `true` |
|
||||||
|
| `BW_RAW` | No | Raw output (no formatting) | `true` |
|
||||||
|
| `BW_RESPONSE` | No | JSON response format | `true` |
|
||||||
|
| `BW_QUIET` | No | Suppress stdout | `true` |
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
|
||||||
|
- **API Token Authentication**: Uses organization `clientId` + `clientSecret`
|
||||||
|
- **Token Storage**: Access tokens and refresh tokens stored securely via Keytar
|
||||||
|
- **Token Refresh**: Automatic refresh when access token expires
|
||||||
|
- **Auth Service**: `src/services/auth.service.ts` handles the authentication flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
**Framework:**
|
### Test Structure
|
||||||
|
|
||||||
- Jest with jest-preset-angular
|
```
|
||||||
- jest-mock-extended for type-safe mocks with `mock<Type>()`
|
src/
|
||||||
|
├── services/
|
||||||
|
│ ├── sync.service.spec.ts # Unit tests (colocated)
|
||||||
|
│ ├── sync.service.integration.spec.ts # Integration tests
|
||||||
|
│ └── directory-services/
|
||||||
|
│ ├── ldap-directory.service.spec.ts
|
||||||
|
│ └── ldap-directory.service.integration.spec.ts
|
||||||
|
utils/
|
||||||
|
└── openldap/
|
||||||
|
├── config-fixtures.ts # Test configuration helpers
|
||||||
|
├── user-fixtures.ts # Expected user data
|
||||||
|
├── group-fixtures.ts # Expected group data
|
||||||
|
├── certs/ # TLS certificates
|
||||||
|
└── docker-compose.yml # LDAP container config
|
||||||
|
```
|
||||||
|
|
||||||
**Test Organization:**
|
### Writing Tests
|
||||||
|
|
||||||
- Tests colocated with source files
|
**Unit Test Template**:
|
||||||
- `*.spec.ts` - Unit tests for individual components/services
|
|
||||||
- `*.integration.spec.ts` - Integration tests against live directory services
|
|
||||||
- Test helpers located in `utils/` directory
|
|
||||||
|
|
||||||
**Test Naming:**
|
```typescript
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
- Descriptive, human-readable test names
|
describe("ServiceName", () => {
|
||||||
- Example: `'should return empty array when no users exist in directory'`
|
let logService: MockProxy<LogService>;
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
let service: ServiceUnderTest;
|
||||||
|
|
||||||
**Test Coverage:**
|
beforeEach(() => {
|
||||||
|
logService = mock();
|
||||||
|
stateService = mock();
|
||||||
|
service = new ServiceUnderTest(logService, stateService);
|
||||||
|
});
|
||||||
|
|
||||||
- New features must include tests
|
it("should do something", async () => {
|
||||||
- Bug fixes should include regression tests
|
// Arrange
|
||||||
- Changes to core sync logic or directory specific logic require integration tests
|
stateService.getSomeValue.mockResolvedValue(expectedValue);
|
||||||
|
|
||||||
**Testing Approach:**
|
// Act
|
||||||
|
const result = await service.doSomething();
|
||||||
|
|
||||||
- **Unit tests**: Mock external API calls using jest-mock-extended
|
// Assert
|
||||||
- **Integration tests**: Use live directory services (Docker containers or configured cloud services)
|
expect(result).toEqual(expectedResult);
|
||||||
- Focus on critical paths (authentication, sync, data transformation)
|
});
|
||||||
- Test error scenarios and edge cases (empty results, malformed data, connection failures), not just happy paths
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Directory Service Patterns
|
**Integration Test Template** (see `ldap-directory.service.integration.spec.ts`):
|
||||||
|
|
||||||
### IDirectoryService Interface
|
```typescript
|
||||||
|
// Requires Docker containers running
|
||||||
|
// npm run test:integration:setup
|
||||||
|
|
||||||
All directory services implement this core interface with methods:
|
describe("ldapDirectoryService", () => {
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
let directoryService: LdapDirectoryService;
|
||||||
|
|
||||||
- `getUsers()` - Retrieve users from directory and transform them into standard objects
|
beforeEach(() => {
|
||||||
- `getGroups()` - Retrieve groups from directory and transform them into standard objects
|
stateService = mock();
|
||||||
- Connection and authentication handling
|
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
||||||
|
stateService.getDirectory
|
||||||
|
.calledWith(DirectoryType.Ldap)
|
||||||
|
.mockResolvedValue(getLdapConfiguration());
|
||||||
|
});
|
||||||
|
|
||||||
### Service-Specific Implementations
|
it("syncs users and groups", async () => {
|
||||||
|
const result = await directoryService.getEntries(true, true);
|
||||||
|
expect(result).toEqual([groupFixtures, userFixtures]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
Each directory service has unique authentication and query patterns:
|
### Running Tests
|
||||||
|
|
||||||
- **LDAP**: Direct LDAP queries, bind authentication
|
```bash
|
||||||
- **Microsoft Entra ID**: Microsoft Graph API, OAuth tokens
|
npm test # All unit tests (excludes integration)
|
||||||
- **Google Workspace**: Google Admin SDK, service account credentials
|
npm test -- path/to/file.spec.ts # Single test file
|
||||||
- **Okta/OneLogin**: REST APIs with API tokens
|
npm run test:watch # Watch mode
|
||||||
|
|
||||||
|
# Integration tests
|
||||||
|
npm run test:integration:setup # Start Docker containers
|
||||||
|
npm run test:integration # Run integration tests
|
||||||
|
npm run test:integration:watch # Watch mode for integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Environment
|
||||||
|
|
||||||
|
- **Mocking**: `jest-mock-extended` with `mock<Type>()` for type-safe mocks
|
||||||
|
- **Alternative**: `@fluffy-spoon/substitute` available for some tests
|
||||||
|
- **Integration**: Docker containers for LDAP (OpenLDAP)
|
||||||
|
- **Fixtures**: Located in `utils/openldap/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style & Standards
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
- **Prettier**: Auto-formatting enforced via pre-commit hooks
|
||||||
|
- **Config**: `.prettierrc` in project root
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- `camelCase` for: variables, functions, method names
|
||||||
|
- `PascalCase` for: classes, interfaces, types, enums
|
||||||
|
- `SCREAMING_SNAKE_CASE` for: constants (rare in this codebase)
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
**Path Aliases:**
|
||||||
|
|
||||||
|
- `@/` maps to project root
|
||||||
|
- Example: `import { SyncService } from "@/src/services/sync.service"`
|
||||||
|
|
||||||
|
**Import Order (ESLint enforced):**
|
||||||
|
|
||||||
|
1. External packages (node_modules)
|
||||||
|
2. jslib imports (`@/jslib/...`)
|
||||||
|
3. Project imports (`@/src/...`)
|
||||||
|
4. Alphabetized within each group with newlines between groups
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// External
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
// jslib
|
||||||
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
|
|
||||||
|
// Project
|
||||||
|
import { DirectoryType } from "@/src/enums/directoryType";
|
||||||
|
import { SyncService } from "@/src/services/sync.service";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
- Avoid unnecessary comments; code should be self-documenting
|
||||||
|
- Use JSDoc only for public APIs that need documentation
|
||||||
|
- Inline comments for complex logic only
|
||||||
|
|
||||||
|
### Pre-commit Hooks
|
||||||
|
|
||||||
|
- **Husky**: Runs `lint-staged` on commit
|
||||||
|
- **lint-staged**: Runs Prettier on all files, ESLint on TypeScript files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint # Check ESLint + Prettier
|
||||||
|
npm run lint:fix # Auto-fix ESLint issues
|
||||||
|
npm run prettier # Auto-format with Prettier
|
||||||
|
npm run test:types # TypeScript type checking
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
### DO
|
||||||
|
|
||||||
|
- ✅ Use `KeytarSecureStorageService` for all credential storage
|
||||||
|
- ✅ Implement `IDirectoryService` interface for new directory providers
|
||||||
|
- ✅ Use the factory pattern via `DirectoryFactoryService`
|
||||||
|
- ✅ Write unit tests with `jest-mock-extended` mocks
|
||||||
|
- ✅ Handle errors with state rollback (delta tokens)
|
||||||
|
- ✅ Use path aliases (`@/src/...`) for imports
|
||||||
|
- ✅ Validate data from external directory services
|
||||||
|
- ✅ Use `force` and `test` parameters consistently in sync methods
|
||||||
|
|
||||||
|
### DON'T
|
||||||
|
|
||||||
|
- ❌ Log credentials, API keys, or tokens
|
||||||
|
- ❌ Hardcode URLs, secrets, or configuration values
|
||||||
|
- ❌ Store sensitive data in LowDB (JSON) - use Keytar
|
||||||
|
- ❌ Skip input validation for LDAP filters (injection risk)
|
||||||
|
- ❌ Use `any` types without explicit justification
|
||||||
|
- ❌ Add new code to `jslib/` (legacy, read-only)
|
||||||
|
- ❌ Ignore delta token rollback on sync failure
|
||||||
|
- ❌ Bypass `overwriteExisting` validation for batch imports (>2000 entries)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
**Desktop GUI (Electron):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # Build main + renderer
|
||||||
|
npm run build:dist # Full distribution build
|
||||||
|
npm run dist:win # Windows installer
|
||||||
|
npm run dist:mac # macOS installer
|
||||||
|
npm run dist:lin # Linux packages (AppImage, RPM)
|
||||||
|
```
|
||||||
|
|
||||||
|
**CLI Tool:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:cli:prod # Production build
|
||||||
|
npm run dist:cli:win # Windows executable
|
||||||
|
npm run dist:cli:mac # macOS executable
|
||||||
|
npm run dist:cli:lin # Linux executable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
Follow semantic versioning: `MAJOR.MINOR.PATCH`
|
||||||
|
|
||||||
|
- Version format: `YYYY.MM.PATCH` (e.g., `2025.12.0`)
|
||||||
|
- Managed in `package.json`
|
||||||
|
|
||||||
|
### Publishing
|
||||||
|
|
||||||
|
- **CI/CD**: GitHub Actions workflows in `.github/workflows/`
|
||||||
|
- **build.yml**: Multi-platform builds with code signing
|
||||||
|
- **release.yml**: Version bumping and publishing
|
||||||
|
- **Code Signing**: Azure Key Vault (Windows), App Store Connect (macOS)
|
||||||
|
- **Auto-update**: Electron Updater for GUI application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### LDAP Connection Failures
|
||||||
|
|
||||||
|
**Problem**: Cannot connect to LDAP server, timeout or connection refused
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Verify hostname and port are correct
|
||||||
|
2. Check SSL/TLS settings match server configuration
|
||||||
|
3. For StartTLS, ensure SSL is enabled and use the non-secure port (389)
|
||||||
|
4. For LDAPS, use port 636 and provide CA certificate path
|
||||||
|
|
||||||
|
#### Keytar/Native Module Issues
|
||||||
|
|
||||||
|
**Problem**: `Error: Module did not self-register` or keytar-related crashes
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run rebuild # Rebuild native modules for current Electron version
|
||||||
|
npm run reset # Full reset of keytar module
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sync Hash Mismatch
|
||||||
|
|
||||||
|
**Problem**: Sync runs but no changes appear in Bitwarden
|
||||||
|
|
||||||
|
**Solution**: The sync service skips if the hash matches the previous sync. Use force sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bwdc sync --force # CLI
|
||||||
|
# Or clear cache
|
||||||
|
bwdc clear-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Large Import Failures
|
||||||
|
|
||||||
|
**Problem**: Sync fails for organizations with >2000 users/groups
|
||||||
|
|
||||||
|
**Solution**: Enable `largeImport` in sync settings. Note: `overwriteExisting` is incompatible with batch mode.
|
||||||
|
|
||||||
|
### Debug Tips
|
||||||
|
|
||||||
|
- Enable debug logging: `BITWARDENCLI_CONNECTOR_DEBUG=true`
|
||||||
|
- View data file location: `bwdc data-file`
|
||||||
|
- Test sync without making changes: `bwdc test`
|
||||||
|
- Check last sync times: `bwdc last-sync users` / `bwdc last-sync groups`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
|
### Official Documentation
|
||||||
- [Contributing Guidelines](https://contributing.bitwarden.com/contributing/)
|
|
||||||
- [Code Style](https://contributing.bitwarden.com/contributing/code-style/)
|
- [Directory Sync CLI Documentation](https://bitwarden.com/help/directory-sync-cli/)
|
||||||
- [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
|
- [Directory Connector Help](https://bitwarden.com/help/directory-sync/)
|
||||||
- [Security Definitions](https://contributing.bitwarden.com/architecture/security/definitions)
|
|
||||||
|
### Internal Documentation
|
||||||
|
|
||||||
|
- [Bitwarden Contributing Guidelines](https://contributing.bitwarden.com/contributing/)
|
||||||
|
- [Code Style Guide](https://contributing.bitwarden.com/contributing/code-style/)
|
||||||
|
|
||||||
|
### Tools & Libraries
|
||||||
|
|
||||||
|
- [ldapts](https://github.com/ldapts/ldapts) - LDAP client for Node.js
|
||||||
|
- [Keytar](https://github.com/atom/node-keytar) - Native keychain access
|
||||||
|
- [Commander.js](https://github.com/tj/commander.js) - CLI framework
|
||||||
|
- [LowDB](https://github.com/typicode/lowdb) - JSON database
|
||||||
|
- [Microsoft Graph Client](https://github.com/microsoftgraph/msgraph-sdk-javascript) - Entra ID API
|
||||||
|
- [Google APIs](https://github.com/googleapis/google-api-nodejs-client) - GSuite API
|
||||||
|
|||||||
30
.claude/commands/code-explainer.md
Normal file
30
.claude/commands/code-explainer.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
description: "Provides a brief explanation of the code attached, including key components, notable patterns, and a code walkthrough."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Explainer
|
||||||
|
|
||||||
|
Provide a brief explanation of the code attached. I'm trying to better understand it.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
- Main classes/functions and their roles
|
||||||
|
- Important dependencies
|
||||||
|
- Critical flows
|
||||||
|
|
||||||
|
## Notable Patterns
|
||||||
|
|
||||||
|
- Design patterns used
|
||||||
|
- Architecture decisions
|
||||||
|
- Important abstractions
|
||||||
|
|
||||||
|
## Code Walkthrough
|
||||||
|
|
||||||
|
- How it works
|
||||||
|
- Key decision points
|
||||||
|
- Important considerations
|
||||||
|
|
||||||
|
## Gotchas & Tips
|
||||||
|
|
||||||
|
- Edge cases to watch for
|
||||||
|
- Performance considerations
|
||||||
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -129,12 +129,12 @@ jobs:
|
|||||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ jobs:
|
|||||||
choco install checksum --no-progress
|
choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -279,12 +279,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -379,12 +379,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -439,12 +439,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|||||||
10
.github/workflows/integration-test.yml
vendored
10
.github/workflows/integration-test.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
id: report
|
id: report
|
||||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
||||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
||||||
# PRs from the repository and all other events are OK.
|
# PRs from the repository and all other events are OK.
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
||||||
@@ -143,6 +143,4 @@ jobs:
|
|||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
|
|
||||||
- name: Upload results to codecov.io
|
- name: Upload results to codecov.io
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
|
||||||
with:
|
|
||||||
report_type: test_results
|
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
release_version: ${{ steps.version.outputs.version }}
|
release_version: ${{ steps.version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
run: npm run test --coverage
|
run: npm run test --coverage
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
||||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
||||||
# PRs from the repository and all other events are OK.
|
# PRs from the repository and all other events are OK.
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
||||||
@@ -67,6 +67,4 @@ jobs:
|
|||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
|
|
||||||
- name: Upload results to codecov.io
|
- name: Upload results to codecov.io
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
|
||||||
with:
|
|
||||||
report_type: test_results
|
|
||||||
|
|||||||
2
.github/workflows/version-bump.yml
vendored
2
.github/workflows/version-bump.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
permission-contents: write
|
permission-contents: write
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
156
ESM_MIGRATION_PLAN.md
Normal file
156
ESM_MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# ESM Migration Plan
|
||||||
|
|
||||||
|
## Migration Status: Partial Success
|
||||||
|
|
||||||
|
The ESM migration has been **partially completed**. The source code is now ESM-compatible with `"type": "module"` in package.json, and webpack outputs CommonJS bundles (`.cjs`) for Node.js compatibility.
|
||||||
|
|
||||||
|
### What Works
|
||||||
|
|
||||||
|
- ✅ CLI build (`bwdc.cjs`) - builds and runs successfully
|
||||||
|
- ✅ Electron main process (`main.cjs`) - builds successfully
|
||||||
|
- ✅ All 130 tests pass
|
||||||
|
- ✅ Source code uses ESM syntax (import/export)
|
||||||
|
|
||||||
|
### What Doesn't Work
|
||||||
|
|
||||||
|
- ❌ Electron renderer build - **pre-existing type errors in jslib** (not caused by this migration)
|
||||||
|
|
||||||
|
The renderer build was failing with 37 TypeScript errors in `jslib/` **before** the ESM migration began. These are ArrayBuffer/SharedArrayBuffer type compatibility issues in the jslib submodule that need to be addressed separately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. package.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"main": "main.cjs"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. tsconfig.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "ES2020",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmitOnError": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Webpack Configurations
|
||||||
|
|
||||||
|
**CLI (webpack.cli.cjs)**
|
||||||
|
|
||||||
|
- Output changed to `.cjs` extension
|
||||||
|
- Added `transpileOnly: true` to ts-loader for faster builds
|
||||||
|
|
||||||
|
**Main (webpack.main.cjs)**
|
||||||
|
|
||||||
|
- Output changed to `.cjs` extension
|
||||||
|
- Added `transpileOnly: true` to ts-loader
|
||||||
|
|
||||||
|
**Renderer (webpack.renderer.cjs)**
|
||||||
|
|
||||||
|
- Created separate `tsconfig.renderer.json` to isolate Angular compilation
|
||||||
|
- Removed ESM output experiments (not compatible with Angular's webpack plugin)
|
||||||
|
|
||||||
|
### 4. src-cli/package.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"bwdc": "../build-cli/bwdc.cjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. New File: tsconfig.renderer.json
|
||||||
|
|
||||||
|
Dedicated TypeScript config for Angular renderer to isolate from jslib type issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Decision
|
||||||
|
|
||||||
|
### Why CJS Output Instead of ESM Output?
|
||||||
|
|
||||||
|
The migration uses a **hybrid approach**:
|
||||||
|
|
||||||
|
- **Source code**: ESM syntax (`import`/`export`)
|
||||||
|
- **Build output**: CommonJS (`.cjs` files)
|
||||||
|
|
||||||
|
This approach was chosen because:
|
||||||
|
|
||||||
|
1. **lowdb v1 incompatibility**: The legacy lowdb v1 used in jslib doesn't work properly with ESM output due to lodash interop issues
|
||||||
|
|
||||||
|
2. **Native module compatibility**: keytar and other native modules work better with CJS
|
||||||
|
|
||||||
|
3. **Electron compatibility**: Electron's main process ESM support is still maturing
|
||||||
|
|
||||||
|
4. **jslib constraints**: The jslib submodule is read-only and contains CJS-only patterns
|
||||||
|
|
||||||
|
The webpack bundler transpiles ESM source to CJS output, giving us modern syntax in the codebase while maintaining runtime compatibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocking Issues for Full ESM
|
||||||
|
|
||||||
|
### 1. jslib Submodule (Read-Only)
|
||||||
|
|
||||||
|
The jslib folder contains:
|
||||||
|
|
||||||
|
- `lowdb` v1.0.0 usage (CJS-only, v7 is ESM but has breaking API changes)
|
||||||
|
- `node-fetch` v2.7.0 usage (CJS-only, v3 is ESM-only)
|
||||||
|
- Pre-existing TypeScript errors (ArrayBuffer type mismatches)
|
||||||
|
|
||||||
|
### 2. Angular Webpack Plugin
|
||||||
|
|
||||||
|
The `@ngtools/webpack` plugin does its own TypeScript compilation and doesn't support `transpileOnly` mode, so it surfaces type errors from jslib.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
To complete full ESM migration:
|
||||||
|
|
||||||
|
1. **Update jslib submodule** - Fix type errors, upgrade to ESM-compatible dependencies
|
||||||
|
2. **Upgrade lowdb** - From v1 to v7 (requires rewriting storage layer)
|
||||||
|
3. **Remove node-fetch** - Use native `fetch` (Node 18+) or upgrade to v3
|
||||||
|
4. **Enable ESM output** - Once dependencies are updated, change webpack output to ESM
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing the Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build CLI
|
||||||
|
npm run build:cli
|
||||||
|
node ./build-cli/bwdc.cjs --help
|
||||||
|
|
||||||
|
# Build Electron main
|
||||||
|
npm run build:main
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
| ------------------------ | ---------------------------------------------------- |
|
||||||
|
| `package.json` | Added `"type": "module"`, changed main to `main.cjs` |
|
||||||
|
| `tsconfig.json` | Added `skipLibCheck`, `noEmitOnError` |
|
||||||
|
| `tsconfig.renderer.json` | New file for Angular compilation |
|
||||||
|
| `webpack.cli.cjs` | Output to `.cjs`, added `transpileOnly` |
|
||||||
|
| `webpack.main.cjs` | Output to `.cjs`, added `transpileOnly` |
|
||||||
|
| `webpack.renderer.cjs` | Use separate tsconfig |
|
||||||
|
| `src-cli/package.json` | Added `"type": "module"`, updated bin path |
|
||||||
10
angular.json
10
angular.json
@@ -18,17 +18,15 @@
|
|||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": {
|
"outputPath": "dist",
|
||||||
"base": "dist"
|
|
||||||
},
|
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
"tsConfig": "tsconfig.json",
|
"tsConfig": "tsconfig.json",
|
||||||
"assets": [],
|
"assets": [],
|
||||||
"styles": [],
|
"styles": [],
|
||||||
"scripts": [],
|
"scripts": []
|
||||||
"browser": "src/main.ts"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,20 @@ module.exports = {
|
|||||||
|
|
||||||
roots: ["<rootDir>"],
|
roots: ["<rootDir>"],
|
||||||
modulePaths: [compilerOptions.baseUrl],
|
modulePaths: [compilerOptions.baseUrl],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
|
moduleNameMapper: {
|
||||||
|
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
|
||||||
|
// ESM compatibility: mock import.meta.url for tests
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
},
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
// Workaround for a memory leak that crashes tests in CI:
|
// Workaround for a memory leak that crashes tests in CI:
|
||||||
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
||||||
// Also anecdotally improves performance when run locally
|
// Also anecdotally improves performance when run locally
|
||||||
maxWorkers: 3,
|
maxWorkers: 3,
|
||||||
|
|
||||||
|
// ESM support
|
||||||
|
extensionsToTreatAsEsm: [".ts"],
|
||||||
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.tsx?$": [
|
"^.+\\.tsx?$": [
|
||||||
"jest-preset-angular",
|
"jest-preset-angular",
|
||||||
@@ -43,6 +50,8 @@ module.exports = {
|
|||||||
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
|
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
|
||||||
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
|
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
|
||||||
isolatedModules: true,
|
isolatedModules: true,
|
||||||
|
// ESM support
|
||||||
|
useESM: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,77 +1,75 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
|
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
|
||||||
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
|
import {
|
||||||
|
DefaultNoComponentGlobalConfig,
|
||||||
|
GlobalConfig,
|
||||||
|
Toast as BaseToast,
|
||||||
|
ToastPackage,
|
||||||
|
ToastrService,
|
||||||
|
TOAST_CONFIG,
|
||||||
|
} from "ngx-toastr";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "[toast-component2]",
|
selector: "[toast-component2]",
|
||||||
template: `
|
template: `
|
||||||
@if (options().closeButton) {
|
<button
|
||||||
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
|
*ngIf="options.closeButton"
|
||||||
<span aria-hidden="true">×</span>
|
(click)="remove()"
|
||||||
</button>
|
type="button"
|
||||||
}
|
class="toast-close-button"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<i></i>
|
<i></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if (title()) {
|
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
|
||||||
<div [class]="options().titleClass" [attr.aria-label]="title()">
|
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
|
||||||
{{ 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>
|
</div>
|
||||||
}
|
<div
|
||||||
`,
|
*ngIf="message && options.enableHtml"
|
||||||
styles: `
|
role="alertdialog"
|
||||||
:host {
|
aria-live="polite"
|
||||||
&.toast-in {
|
[class]="options.messageClass"
|
||||||
animation: toast-animation var(--animation-duration) var(--animation-easing);
|
[innerHTML]="message"
|
||||||
}
|
></div>
|
||||||
|
<div
|
||||||
&.toast-out {
|
*ngIf="message && !options.enableHtml"
|
||||||
animation: toast-animation var(--animation-duration) var(--animation-easing) reverse
|
role="alertdialog"
|
||||||
forwards;
|
aria-live="polite"
|
||||||
}
|
[class]="options.messageClass"
|
||||||
}
|
[attr.aria-label]="message"
|
||||||
|
>
|
||||||
@keyframes toast-animation {
|
{{ message }}
|
||||||
from {
|
</div>
|
||||||
opacity: 0;
|
</div>
|
||||||
}
|
<div *ngIf="options.progressBar">
|
||||||
to {
|
<div class="toast-progress" [style.width]="width + '%'"></div>
|
||||||
opacity: 1;
|
</div>
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger("flyInOut", [
|
||||||
|
state("inactive", style({ opacity: 0 })),
|
||||||
|
state("active", style({ opacity: 1 })),
|
||||||
|
state("removed", style({ opacity: 0 })),
|
||||||
|
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
|
||||||
|
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
|
||||||
|
]),
|
||||||
|
],
|
||||||
preserveWhitespaces: false,
|
preserveWhitespaces: false,
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class BitwardenToast extends Toast {}
|
export class BitwardenToast extends BaseToast {
|
||||||
|
constructor(
|
||||||
|
protected toastrService: ToastrService,
|
||||||
|
public toastPackage: ToastPackage,
|
||||||
|
) {
|
||||||
|
super(toastrService, toastPackage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const BitwardenToastGlobalConfig: GlobalConfig = {
|
export const BitwardenToastGlobalConfig: GlobalConfig = {
|
||||||
...DefaultNoComponentGlobalConfig,
|
...DefaultNoComponentGlobalConfig,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("SymmetricCryptoKey", () => {
|
|||||||
new SymmetricCryptoKey(null);
|
new SymmetricCryptoKey(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(t).toThrow("Must provide key");
|
expect(t).toThrowError("Must provide key");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("guesses encKey from key length", () => {
|
describe("guesses encKey from key length", () => {
|
||||||
@@ -63,7 +63,7 @@ describe("SymmetricCryptoKey", () => {
|
|||||||
new SymmetricCryptoKey(makeStaticByteArray(30));
|
new SymmetricCryptoKey(makeStaticByteArray(30));
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(t).toThrow("Unable to determine encType.");
|
expect(t).toThrowError("Unable to determine encType.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ export function makeStaticByteArray(length: number, start = 0) {
|
|||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
arr[i] = start + i;
|
arr[i] = start + i;
|
||||||
}
|
}
|
||||||
return arr.buffer;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,9 @@ export class NodeUtils {
|
|||||||
.on("error", (err) => reject(err));
|
.on("error", (err) => reject(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/31394257
|
||||||
|
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
|
||||||
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Utils {
|
|||||||
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
|
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
static fromB64ToArray(str: string): Uint8Array {
|
||||||
if (Utils.isNode) {
|
if (Utils.isNode) {
|
||||||
return new Uint8Array(Buffer.from(str, "base64"));
|
return new Uint8Array(Buffer.from(str, "base64"));
|
||||||
} else {
|
} else {
|
||||||
@@ -49,11 +49,11 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromUrlB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
static fromUrlB64ToArray(str: string): Uint8Array {
|
||||||
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
|
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromHexToArray(str: string): Uint8Array<ArrayBuffer> {
|
static fromHexToArray(str: string): Uint8Array {
|
||||||
if (Utils.isNode) {
|
if (Utils.isNode) {
|
||||||
return new Uint8Array(Buffer.from(str, "hex"));
|
return new Uint8Array(Buffer.from(str, "hex"));
|
||||||
} else {
|
} else {
|
||||||
@@ -65,7 +65,7 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromUtf8ToArray(str: string): Uint8Array<ArrayBuffer> {
|
static fromUtf8ToArray(str: string): Uint8Array {
|
||||||
if (Utils.isNode) {
|
if (Utils.isNode) {
|
||||||
return new Uint8Array(Buffer.from(str, "utf8"));
|
return new Uint8Array(Buffer.from(str, "utf8"));
|
||||||
} else {
|
} else {
|
||||||
@@ -78,7 +78,7 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromByteStringToArray(str: string): Uint8Array<ArrayBuffer> {
|
static fromByteStringToArray(str: string): Uint8Array {
|
||||||
const arr = new Uint8Array(str.length);
|
const arr = new Uint8Array(str.length);
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
arr[i] = str.charCodeAt(i);
|
arr[i] = str.charCodeAt(i);
|
||||||
@@ -99,8 +99,8 @@ export class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromBufferToUrlB64(buffer: Uint8Array<ArrayBuffer>): string {
|
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer.buffer));
|
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromB64toUrlB64(b64Str: string) {
|
static fromB64toUrlB64(b64Str: string) {
|
||||||
|
|||||||
@@ -636,9 +636,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
|
|
||||||
const encBytes = new Uint8Array(encBuf);
|
const encBytes = new Uint8Array(encBuf);
|
||||||
const encType = encBytes[0];
|
const encType = encBytes[0];
|
||||||
let ctBytes: Uint8Array<ArrayBuffer> = null;
|
let ctBytes: Uint8Array = null;
|
||||||
let ivBytes: Uint8Array<ArrayBuffer> = null;
|
let ivBytes: Uint8Array = null;
|
||||||
let macBytes: Uint8Array<ArrayBuffer> = null;
|
let macBytes: Uint8Array = null;
|
||||||
|
|
||||||
switch (encType) {
|
switch (encType) {
|
||||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||||
|
|||||||
@@ -127,13 +127,6 @@ export class WindowMain {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable SharedArrayBuffer. See https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
|
|
||||||
this.win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
|
||||||
details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
|
|
||||||
details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
|
|
||||||
callback({ responseHeaders: details.responseHeaders });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.windowStates[mainWindowSizeKey].isMaximized) {
|
if (this.windowStates[mainWindowSizeKey].isMaximized) {
|
||||||
this.win.maximize();
|
this.win.maximize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe("NodeCrypto Function Service", () => {
|
|||||||
it("should fail with prk too small", async () => {
|
it("should fail with prk too small", async () => {
|
||||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
const f = cryptoFunctionService.hkdfExpand(
|
const f = cryptoFunctionService.hkdfExpand(
|
||||||
Utils.fromB64ToArray(prk16Byte).buffer,
|
Utils.fromB64ToArray(prk16Byte),
|
||||||
"info",
|
"info",
|
||||||
32,
|
32,
|
||||||
"sha256",
|
"sha256",
|
||||||
@@ -105,7 +105,7 @@ describe("NodeCrypto Function Service", () => {
|
|||||||
it("should fail with outputByteSize is too large", async () => {
|
it("should fail with outputByteSize is too large", async () => {
|
||||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
const f = cryptoFunctionService.hkdfExpand(
|
const f = cryptoFunctionService.hkdfExpand(
|
||||||
Utils.fromB64ToArray(prk32Byte).buffer,
|
Utils.fromB64ToArray(prk32Byte),
|
||||||
"info",
|
"info",
|
||||||
8161,
|
8161,
|
||||||
"sha256",
|
"sha256",
|
||||||
@@ -341,7 +341,7 @@ function testHkdf(
|
|||||||
utf8Key: string,
|
utf8Key: string,
|
||||||
unicodeKey: string,
|
unicodeKey: string,
|
||||||
) {
|
) {
|
||||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer;
|
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==");
|
||||||
|
|
||||||
const regularSalt = "salt";
|
const regularSalt = "salt";
|
||||||
const utf8Salt = "üser_salt";
|
const utf8Salt = "üser_salt";
|
||||||
@@ -393,7 +393,7 @@ function testHkdfExpand(
|
|||||||
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
|
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
|
||||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
const okm = await cryptoFunctionService.hkdfExpand(
|
const okm = await cryptoFunctionService.hkdfExpand(
|
||||||
Utils.fromB64ToArray(b64prk).buffer,
|
Utils.fromB64ToArray(b64prk),
|
||||||
info,
|
info,
|
||||||
outputByteSize,
|
outputByteSize,
|
||||||
algorithm,
|
algorithm,
|
||||||
|
|||||||
11610
package-lock.json
generated
11610
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@@ -2,7 +2,8 @@
|
|||||||
"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": "2026.2.0",
|
"version": "2025.12.0",
|
||||||
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
"password",
|
"password",
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
"url": "https://github.com/bitwarden/directory-connector"
|
"url": "https://github.com/bitwarden/directory-connector"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "main.js",
|
"main": "main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sub:init": "git submodule update --init --recursive",
|
"sub:init": "git submodule update --init --recursive",
|
||||||
"sub:update": "git submodule update --remote",
|
"sub:update": "git submodule update --remote",
|
||||||
@@ -73,17 +74,17 @@
|
|||||||
"test:types": "npx tsc --noEmit"
|
"test:types": "npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-eslint/eslint-plugin-template": "21.1.0",
|
"@angular-devkit/build-angular": "20.3.3",
|
||||||
"@angular-eslint/template-parser": "21.1.0",
|
"@angular-eslint/eslint-plugin-template": "20.7.0",
|
||||||
"@angular/build": "21.1.2",
|
"@angular-eslint/template-parser": "20.7.0",
|
||||||
"@angular/compiler-cli": "21.1.1",
|
"@angular/compiler-cli": "20.3.15",
|
||||||
"@electron/notarize": "2.5.0",
|
"@electron/notarize": "2.5.0",
|
||||||
"@electron/rebuild": "4.0.1",
|
"@electron/rebuild": "4.0.1",
|
||||||
"@fluffy-spoon/substitute": "1.208.0",
|
"@fluffy-spoon/substitute": "1.208.0",
|
||||||
"@microsoft/microsoft-graph-types": "2.43.1",
|
"@microsoft/microsoft-graph-types": "2.43.1",
|
||||||
"@ngtools/webpack": "21.1.2",
|
"@ngtools/webpack": "20.3.3",
|
||||||
"@types/inquirer": "8.2.10",
|
"@types/inquirer": "8.2.10",
|
||||||
"@types/jest": "30.0.0",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lowdb": "1.0.15",
|
"@types/lowdb": "1.0.15",
|
||||||
"@types/node": "22.19.2",
|
"@types/node": "22.19.2",
|
||||||
"@types/node-fetch": "2.6.12",
|
"@types/node-fetch": "2.6.12",
|
||||||
@@ -91,12 +92,10 @@
|
|||||||
"@types/proper-lockfile": "4.1.4",
|
"@types/proper-lockfile": "4.1.4",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/tldjs": "2.3.4",
|
"@types/tldjs": "2.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||||
"@typescript-eslint/parser": "8.54.0",
|
"@typescript-eslint/parser": "8.50.0",
|
||||||
"@yao-pkg/pkg": "5.16.1",
|
"@yao-pkg/pkg": "5.16.1",
|
||||||
"babel-loader": "10.0.0",
|
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"jest-environment-jsdom": "30.2.0",
|
|
||||||
"concurrently": "9.2.0",
|
"concurrently": "9.2.0",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
@@ -107,7 +106,7 @@
|
|||||||
"electron-log": "5.4.1",
|
"electron-log": "5.4.1",
|
||||||
"electron-reload": "2.0.0-alpha.1",
|
"electron-reload": "2.0.0-alpha.1",
|
||||||
"electron-store": "8.2.0",
|
"electron-store": "8.2.0",
|
||||||
"electron-updater": "6.7.3",
|
"electron-updater": "6.6.2",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.4",
|
||||||
@@ -119,16 +118,16 @@
|
|||||||
"html-loader": "5.1.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.6.3",
|
"html-webpack-plugin": "5.6.3",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jest": "30.2.0",
|
"jest": "29.7.0",
|
||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"jest-mock-extended": "4.0.0",
|
"jest-mock-extended": "4.0.0",
|
||||||
"jest-preset-angular": "16.0.0",
|
"jest-preset-angular": "14.6.0",
|
||||||
"lint-staged": "16.2.6",
|
"lint-staged": "16.2.6",
|
||||||
"mini-css-extract-plugin": "2.10.0",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"minimatch": "5.1.2",
|
"minimatch": "5.1.2",
|
||||||
"node-forge": "1.3.2",
|
"node-forge": "1.3.2",
|
||||||
"node-loader": "2.1.0",
|
"node-loader": "2.1.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.7.4",
|
||||||
"rimraf": "6.1.0",
|
"rimraf": "6.1.0",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"sass": "1.97.1",
|
"sass": "1.97.1",
|
||||||
@@ -136,25 +135,25 @@
|
|||||||
"ts-jest": "29.4.1",
|
"ts-jest": "29.4.1",
|
||||||
"ts-loader": "9.5.2",
|
"ts-loader": "9.5.2",
|
||||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
"type-fest": "5.4.2",
|
"type-fest": "5.3.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.8.3",
|
||||||
"webpack": "5.104.1",
|
"webpack": "5.104.1",
|
||||||
"webpack-cli": "6.0.1",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
"webpack-node-externals": "3.0.0",
|
"webpack-node-externals": "3.0.0",
|
||||||
"zone.js": "0.16.0"
|
"zone.js": "0.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "21.1.1",
|
"@angular/animations": "20.3.15",
|
||||||
"@angular/cdk": "21.1.1",
|
"@angular/cdk": "20.2.14",
|
||||||
"@angular/cli": "21.1.2",
|
"@angular/cli": "20.3.3",
|
||||||
"@angular/common": "21.1.1",
|
"@angular/common": "20.3.15",
|
||||||
"@angular/compiler": "21.1.1",
|
"@angular/compiler": "20.3.15",
|
||||||
"@angular/core": "21.1.1",
|
"@angular/core": "20.3.15",
|
||||||
"@angular/forms": "21.1.1",
|
"@angular/forms": "20.3.15",
|
||||||
"@angular/platform-browser": "21.1.1",
|
"@angular/platform-browser": "20.3.15",
|
||||||
"@angular/platform-browser-dynamic": "21.1.1",
|
"@angular/platform-browser-dynamic": "20.3.15",
|
||||||
"@angular/router": "21.1.1",
|
"@angular/router": "20.3.15",
|
||||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||||
"big-integer": "1.6.52",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "5.3.7",
|
"bootstrap": "5.3.7",
|
||||||
@@ -166,16 +165,16 @@
|
|||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"ldapts": "8.1.3",
|
"ldapts": "8.0.1",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
"ngx-toastr": "20.0.4",
|
"ngx-toastr": "19.1.0",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
"parse5": "8.0.0",
|
"parse5": "8.0.0",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"tldjs": "2.3.1",
|
"tldjs": "2.3.1",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"zone.js": "0.16.0"
|
"zone.js": "0.15.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~20",
|
"node": "~20",
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
"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.9.5",
|
"version": "2.9.5",
|
||||||
|
"type": "module",
|
||||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||||
"homepage": "https://bitwarden.com",
|
"homepage": "https://bitwarden.com",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "main.js",
|
"main": "main.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/bitwarden/directory-connector"
|
"url": "https://github.com/bitwarden/directory-connector"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"bwdc": "../build-cli/bwdc.js"
|
"bwdc": "../build-cli/bwdc.cjs"
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": "../build-cli/**/*"
|
"assets": "../build-cli/**/*"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
|
import { enableProdMode } from "@angular/core";
|
||||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||||
|
|
||||||
import { isDev } from "@/jslib/electron/src/utils";
|
import { isDev } from "@/jslib/electron/src/utils";
|
||||||
@@ -11,7 +11,4 @@ if (!isDev()) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule, {
|
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||||
applicationProviders: [provideZoneChangeDetection()],
|
|
||||||
preserveWhitespaces: true,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,25 +3,17 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
{{ "lastGroupSync" | i18n }}:
|
{{ "lastGroupSync" | i18n }}:
|
||||||
@if (!lastGroupSync) {
|
<span *ngIf="!lastGroupSync">-</span>
|
||||||
<span>-</span>
|
|
||||||
}
|
|
||||||
{{ lastGroupSync | date: "medium" }}
|
{{ lastGroupSync | date: "medium" }}
|
||||||
<br />
|
<br />
|
||||||
{{ "lastUserSync" | i18n }}:
|
{{ "lastUserSync" | i18n }}:
|
||||||
@if (!lastUserSync) {
|
<span *ngIf="!lastUserSync">-</span>
|
||||||
<span>-</span>
|
|
||||||
}
|
|
||||||
{{ lastUserSync | date: "medium" }}
|
{{ lastUserSync | date: "medium" }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ "syncStatus" | i18n }}:
|
{{ "syncStatus" | i18n }}:
|
||||||
@if (syncRunning) {
|
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
|
||||||
<strong class="text-success">{{ "running" | i18n }}</strong>
|
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
|
||||||
}
|
|
||||||
@if (!syncRunning) {
|
|
||||||
<strong class="text-danger">{{ "stopped" | i18n }}</strong>
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
||||||
<button
|
<button
|
||||||
@@ -68,85 +60,57 @@
|
|||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
|
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
@if (!simForm.loading && (simUsers || simGroups)) {
|
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
||||||
<hr />
|
<hr />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<h4>{{ "users" | i18n }}</h4>
|
<h4>{{ "users" | i18n }}</h4>
|
||||||
@if (simEnabledUsers && simEnabledUsers.length) {
|
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
||||||
<ul class="bwi-ul testing-list">
|
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
|
||||||
@for (u of simEnabledUsers; track u) {
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
<li title="{{ u.referenceId }}">
|
{{ u.displayName }}
|
||||||
<i class="bwi bwi-li bwi-user"></i>
|
</li>
|
||||||
{{ u.displayName }}
|
</ul>
|
||||||
</li>
|
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
|
||||||
}
|
{{ "noUsers" | i18n }}
|
||||||
</ul>
|
</p>
|
||||||
}
|
|
||||||
@if (!simEnabledUsers || !simEnabledUsers.length) {
|
|
||||||
<p>
|
|
||||||
{{ "noUsers" | i18n }}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<h4>{{ "disabledUsers" | i18n }}</h4>
|
<h4>{{ "disabledUsers" | i18n }}</h4>
|
||||||
@if (simDisabledUsers && simDisabledUsers.length) {
|
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
||||||
<ul class="bwi-ul testing-list">
|
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
|
||||||
@for (u of simDisabledUsers; track u) {
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
<li title="{{ u.referenceId }}">
|
{{ u.displayName }}
|
||||||
<i class="bwi bwi-li bwi-user"></i>
|
</li>
|
||||||
{{ u.displayName }}
|
</ul>
|
||||||
</li>
|
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
|
||||||
}
|
{{ "noUsers" | i18n }}
|
||||||
</ul>
|
</p>
|
||||||
}
|
|
||||||
@if (!simDisabledUsers || !simDisabledUsers.length) {
|
|
||||||
<p>
|
|
||||||
{{ "noUsers" | i18n }}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<h4>{{ "deletedUsers" | i18n }}</h4>
|
<h4>{{ "deletedUsers" | i18n }}</h4>
|
||||||
@if (simDeletedUsers && simDeletedUsers.length) {
|
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
||||||
<ul class="bwi-ul testing-list">
|
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
|
||||||
@for (u of simDeletedUsers; track u) {
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
<li title="{{ u.referenceId }}">
|
{{ u.displayName }}
|
||||||
<i class="bwi bwi-li bwi-user"></i>
|
</li>
|
||||||
{{ u.displayName }}
|
</ul>
|
||||||
</li>
|
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
|
||||||
}
|
{{ "noUsers" | i18n }}
|
||||||
</ul>
|
</p>
|
||||||
}
|
|
||||||
@if (!simDeletedUsers || !simDeletedUsers.length) {
|
|
||||||
<p>
|
|
||||||
{{ "noUsers" | i18n }}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<h4>{{ "groups" | i18n }}</h4>
|
<h4>{{ "groups" | i18n }}</h4>
|
||||||
@if (simGroups && simGroups.length) {
|
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
|
||||||
<ul class="bwi-ul testing-list">
|
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
|
||||||
@for (g of simGroups; track g) {
|
<i class="bwi bwi-li bwi-sitemap"></i>
|
||||||
<li title="{{ g.referenceId }}">
|
{{ g.displayName }}
|
||||||
<i class="bwi bwi-li bwi-sitemap"></i>
|
<ul class="small" *ngIf="g.users && g.users.length">
|
||||||
{{ g.displayName }}
|
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
|
||||||
@if (g.users && g.users.length) {
|
{{ u.displayName }}
|
||||||
<ul class="small">
|
|
||||||
@for (u of g.users; track u) {
|
|
||||||
<li title="{{ u.referenceId }}">
|
|
||||||
{{ u.displayName }}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</li>
|
</li>
|
||||||
}
|
</ul>
|
||||||
</ul>
|
</li>
|
||||||
}
|
</ul>
|
||||||
@if (!simGroups || !simGroups.length) {
|
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
|
||||||
<p>{{ "noGroups" | i18n }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
|
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
|
||||||
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
|
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
|
||||||
@for (o of directoryOptions; track o) {
|
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
|
||||||
<option [ngValue]="o.value">
|
{{ o.name }}
|
||||||
{{ o.name }}
|
</option>
|
||||||
</option>
|
|
||||||
}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap">
|
<div [hidden]="directory != directoryType.Ldap">
|
||||||
@@ -53,22 +51,20 @@
|
|||||||
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
|
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (!ldap.ad) {
|
<div class="mb-3" *ngIf="!ldap.ad">
|
||||||
<div class="mb-3">
|
<div class="form-check">
|
||||||
<div class="form-check">
|
<input
|
||||||
<input
|
class="form-check-input"
|
||||||
class="form-check-input"
|
type="checkbox"
|
||||||
type="checkbox"
|
id="pagedSearch"
|
||||||
id="pagedSearch"
|
[(ngModel)]="ldap.pagedSearch"
|
||||||
[(ngModel)]="ldap.pagedSearch"
|
name="PagedSearch"
|
||||||
name="PagedSearch"
|
/>
|
||||||
/>
|
<label class="form-check-label" for="pagedSearch">{{
|
||||||
<label class="form-check-label" for="pagedSearch">{{
|
"ldapPagedResults" | i18n
|
||||||
"ldapPagedResults" | i18n
|
}}</label>
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
@@ -83,122 +79,116 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (ldap.ssl) {
|
<div class="ms-4" *ngIf="ldap.ssl">
|
||||||
<div class="ms-4">
|
<div class="mb-3">
|
||||||
<div class="mb-3">
|
<div class="form-check">
|
||||||
<div class="form-check">
|
<input
|
||||||
<input
|
class="form-check-input"
|
||||||
class="form-check-input"
|
type="radio"
|
||||||
type="radio"
|
[value]="false"
|
||||||
[value]="false"
|
id="ssl"
|
||||||
id="ssl"
|
[(ngModel)]="ldap.startTls"
|
||||||
[(ngModel)]="ldap.startTls"
|
name="SSL"
|
||||||
name="SSL"
|
/>
|
||||||
/>
|
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
||||||
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
[value]="true"
|
|
||||||
id="startTls"
|
|
||||||
[(ngModel)]="ldap.startTls"
|
|
||||||
name="StartTLS"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@if (ldap.startTls) {
|
<div class="form-check">
|
||||||
<div class="ms-4">
|
<input
|
||||||
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
class="form-check-input"
|
||||||
<div class="mb-3">
|
type="radio"
|
||||||
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
|
[value]="true"
|
||||||
<input
|
id="startTls"
|
||||||
type="file"
|
[(ngModel)]="ldap.startTls"
|
||||||
class="form-control mb-2"
|
name="StartTLS"
|
||||||
id="tlsCaPath_file"
|
/>
|
||||||
(change)="setSslPath('tlsCaPath')"
|
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="tlsCaPath"
|
|
||||||
name="TLSCaPath"
|
|
||||||
[(ngModel)]="ldap.tlsCaPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (!ldap.startTls) {
|
|
||||||
<div class="ms-4">
|
|
||||||
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="form-control mb-2"
|
|
||||||
id="sslCertPath_file"
|
|
||||||
(change)="setSslPath('sslCertPath')"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="sslCertPath"
|
|
||||||
name="SSLCertPath"
|
|
||||||
[(ngModel)]="ldap.sslCertPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="form-control mb-2"
|
|
||||||
id="sslKeyPath_file"
|
|
||||||
(change)="setSslPath('sslKeyPath')"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="sslKeyPath"
|
|
||||||
name="SSLKeyPath"
|
|
||||||
[(ngModel)]="ldap.sslKeyPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="form-control mb-2"
|
|
||||||
id="sslCaPath_file"
|
|
||||||
(change)="setSslPath('sslCaPath')"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="sslCaPath"
|
|
||||||
name="SSLCaPath"
|
|
||||||
[(ngModel)]="ldap.sslCaPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="certDoNotVerify"
|
|
||||||
[(ngModel)]="ldap.sslAllowUnauthorized"
|
|
||||||
name="CertDoNoVerify"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="certDoNotVerify">{{
|
|
||||||
"ldapCertDoNotVerify" | i18n
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="ms-4" *ngIf="ldap.startTls">
|
||||||
|
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control mb-2"
|
||||||
|
id="tlsCaPath_file"
|
||||||
|
(change)="setSslPath('tlsCaPath')"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="tlsCaPath"
|
||||||
|
name="TLSCaPath"
|
||||||
|
[(ngModel)]="ldap.tlsCaPath"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ms-4" *ngIf="!ldap.startTls">
|
||||||
|
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control mb-2"
|
||||||
|
id="sslCertPath_file"
|
||||||
|
(change)="setSslPath('sslCertPath')"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="sslCertPath"
|
||||||
|
name="SSLCertPath"
|
||||||
|
[(ngModel)]="ldap.sslCertPath"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control mb-2"
|
||||||
|
id="sslKeyPath_file"
|
||||||
|
(change)="setSslPath('sslKeyPath')"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="sslKeyPath"
|
||||||
|
name="SSLKeyPath"
|
||||||
|
[(ngModel)]="ldap.sslKeyPath"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control mb-2"
|
||||||
|
id="sslCaPath_file"
|
||||||
|
(change)="setSslPath('sslCaPath')"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="sslCaPath"
|
||||||
|
name="SSLCaPath"
|
||||||
|
[(ngModel)]="ldap.sslCaPath"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="certDoNotVerify"
|
||||||
|
[(ngModel)]="ldap.sslAllowUnauthorized"
|
||||||
|
name="CertDoNoVerify"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="certDoNotVerify">{{
|
||||||
|
"ldapCertDoNotVerify" | i18n
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3" [hidden]="true">
|
<div class="mb-3" [hidden]="true">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
@@ -221,12 +211,10 @@
|
|||||||
name="Username"
|
name="Username"
|
||||||
[(ngModel)]="ldap.username"
|
[(ngModel)]="ldap.username"
|
||||||
/>
|
/>
|
||||||
@if (ldap.ad) {
|
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} company\admin</div>
|
<div class="form-text" *ngIf="!ldap.ad">
|
||||||
}
|
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
|
||||||
@if (!ldap.ad) {
|
</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} cn=admin,dc=company,dc=com</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">{{ "password" | i18n }}</label>
|
<label for="password" class="form-label">{{ "password" | i18n }}</label>
|
||||||
@@ -616,24 +604,18 @@
|
|||||||
name="UserFilter"
|
name="UserFilter"
|
||||||
[(ngModel)]="sync.userFilter"
|
[(ngModel)]="sync.userFilter"
|
||||||
></textarea>
|
></textarea>
|
||||||
@if (directory === directoryType.Ldap) {
|
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||||
<div class="form-text">
|
{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))
|
||||||
{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))
|
</div>
|
||||||
</div>
|
<div class="form-text" *ngIf="directory === directoryType.EntraID">
|
||||||
}
|
{{ "ex" | i18n }} exclude:joe@company.com
|
||||||
@if (directory === directoryType.EntraID) {
|
</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} exclude:joe@company.com</div>
|
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||||
}
|
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
||||||
@if (directory === directoryType.Okta) {
|
</div>
|
||||||
<div class="form-text">
|
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||||
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
{{ "ex" | i18n }} exclude:joe@company.com | orgUnitPath=/Engineering
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
@if (directory === directoryType.GSuite) {
|
|
||||||
<div class="form-text">
|
|
||||||
{{ "ex" | i18n }} exclude:joe@company.com | orgUnitPath=/Engineering
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||||
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
|
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
|
||||||
@@ -699,20 +681,18 @@
|
|||||||
name="GroupFilter"
|
name="GroupFilter"
|
||||||
[(ngModel)]="sync.groupFilter"
|
[(ngModel)]="sync.groupFilter"
|
||||||
></textarea>
|
></textarea>
|
||||||
@if (directory === directoryType.Ldap) {
|
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||||
<div class="form-text">
|
{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
|
||||||
{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
|
</div>
|
||||||
</div>
|
<div class="form-text" *ngIf="directory === directoryType.EntraID">
|
||||||
}
|
{{ "ex" | i18n }} include:Sales,IT
|
||||||
@if (directory === directoryType.EntraID) {
|
</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
|
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||||
}
|
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
|
||||||
@if (directory === directoryType.Okta) {
|
</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</div>
|
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||||
}
|
{{ "ex" | i18n }} include:Sales,IT
|
||||||
@if (directory === directoryType.GSuite) {
|
</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||||
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
|
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
|
||||||
@@ -723,12 +703,8 @@
|
|||||||
name="GroupPath"
|
name="GroupPath"
|
||||||
[(ngModel)]="sync.groupPath"
|
[(ngModel)]="sync.groupPath"
|
||||||
/>
|
/>
|
||||||
@if (!ldap.ad) {
|
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
|
||||||
<div class="form-text">{{ "ex" | i18n }} CN=Groups</div>
|
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
|
||||||
}
|
|
||||||
@if (ldap.ad) {
|
|
||||||
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
2
src/scss/bootstrap.scss
vendored
2
src/scss/bootstrap.scss
vendored
@@ -28,4 +28,4 @@ $danger: map_get($theme-colors, "danger");
|
|||||||
$secondary: map_get($theme-colors, "secondary");
|
$secondary: map_get($theme-colors, "secondary");
|
||||||
$secondary-alt: map_get($theme-colors, "secondary-alt");
|
$secondary-alt: map_get($theme-colors, "secondary-alt");
|
||||||
|
|
||||||
@import "bootstrap/scss/bootstrap.scss";
|
@import "~bootstrap/scss/bootstrap.scss";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
html.os_windows {
|
html.os_windows {
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding: 10px 0 20px 0;
|
padding: 10px 0 20px 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import "ngx-toastr/toastr";
|
@import "~ngx-toastr/toastr";
|
||||||
|
|
||||||
@import "bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
.toast-container {
|
.toast-container {
|
||||||
.toast-close-button {
|
.toast-close-button {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { webcrypto } from "crypto";
|
import { webcrypto } from "crypto";
|
||||||
import { TextEncoder, TextDecoder } from "util";
|
|
||||||
|
|
||||||
Object.assign(globalThis, { TextEncoder, TextDecoder });
|
import "jest-preset-angular/setup-jest";
|
||||||
|
|
||||||
Object.defineProperty(window, "CSS", { value: null });
|
Object.defineProperty(window, "CSS", { value: null });
|
||||||
Object.defineProperty(window, "getComputedStyle", {
|
Object.defineProperty(window, "getComputedStyle", {
|
||||||
value: () => {
|
value: () => {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"target": "ES2016",
|
"target": "ES2020",
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
"lib": ["es5", "es6", "es7", "dom"],
|
"lib": ["es5", "es6", "es7", "dom"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmitOnError": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"tldjs": ["./jslib/common/src/misc/tldjs.noop"],
|
"tldjs": ["./jslib/common/src/misc/tldjs.noop"],
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
|
|||||||
13
tsconfig.renderer.json
Normal file
13
tsconfig.renderer.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"strictTemplates": true,
|
||||||
|
"preserveWhitespaces": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmitOnError": false
|
||||||
|
},
|
||||||
|
"include": ["src/app"],
|
||||||
|
"exclude": ["jslib", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
@@ -14,7 +14,12 @@ const ENV = (process.env.ENV = process.env.NODE_ENV);
|
|||||||
const moduleRules = [
|
const moduleRules = [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
use: "ts-loader",
|
use: {
|
||||||
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
exclude: path.resolve(__dirname, "node_modules"),
|
exclude: path.resolve(__dirname, "node_modules"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -62,7 +67,7 @@ const config = {
|
|||||||
modules: [path.resolve("node_modules")],
|
modules: [path.resolve("node_modules")],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: "[name].js",
|
filename: "[name].cjs",
|
||||||
path: path.resolve(__dirname, "build-cli"),
|
path: path.resolve(__dirname, "build-cli"),
|
||||||
},
|
},
|
||||||
module: { rules: moduleRules },
|
module: { rules: moduleRules },
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ const common = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: "ts-loader",
|
use: {
|
||||||
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -57,6 +62,9 @@ const main = {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
output: {
|
||||||
|
filename: "[name].cjs",
|
||||||
|
},
|
||||||
externals: {
|
externals: {
|
||||||
"electron-reload": "commonjs2 electron-reload",
|
"electron-reload": "commonjs2 electron-reload",
|
||||||
keytar: "commonjs2 keytar",
|
keytar: "commonjs2 keytar",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const common = {
|
|||||||
plugins: [],
|
plugins: [],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".tsx", ".ts", ".js", ".json"],
|
extensions: [".tsx", ".ts", ".js", ".json"],
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.renderer.json" })],
|
||||||
symlinks: false,
|
symlinks: false,
|
||||||
modules: [path.resolve("node_modules")],
|
modules: [path.resolve("node_modules")],
|
||||||
},
|
},
|
||||||
@@ -113,7 +113,7 @@ const renderer = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new AngularWebpackPlugin({
|
new AngularWebpackPlugin({
|
||||||
tsConfigPath: "tsconfig.json",
|
tsConfigPath: "tsconfig.renderer.json",
|
||||||
entryModule: "src/app/app.module#AppModule",
|
entryModule: "src/app/app.module#AppModule",
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user