From 567ca551dcffabdd9f61774a56b5ec0c5a2beef5 Mon Sep 17 00:00:00 2001 From: Krista House Date: Fri, 10 Jul 2026 14:02:24 -0400 Subject: [PATCH 1/2] feat(webapp): add Active Directory web client Adds Active Directory web-client support for LDAP and LDAPS sessions, including the connection form, session lifecycle integration, LDAP WASM data adapter, and WebTerminal Active Directory composite wiring. Integrates the Active Directory UI with the Gateway theme using scoped bridge tokens and the published Active Directory GUI package. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- webapp/apps/gateway-ui/package.json | 6 +- .../apps/gateway-ui/src/assets/css/main.scss | 12 + .../src/client/app/app.constants.ts | 5 + .../services/active-directory-data.service.ts | 440 ++++++++++++++++++ ...ctive-directory-session-manager.service.ts | 26 ++ .../active-directory-session-store.service.ts | 23 + .../active-directory-translator.service.ts | 187 ++++++++ .../services/active-directory-ui.service.ts | 85 ++++ ...web-client-active-directory.component.html | 32 ++ ...web-client-active-directory.component.scss | 101 ++++ .../web-client-active-directory.component.ts | 397 ++++++++++++++++ .../active-directory-form.component.html | 80 ++++ .../active-directory-form.component.scss | 17 + .../active-directory-form.component.ts | 96 ++++ .../form/web-client-form.component.html | 10 +- .../form/web-client-form.component.ts | 12 + .../modules/web-client/web-client.module.ts | 8 +- .../shared/enums/web-client-protocol.enum.ts | 12 +- .../connection-params.interfaces.ts | 23 + .../app/shared/interfaces/forms.interfaces.ts | 15 +- .../app/shared/models/web-session.model.ts | 5 + .../app/shared/services/web-client.service.ts | 46 +- .../app/styles/ad-theme-bridge.gateway.scss | 254 ++++++++++ webapp/pnpm-lock.yaml | 136 +++++- 24 files changed, 2007 insertions(+), 21 deletions(-) create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss create mode 100644 webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts create mode 100644 webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss diff --git a/webapp/apps/gateway-ui/package.json b/webapp/apps/gateway-ui/package.json index e307cdda4..37eab3527 100644 --- a/webapp/apps/gateway-ui/package.json +++ b/webapp/apps/gateway-ui/package.json @@ -30,13 +30,15 @@ "@devolutions/iron-remote-desktop": "^0.11.0", "@devolutions/iron-remote-desktop-rdp": "^0.7.0", "@devolutions/iron-remote-desktop-vnc": "0.7.0", + "@devolutions/ldap-wasm-js": "^0.3.4", "@devolutions/terminal-shared": "^1.4.1", + "@devolutions/web-active-directory-gui": "0.0.13", "@devolutions/web-ssh-gui": "^0.7.0", "@devolutions/web-telnet-gui": "^0.4.4", "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^1.2.5", - "@primeuix/themes": "^1.2.5", - "@primeuix/utils": "^0.6.3", + "@primeuix/themes": "2.0.3", + "@primeuix/utils": "0.7.2", "@xterm/addon-clipboard": "^0.1.0", "@xterm/xterm": "^5.5.0", "i18next": "^25.5.3", diff --git a/webapp/apps/gateway-ui/src/assets/css/main.scss b/webapp/apps/gateway-ui/src/assets/css/main.scss index 27eea1f40..494b2855a 100644 --- a/webapp/apps/gateway-ui/src/assets/css/main.scss +++ b/webapp/apps/gateway-ui/src/assets/css/main.scss @@ -9,6 +9,8 @@ @use 'dvls-stack/variables.generated' as *; @use 'dvls-stack/theme/light-theme' as *; @use 'dvls-stack/theme/dark-theme' as *; +@use '@devolutions/web-active-directory-gui/assets/css/ad-styles' as *; +@use '../../client/app/styles/ad-theme-bridge.gateway' as *; @use 'style/base' as *; @use 'style/entry-status' as *; @@ -57,6 +59,16 @@ line-height: 1; } +/* PrimeIcons must always win over generic PrimeNG font overrides. */ +.pi, +[class^="pi-"], +[class*=" pi-"] { + font-family: 'primeicons' !important; + font-style: normal; + font-weight: normal; + line-height: 1; +} + .full-width-dropdown .p-dropdown, .full-width-dropdown .p-select { width: 100%; diff --git a/webapp/apps/gateway-ui/src/client/app/app.constants.ts b/webapp/apps/gateway-ui/src/client/app/app.constants.ts index 7b0b65b05..d620d6b6f 100644 --- a/webapp/apps/gateway-ui/src/client/app/app.constants.ts +++ b/webapp/apps/gateway-ui/src/client/app/app.constants.ts @@ -5,6 +5,7 @@ export const DVL_TELNET_ICON = 'dvl-icon-entry-session-telnet'; export const DVL_SSH_ICON = 'dvl-icon-entry-session-ssh'; export const DVL_VNC_ICON = 'dvl-icon-entry-session-vnc'; export const DVL_ARD_ICON = 'dvl-icon-entry-session-apple-remote-desktop'; +export const DVL_ACTIVE_DIRECTORY_ICON = 'dvl-icon-entry-ActiveDirectory'; export const DVL_WARNING_ICON = 'dvl-icon-warning'; export const JET_RDP_URL = '/jet/rdp'; @@ -12,6 +13,7 @@ export const JET_TELNET_URL = '/jet/fwd/tcp'; export const JET_SSH_URL = '/jet/fwd/tcp'; export const JET_VNC_URL = '/jet/fwd/tcp'; export const JET_ARD_URL = '/jet/fwd/tcp'; +export const JET_AD_URL = '/jet/fwd'; export const JET_KDC_PROXY_URL = '/jet/KdcProxy'; export const ProtocolIconMap = { @@ -20,6 +22,7 @@ export const ProtocolIconMap = { [Protocol.SSH]: DVL_SSH_ICON, [Protocol.VNC]: DVL_VNC_ICON, [Protocol.ARD]: DVL_ARD_ICON, + [Protocol.ActiveDirectory]: DVL_ACTIVE_DIRECTORY_ICON, }; export const ProtocolNameToProtocolMap = { @@ -28,4 +31,6 @@ export const ProtocolNameToProtocolMap = { telnet: Protocol.Telnet, rdp: Protocol.RDP, ard: Protocol.ARD, + ldap: Protocol.ActiveDirectory, + ldaps: Protocol.ActiveDirectory, }; diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts new file mode 100644 index 000000000..0c74bee9f --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts @@ -0,0 +1,440 @@ +import { Injectable, NgZone } from '@angular/core'; +import init, { + Attribute, + AttributesArray, + BinaryLdapModifies, + LdapControl, + LdapControlArray, + LdapResult, + LdapSession, + LdapSessionParameters, + LoggingLevel, + ModifyRequest, + SaslBindConfig, + SearchParameters, + SspiAuthMethod, + set_logging_level, +} from '@devolutions/ldap-wasm-js'; +import { + AdAddRequest, + AdBindRequest, + AdBindResult, + AdCapabilities, + AdDataProvider, + AdDeleteRequest, + AdModifyDnRequest, + AdModifyRequest, + AdPageControl, + AdPageResult, + AdResult, + AdSearchParams, + AdSearchResult, + LdapControlArrayLike, + LdapResultLike, + LdapSessionLike, + normalizeError, + SearchEntryLike, + SearchMessageLike, +} from '@devolutions/web-active-directory-gui'; +import { ActiveDirectorySessionStoreService } from './active-directory-session-store.service'; + +const LDAP_DECODER_MAX_BYTES = 32656; +const LDAP_PAGED_SEARCH_SIZE_LIMIT = 100000; + +@Injectable() +export class ActiveDirectoryDataService implements AdDataProvider { + private wasmInitialized = false; + private wasmInitPromise: Promise | null = null; + + constructor( + private readonly zone: NgZone, + private readonly sessionStore: ActiveDirectorySessionStoreService, + ) {} + + async search(params: AdSearchParams): Promise { + const session = this.sessionStore.getSession(); + + try { + const result = await this.zone.runOutsideAngular(() => + session.search({ + search_base: params.baseDn, + filter: params.filter, + scope: params.scope, + attributes: params.attributes || [], + size_limit: undefined, + time_limit: params.timeLimit, + controls: params.controls, + }), + ); + + const entries = this.extractSearchEntries(result.messages || []); + + return { + entries, + total: entries.length, + nextCookie: null, + control: undefined, + }; + } catch (error) { + throw normalizeError(error); + } + } + + async pagedSearch(ctrl: AdPageControl): Promise { + const session = this.sessionStore.getSession(); + + try { + const controlsWithPaging: LdapControlArrayLike = [ + ...(ctrl.controls || []), + { + simple_paged_results: { + size: ctrl.pageSize, + cookie: ctrl.cookie ?? [], + }, + }, + ]; + + const result = await this.zone.runOutsideAngular(() => + session.search({ + search_base: ctrl.baseDn, + filter: ctrl.filter, + scope: ctrl.scope, + attributes: ctrl.attributes || [], + size_limit: LDAP_PAGED_SEARCH_SIZE_LIMIT, + time_limit: ctrl.timeLimit, + controls: controlsWithPaging, + }), + ); + + const entries = this.extractSearchEntries(result.messages || []); + const searchDone = result.messages?.find((message) => 'search_done' in message.op); + const responseControl = searchDone?.ctrl ?? []; + const pagedControl = responseControl.find( + (ldapControl): ldapControl is { simple_paged_results: { size: number; cookie: number[] } } => + 'simple_paged_results' in ldapControl, + ); + const cookie = pagedControl?.simple_paged_results.cookie ?? []; + + return { + entries, + hasMore: cookie.length > 0, + cookie, + control: responseControl, + }; + } catch (error) { + throw normalizeError(error); + } + } + + async getCapabilities(): Promise { + return { + canCreateUser: true, + canCreateGroup: true, + canResetPassword: true, + canDelete: true, + }; + } + + async connect(gatewayWsUrl: string): Promise { + try { + await this.initializeWasm(); + + const ldapSession = await this.zone.runOutsideAngular(() => + LdapSession.connect(new LdapSessionParameters(gatewayWsUrl, LDAP_DECODER_MAX_BYTES)), + ); + + const wrappedSession: LdapSessionLike = { + sasl_bind: async (args) => { + const response = await this.zone.runOutsideAngular(() => ldapSession.sasl_bind(this.toSaslBindConfig(args))); + + return { + res: response.res, + serverSaslCredential: response.saslcreds, + }; + }, + unbind: (ldapControl) => + this.zone.runOutsideAngular(() => ldapSession.unbind(this.toLdapControls(ldapControl))), + search: async (args) => { + const result = await this.zone.runOutsideAngular(() => ldapSession.search(this.toSearchParameters(args))); + + return { + messages: result.messages.flatMap((message): SearchMessageLike[] => { + if ('search_entry' in message.op) { + return [{ op: { search_entry: message.op.search_entry }, ctrl: message.ctrl }]; + } + + if ('search_done' in message.op) { + return [{ op: { search_done: message.op.search_done }, ctrl: message.ctrl }]; + } + + return []; + }), + }; + }, + modify: (dn, modifies, controls) => + this.zone.runOutsideAngular(() => + ldapSession.modify(dn, this.toBinaryLdapModifies(modifies), this.toLdapControls(controls)), + ), + modifyDn: (dn, newRdn, deleteOldRdn, newSuperior, controls) => + this.zone.runOutsideAngular(() => + ldapSession.modify_dn(dn, newRdn, deleteOldRdn, newSuperior, this.toLdapControls(controls)), + ), + delete: (dn, controls) => + this.zone.runOutsideAngular(() => ldapSession.delete(dn, this.toLdapControls(controls))), + add: (dn, attributes, controls) => + this.zone.runOutsideAngular(() => + ldapSession.add(dn, this.toAttributesArray(attributes), this.toLdapControls(controls)), + ), + }; + + this.sessionStore.setSession(wrappedSession); + + return wrappedSession; + } catch (error) { + throw normalizeError(error); + } + } + + async bind(req: AdBindRequest): Promise { + try { + const { domain, serverComputerName } = this.resolveDomainAndServerComputerName( + req.domain, + req.serverComputerName, + ); + const response = await req.session.sasl_bind({ + username: req.username, + password: req.password, + auth_method: { + negotiate: { + domain, + server_computer_name: serverComputerName, + kdc_proxy_url: req.kdcProxyUrl, + }, + }, + sign: req.sign, + seal: req.seal, + controls: undefined, + }); + + return response.res; + } catch (error) { + throw normalizeError(error); + } + } + + async unbind(session: LdapSessionLike, control?: LdapControlArrayLike): Promise { + try { + await session.unbind(control); + this.sessionStore.clearSession(); + } catch (error) { + throw normalizeError(error); + } + } + + makeGatewayWsUrl(gatewayUrl: string | URL, sessionId: string, isLdaps: boolean, token?: string): string { + const url = typeof gatewayUrl === 'string' ? new URL(gatewayUrl, window.location.href) : new URL(gatewayUrl.href); + const forwardProtocol = isLdaps ? 'tls' : 'tcp'; + + url.pathname = `${url.pathname.replace(/\/$/, '')}/${forwardProtocol}/${sessionId}`; + url.search = token ? `?token=${token}` : ''; + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + + return url.href; + } + + async addEntry(req: AdAddRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.add(req.dn, req.attributes, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async deleteEntry(req: AdDeleteRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.delete(req.dn, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async modifyEntry(req: AdModifyRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.modify(req.dn, req.modifies, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async modifyDnEntry(req: AdModifyDnRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => + req.session.modifyDn(req.dn, req.newRdn, req.deleteOldRdn, req.newSuperior ?? null, req.controls), + ); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async initializeWasm(): Promise { + if (this.wasmInitialized) { + return; + } + + if (!this.wasmInitPromise) { + this.wasmInitPromise = (async () => { + await this.zone.runOutsideAngular(() => init()); + set_logging_level(LoggingLevel.Warn); + this.wasmInitialized = true; + })(); + } + + return this.wasmInitPromise; + } + + private extractSearchEntries(messages: { op: { search_entry?: SearchEntryLike } }[]): SearchEntryLike[] { + return messages.flatMap((message) => (message.op.search_entry ? [message.op.search_entry] : [])); + } + + private toSearchParameters(args: { + search_base: string; + filter: string; + scope: SearchParameters['scope']; + attributes: string[]; + size_limit?: number; + time_limit?: number; + controls?: LdapControlArrayLike; + }): SearchParameters { + return { + search_base: args.search_base, + filter: args.filter, + scope: args.scope, + attributes: args.attributes, + size_limit: args.size_limit, + time_limit: args.time_limit, + controls: this.toLdapControls(args.controls), + }; + } + + private toLdapControls(controls?: LdapControlArrayLike | null): LdapControlArray | undefined { + return controls?.map((control) => this.toLdapControl(control)); + } + + private toLdapControl(control: LdapControlArrayLike[number]): LdapControl { + if (this.isSimplePagedResultsControl(control)) { + return { + simple_paged_results: { + size: control.simple_paged_results.size, + cookie: control.simple_paged_results.cookie, + }, + }; + } + + return control as LdapControl; + } + + private isSimplePagedResultsControl( + control: LdapControlArrayLike[number], + ): control is { simple_paged_results: { size: number; cookie: number[] } } { + return ( + 'simple_paged_results' in control && + typeof control.simple_paged_results === 'object' && + control.simple_paged_results !== null && + 'size' in control.simple_paged_results && + typeof control.simple_paged_results.size === 'number' && + 'cookie' in control.simple_paged_results && + Array.isArray(control.simple_paged_results.cookie) + ); + } + + private toSaslBindConfig(args: Parameters[0]): SaslBindConfig { + return { + username: args.username, + password: args.password, + auth_method: this.toSspiAuthMethod(args.auth_method), + sign: args.sign, + seal: args.seal, + controls: this.toLdapControls(args.controls), + }; + } + + private toBinaryLdapModifies(modifies: Parameters[1]): BinaryLdapModifies { + return modifies.map( + (modify): ModifyRequest => ({ + operation: modify.operation, + attribute: this.toLdapAttribute(modify.attribute), + }), + ); + } + + private toAttributesArray(attributes: Parameters[1]): AttributesArray { + return attributes.map((attribute) => this.toLdapAttribute(attribute)); + } + + private toLdapAttribute(attribute: Parameters[1][number]): Attribute { + return { + attribute_name: attribute.attribute_name, + attribute_value: attribute.attribute_value, + }; + } + + private toSspiAuthMethod(authMethod: unknown): SspiAuthMethod { + if (this.isSspiAuthMethod(authMethod)) { + return authMethod; + } + + throw new Error('invalid active directory authentication method'); + } + + private isSspiAuthMethod(authMethod: unknown): authMethod is SspiAuthMethod { + if (typeof authMethod !== 'object' || authMethod === null) { + return false; + } + + return 'negotiate' in authMethod || 'kerberos' in authMethod || 'ntlm' in authMethod; + } + + private toAdResult(result: LdapResultLike | LdapResult): AdResult { + return { + code: String(result.code), + message: result.message, + matchedDn: this.getMatchedDn(result), + referral: result.referral, + }; + } + + private getMatchedDn(result: LdapResultLike | LdapResult): string | undefined { + if ('matchedDn' in result && typeof result.matchedDn === 'string') { + return result.matchedDn; + } + + if ('matcheddn' in result && typeof result.matcheddn === 'string') { + return result.matcheddn; + } + + return undefined; + } + + private resolveDomainAndServerComputerName( + domain: string | undefined, + serverComputerName: string, + ): { domain: string | undefined; serverComputerName: string } { + if (domain) { + return { + domain, + serverComputerName: `${serverComputerName.replace(`.${domain}`, '')}.${domain}`, + }; + } + + const parts = serverComputerName.split('.'); + + return { + domain: parts.length > 1 ? parts.slice(1).join('.') : undefined, + serverComputerName, + }; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts new file mode 100644 index 000000000..2436138a6 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { AdSessionManager, AdWebSessionConfig } from '@devolutions/web-active-directory-gui'; +import { Observable, of, throwError } from 'rxjs'; + +@Injectable() +export class ActiveDirectorySessionManagerService implements AdSessionManager { + private readonly sessionConfigMap = new Map(); + + setWebSessionConfig(config: AdWebSessionConfig): void { + this.sessionConfigMap.set(config.sessionId, config); + } + + clearWebSessionConfig(sessionId: string): void { + this.sessionConfigMap.delete(sessionId); + } + + getWebSessionConfig(sessionId: string): Observable { + const config = this.sessionConfigMap.get(sessionId); + + if (!config) { + return throwError(() => new Error(`Active Directory session config not found for session ${sessionId}`)); + } + + return of(config); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts new file mode 100644 index 000000000..7098b927a --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { LdapSessionLike } from '@devolutions/web-active-directory-gui'; + +@Injectable() +export class ActiveDirectorySessionStoreService { + private session: LdapSessionLike | null = null; + + setSession(session: LdapSessionLike): void { + this.session = session; + } + + getSession(): LdapSessionLike { + if (!this.session) { + throw new Error('No active LDAP session'); + } + + return this.session; + } + + clearSession(): void { + this.session = null; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts new file mode 100644 index 000000000..f3bea5dd7 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts @@ -0,0 +1,187 @@ +import { Injectable } from '@angular/core'; +import { AdTranslator } from '@devolutions/web-active-directory-gui'; + +@Injectable() +export class ActiveDirectoryTranslatorService implements AdTranslator { + private readonly translations: Record = { + btnCancel: 'Cancel', + btnClose: 'Close', + btnCreate: 'Create', + btnDelete: 'Delete', + btnEdit: 'Edit', + btnNo: 'No', + btnOk: 'OK', + btnSave: 'Save', + btnYes: 'Yes', + ActiveDirectoryGroupScope_DomainLocal: 'Domain local', + ActiveDirectoryGroupScope_Global: 'Global', + ActiveDirectoryGroupScope_Universal: 'Universal', + ActiveDirectoryGroupType_Distribution: 'Distribution', + ActiveDirectoryGroupType_Security: 'Security', + AlreadyExists: 'Already exists', + AnErrorOccurredWhileSearchingForX: 'An error occurred while searching for {0}', + AnUnexpectedErrorOccured: 'An unexpected error occurred', + BitLockerMessageFormat: + 'Recovery Password:\r\n {0}\r\n {1}\r\n\r\nComputer: {2}\r\nDate: {3}\r\nPassword ID: {4}', + ConnectionLogMessage_ResetPassword: 'Reset password', + ConnectionLogMessage_UserResetPassword: 'User reset password', + CopiedToClipboard: 'Copied to clipboard', + DoYouReallyWantToDeleteX: 'Do you really want to delete {0}?', + DoYouReallyWantToDisableTheSelectedUser: 'Do you really want to disable the selected user?', + DoYouReallyWantToEnableTheSelectedUser: 'Do you really want to enable the selected user?', + DoYouWantToUnlockTheUserX: 'Do you want to unlock the user {0}?', + FailedToGetChildGroupsOfActiveDirectoryGroupX: 'Failed to get child groups of Active Directory group {0}', + MissingRequiredSettings: 'Missing required settings', + OperationFailed: 'Operation failed', + TheGroupHasBeenSuccessfullyCreated: 'The group has been successfully created', + TheObjectHasBeenSuccessfullyDeleted: 'The object has been successfully deleted', + ThePasswordHasBeenSuccessfullyReset: 'The password has been successfully reset', + TheUserHasBeenSuccessfullyCreated: 'The user has been successfully created', + TheUserHasBeenSuccessfullyDisabled: 'The user has been successfully disabled.', + TheUserHasBeenSuccessfullyEnabled: 'The user has been successfully enabled', + TheUserHasBeenSuccessfullyUnlocked: 'The user has been successfully unlocked', + UnableToCopy: 'Unable to Copy', + lblNo: 'No', + lblYes: 'Yes', + YourConnectionIsNotReady: 'Your connection is not ready', + YouMustFillAllFieldsBeforeContinuing: 'You must fill in all fields before continuing.', + OperationNotPermitted: 'Operation not permitted', + OperationsError: 'Operations error', + error: 'Error', + errBindFailed: 'Authentication failed', + errConnectionFailed: 'Failed to connect to Active Directory', + errInvalidCredentials: 'Invalid credentials', + errOperationFailed: 'Operation failed', + errPermissionDenied: 'Permission denied', + errSearchFailed: 'Search failed', + errTimeout: 'Operation timed out', + ldapAlreadyExists: 'Object already exists', + ldapAuthMethodNotSupported: 'Authentication method not supported', + ldapBusy: 'Server is busy', + ldapConstraintViolation: 'Constraint violation', + ldapInsufficientAccessRights: 'Insufficient access rights', + ldapInvalidCredentials: 'Invalid credentials', + ldapInvalidDNSyntax: 'Invalid DN syntax', + ldapNoSuchObject: 'Object not found', + ldapOperationsError: 'Operations error', + ldapProtocolError: 'Protocol error', + ldapSizeLimitExceeded: 'Size limit exceeded', + ldapStrongAuthRequired: 'Strong authentication required', + ldapSuccess: 'Operation completed successfully', + ldapTimeLimitExceeded: 'Time limit exceeded', + ldapUnavailable: 'Server is unavailable', + ldapUnwillingToPerform: 'Server unwilling to perform', + ldapdisplayname: 'Display Name', + lblAccountName: 'Account Name', + lblActiveDirectoryBitLockerFind: 'Find BitLocker Recovery Password', + lblActiveDirectoryPropertyListEditor: 'Property List Editor', + lblActiveDirectoryPropertyValueEditor: 'Property Value Editor', + lblActiveDirectoryUserProperties: 'User Properties', + lblAdd: 'Add', + lblAddMembers: 'Add Members', + lblAllPrinters: 'All Printers', + lblAttribute: 'Attribute', + lblAttributes: 'Attributes', + lblBitLocker: 'BitLocker', + lblBitLockerRecoveryPassword: 'BitLocker Recovery Password', + lblCancel: 'Cancel', + lblClear: 'Clear', + lblCloseSession: 'Close Session', + lblCommonName: 'Name', + lblComputer: 'Computer', + lblConfirm: 'Confirm', + lblConnect: 'Connect', + lblContainer: 'Container', + lblCopyToClipboard: 'Copy to Clipboard', + lblCreationDate: 'Creation Date', + lblDelete: 'Delete', + lblDisable: 'Disable', + lblDisabled: 'Disabled', + lblDisconnect: 'Disconnect', + lblDistinguishedName: 'Distinguished Name', + lblDomain: 'Domain', + lblEdit: 'Edit', + lblEnable: 'Enable', + lblEnterTextToSearch: 'Enter text to search', + lblExpand: 'Expand', + lblFilter: 'Filter', + lblFullName: 'Full Name', + lblGroup: 'Group', + lblGroupName: 'Group Name', + lblGroupPolicySettings: 'Group Policy Settings', + lblGroupProperties: 'Group Properties', + lblGroupScope: 'Group Scope', + lblGroupType: 'Group Type', + lblHidePassword: 'Hide Password', + lblLastModifiedOn: 'Last Modified', + lblLoading: 'Loading...', + lblMember: 'Member', + lblMemberOf: 'Member Of', + lblMembers: 'Members', + lblMore: 'More', + lblMustChangePasswordAtNextLogon: 'Must change password at next logon', + lblName: 'Name', + lblNewGroup: 'New Group', + lblNewUser: 'New User', + lblNoDataToDisplay: 'No data to display', + lblNoResultsFound: 'No results found', + lblOk: 'OK', + lblOrganizationalUnit: 'Organizational Unit', + lblPassword: 'Password', + lblPasswordID: 'Password ID', + lblProperties: 'Properties', + lblRecentlyDisabled: 'Recently Disabled', + lblRecentlyLocked: 'Recently Locked', + lblRecentlyModified: 'Recently Modified', + lblRefresh: 'Refresh', + lblRemove: 'Remove', + lblResults: 'Results', + lblRevealPassword: 'Reveal Password', + lblSamAccountName: 'SAM Account Name', + lblSave: 'Save', + lblSearch: 'Search', + lblSelect: 'Select', + lblType: 'Type', + lblUnlock: 'Unlock', + lblUPNLogonName: 'UPN Logon Name', + lblUser: 'User', + lblUserCreatedButPasswordFailed: 'User created, but password update failed', + lblValue: 'Value', + lblValues: 'Values', + lblValueToAdd: 'Value to Add', + lblWarning: 'Warning', + msgConfirmDelete: 'Are you sure you want to delete this item?', + msgConnected: 'Connected to Active Directory', + msgConnectionLost: 'Connection to Active Directory lost', + msgDeleteSuccess: 'Item deleted successfully', + msgDisconnected: 'Disconnected from Active Directory', + msgError: 'An error occurred', + msgNoResults: 'No results found', + msgSaveSuccess: 'Changes saved successfully', + msgSessionTerminated: 'Session has been terminated', + msgSuccess: 'Success', + }; + + translate(key: string, valuesToInclude?: Record | Array): string { + const translatedKey = this.translations[key] ?? key; + + if (!valuesToInclude) { + return translatedKey; + } + + if (Array.isArray(valuesToInclude)) { + return valuesToInclude.reduce( + (translated, value, index) => translated.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value)), + translatedKey, + ); + } + + return Object.entries(valuesToInclude).reduce( + (translated, [name, value]) => + translated + .replace(new RegExp(`\\{${name}\\}`, 'g'), String(value)) + .replace(new RegExp(`{{\\s*${name}\\s*}}`, 'g'), String(value)), + translatedKey, + ); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts new file mode 100644 index 000000000..32560bdfe --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { AdUi, AdUiConfirmOptions, AdUiToastOptions } from '@devolutions/web-active-directory-gui'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { ActiveDirectoryTranslatorService } from './active-directory-translator.service'; + +const AD_CONFIRM_KEY = 'adc-confirm-dialog'; +const AD_TOAST_KEY = 'adc-toast'; + +@Injectable() +export class ActiveDirectoryUiService implements AdUi { + private loadingCount = 0; + + constructor( + private readonly confirmationService: ConfirmationService, + private readonly messageService: MessageService, + private readonly translator: ActiveDirectoryTranslatorService, + ) {} + + toast(input: AdUiToastOptions): void { + this.messageService.add({ + key: input.key ?? AD_TOAST_KEY, + severity: input.severity, + summary: input.summary, + detail: input.detail, + life: input.life ?? 5000, + sticky: input.sticky, + }); + } + + clearToast(key?: string): void { + this.messageService.clear(key); + } + + confirm(input: AdUiConfirmOptions): void { + this.confirmationService.confirm({ + key: input.key ?? AD_CONFIRM_KEY, + header: this.translator.translate('lblConfirm'), + message: input.message, + acceptLabel: input.acceptLabel ?? this.translator.translate('lblYes'), + rejectLabel: input.rejectLabel ?? this.translator.translate('lblNo'), + acceptButtonStyleClass: 'p-button-sm', + rejectButtonStyleClass: 'p-button-sm p-button-text', + accept: input.accept, + reject: () => input.reject?.(), + }); + } + + showLoading(): void { + this.loadingCount++; + document.body.style.cursor = 'wait'; + } + + hideLoading(): void { + this.loadingCount = Math.max(0, this.loadingCount - 1); + + if (this.loadingCount === 0) { + document.body.style.cursor = 'default'; + } + } + + warn(message: string): void { + this.toast({ + severity: 'warn', + summary: this.translator.translate('lblWarning'), + detail: message, + }); + } + + error(message?: string, detail?: string): void { + this.toast({ + severity: 'error', + summary: message ?? this.translator.translate('error'), + detail, + sticky: true, + }); + } + + success(message?: string, detail?: string): void { + this.toast({ + severity: 'success', + summary: message ?? this.translator.translate('msgSuccess'), + detail, + }); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html new file mode 100644 index 000000000..c64af004a --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html @@ -0,0 +1,32 @@ +
+ @if (renderActiveDirectory) { + + } +
+ + + + diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss new file mode 100644 index 000000000..4f096256e --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss @@ -0,0 +1,101 @@ +:host { + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + background-color: var(--connection-view-default-background-color, #fff) !important; + background-image: none !important; +} + +.active-directory-session-container { + display: flex; + flex-direction: column; + height: 100%; +} + +:host ::ng-deep .ad-dvl-toast.p-toast { + top: 68px; + right: 12px; + width: min(400px, calc(100vw - 24px)); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message { + margin: 0 0 8px; + border: 1px solid transparent; + border-radius: 6px; + box-shadow: none; + overflow: hidden; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-content { + min-height: 52px; + padding: 12px 14px; + display: flex; + align-items: center; + gap: 12px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-success { + border-color: var(--success-message-border-color); + background: var(--success-message-background-color); + color: var(--success-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-info { + border-color: var(--info-message-border-color); + background: var(--info-message-background-color); + color: var(--info-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-warn { + border-color: var(--warning-message-border-color); + background: var(--warning-message-background-color); + color: var(--warning-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-error { + border-color: var(--error-message-border-color); + background: var(--error-message-background-color); + color: var(--error-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-icon { + margin: 0; + width: 16px; + height: 16px; + font-size: 14px; + color: currentColor; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-text { + margin: 0; + flex: 1 1 auto; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-summary { + display: block; + color: currentColor; + font-size: 14px; + font-weight: 600; + line-height: 1.3; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-detail { + margin: 2px 0 0; + color: currentColor; + opacity: 1; + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-icon-close { + color: currentColor; + width: 28px; + height: 28px; + border-radius: 4px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-icon-close:hover { + background: rgba(var(--base-color-rgb), 0.06); +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts new file mode 100644 index 000000000..e6a829b9e --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts @@ -0,0 +1,397 @@ +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + HostBinding, + OnDestroy, + Output, + ViewChild, +} from '@angular/core'; +import { + ActiveDirectoryConnectionParams, + ActiveDirectoryMainHandle, + AD_DATA_PROVIDER, + AD_SESSION_MANAGER, + AD_TRANSLATOR, + AD_UI, + AdConnectionError, + AdConnectionEventType, + matchTranslateKey, +} from '@devolutions/web-active-directory-gui'; +import { DVL_ACTIVE_DIRECTORY_ICON, DVL_WARNING_ICON, JET_AD_URL } from '@gateway/app.constants'; +import { WebClientBaseComponent } from '@shared/bases/base-web-client.component'; +import { GatewayAlertMessageService } from '@shared/components/gateway-alert-message/gateway-alert-message.service'; +import { ActiveDirectoryConnectionParameters } from '@shared/interfaces/connection-params.interfaces'; +import { ActiveDirectoryFormDataInput } from '@shared/interfaces/forms.interfaces'; +import { ComponentStatus } from '@shared/models/component-status.model'; +import { AnalyticService, ProtocolString } from '@shared/services/analytic.service'; +import { UtilsService } from '@shared/services/utils.service'; +import { DefaultLdapPort, WebClientService } from '@shared/services/web-client.service'; +import { WebSessionService } from '@shared/services/web-session.service'; +import { EMPTY, from, Observable, of, Subscription } from 'rxjs'; +import { catchError, map, switchMap, takeUntil } from 'rxjs/operators'; +import { v4 as uuidv4 } from 'uuid'; +import { ActiveDirectoryDataService } from './services/active-directory-data.service'; +import { ActiveDirectorySessionManagerService } from './services/active-directory-session-manager.service'; +import { ActiveDirectorySessionStoreService } from './services/active-directory-session-store.service'; +import { ActiveDirectoryTranslatorService } from './services/active-directory-translator.service'; +import { ActiveDirectoryUiService } from './services/active-directory-ui.service'; + +@Component({ + standalone: false, + templateUrl: 'web-client-active-directory.component.html', + styleUrls: ['web-client-active-directory.component.scss'], + providers: [ + ActiveDirectoryDataService, + ActiveDirectorySessionManagerService, + ActiveDirectorySessionStoreService, + ActiveDirectoryTranslatorService, + ActiveDirectoryUiService, + { provide: AD_DATA_PROVIDER, useExisting: ActiveDirectoryDataService }, + { provide: AD_SESSION_MANAGER, useExisting: ActiveDirectorySessionManagerService }, + { provide: AD_TRANSLATOR, useExisting: ActiveDirectoryTranslatorService }, + { provide: AD_UI, useExisting: ActiveDirectoryUiService }, + ], +}) +export class WebClientActiveDirectoryComponent extends WebClientBaseComponent implements AfterViewInit, OnDestroy { + @HostBinding('attr.data-ad-theme-bridge') readonly adBridge = '1'; + + @Output() readonly componentStatus = new EventEmitter(); + @Output() readonly sizeChange = new EventEmitter(); + + @ViewChild('activeDirectoryContainer') activeDirectoryContainerRef: ElementRef; + + formData: ActiveDirectoryFormDataInput; + renderActiveDirectory = false; + + private activeDirectoryHandle: ActiveDirectoryMainHandle | null = null; + private activeDirectoryEventsSubscription: Subscription | null = null; + private renderActiveDirectoryTimeout: ReturnType | null = null; + + constructor( + protected gatewayAlertMessageService: GatewayAlertMessageService, + protected analyticService: AnalyticService, + private readonly utils: UtilsService, + private readonly webClientService: WebClientService, + private readonly webSessionService: WebSessionService, + private readonly activeDirectoryDataService: ActiveDirectoryDataService, + private readonly activeDirectorySessionManager: ActiveDirectorySessionManagerService, + private readonly activeDirectoryTranslator: ActiveDirectoryTranslatorService, + private readonly activeDirectoryUiService: ActiveDirectoryUiService, + ) { + super(gatewayAlertMessageService, analyticService); + } + + ngAfterViewInit(): void { + this.renderActiveDirectoryTimeout = setTimeout(() => { + this.renderActiveDirectory = true; + this.renderActiveDirectoryTimeout = null; + }); + } + + override ngOnDestroy(): void { + if (this.renderActiveDirectoryTimeout) { + clearTimeout(this.renderActiveDirectoryTimeout); + this.renderActiveDirectoryTimeout = null; + } + + this.activeDirectoryEventsSubscription?.unsubscribe(); + + if (this.currentStatus.isInitialized && !this.currentStatus.isDisabled) { + this.activeDirectoryHandle?.close(); + this.webClientConnectionClosed(); + } + + this.activeDirectorySessionManager.clearWebSessionConfig(this.webSessionId); + + super.ngOnDestroy(); + } + + onMainReady(handle: ActiveDirectoryMainHandle): void { + this.activeDirectoryHandle = handle; + this.subscribeToActiveDirectoryEvents(handle); + this.startConnectionProcess(); + } + + startTerminationProcess(): void { + this.currentStatus.isDisabledByUser = true; + this.sendTerminateSessionCmd(); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + sendTerminateSessionCmd(): void { + if (!this.activeDirectoryHandle || !this.currentStatus.isInitialized) { + return; + } + + this.currentStatus.isInitialized = false; + this.activeDirectoryHandle.close(); + } + + private startConnectionProcess(): void { + this.getFormData() + .pipe( + takeUntil(this.destroyed$), + switchMap(() => this.fetchParameters(this.formData)), + switchMap((params) => this.webClientService.fetchActiveDirectoryToken(params)), + switchMap((params) => from(this.activeDirectoryDataService.initializeWasm()).pipe(map(() => params))), + map((params) => this.toActiveDirectoryConnectionParams(params)), + switchMap((params) => this.callConnect(params)), + catchError((error) => { + this.handleSessionError(this.toAdConnectionError(error), true); + return EMPTY; + }), + ) + .subscribe(); + } + + private callConnect(connectionParameters: ActiveDirectoryConnectionParams): Observable { + if (!this.activeDirectoryHandle) { + return EMPTY; + } + + this.loading = true; + return this.activeDirectoryHandle.connect(connectionParameters); + } + + private getFormData(): Observable { + return from(this.webSessionService.getWebSession(this.webSessionId)).pipe( + map((currentWebSession) => { + this.formData = currentWebSession.data as ActiveDirectoryFormDataInput; + this.sessionInfoUsername = this.formData.username; + this.refreshSessionInfo(); + }), + ); + } + + private fetchParameters(formData: ActiveDirectoryFormDataInput): Observable { + const sessionId = uuidv4(); + const extractedData = this.utils.string.extractHostnameAndPort( + formData.hostname, + Number(formData.port) || DefaultLdapPort, + ); + const gatewayAddress = new URL(JET_AD_URL, window.location.href).href; + + this.sessionInfoUrl = this.toUserFacingUrl(gatewayAddress); + this.sessionInfoUsername = formData.username; + this.refreshSessionInfo(); + + return of({ + host: extractedData.hostname, + port: extractedData.port, + username: formData.username, + password: formData.password, + domain: formData.domain ?? '', + gatewayAddress, + sessionId, + useLdaps: formData.useLdaps, + organizationalUnit: formData.organizationalUnit, + }); + } + + private toActiveDirectoryConnectionParams( + connectionParameters: ActiveDirectoryConnectionParameters, + ): ActiveDirectoryConnectionParams { + this.activeDirectorySessionManager.setWebSessionConfig({ + sessionId: connectionParameters.sessionId, + // Gateway standalone does not expose a DVLS gateway identifier here. + gatewayId: '', + hostname: connectionParameters.host, + port: connectionParameters.port, + username: connectionParameters.username, + password: connectionParameters.password, + domain: connectionParameters.domain, + useSSL: connectionParameters.useLdaps, + token: connectionParameters.token, + kdcProxyUrl: connectionParameters.kdcUrl, + getGatewayWebSocketUrl: (path: string) => new URL(path, window.location.href).href, + }); + + return { + connectionSettings: { + host: connectionParameters.host, + port: connectionParameters.port.toString(), + username: connectionParameters.username, + password: connectionParameters.password, + domain: connectionParameters.domain ?? '', + gatewayUrl: connectionParameters.gatewayAddress, + token: connectionParameters.token, + kdcUrl: connectionParameters.kdcUrl, + }, + isLdaps: connectionParameters.useLdaps, + sessionId: connectionParameters.sessionId, + sessionRecordingTokens: { + gatewayId: null, + recordingUrl: null, + shouldStartRecording: 'no', + }, + }; + } + + private subscribeToActiveDirectoryEvents(handle: ActiveDirectoryMainHandle): void { + this.activeDirectoryEventsSubscription?.unsubscribe(); + this.activeDirectoryEventsSubscription = handle.adConnectionEvents + .pipe(takeUntil(this.destroyed$)) + .subscribe((event) => { + switch (event.type) { + case AdConnectionEventType.Connected: + this.handleSessionStarted(); + break; + case AdConnectionEventType.Terminated: + this.handleSessionTerminated(this.activeDirectoryTranslator.translate('msgSessionTerminated')); + break; + case AdConnectionEventType.Error: + this.handleSessionError(event.error, !this.currentStatus.isInitialized); + break; + case AdConnectionEventType.Warning: + this.activeDirectoryUiService.warn(this.activeDirectoryTranslator.translate(event.message)); + break; + case AdConnectionEventType.Success: + this.activeDirectoryUiService.success(this.activeDirectoryTranslator.translate(event.message)); + break; + } + }); + } + + private handleSessionStarted(): void { + this.loading = false; + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_ACTIVE_DIRECTORY_ICON); + this.initializeStatus(); + this.handleConnectionSuccess(); + } + + private handleSessionTerminated(message: string): void { + this.loading = false; + + if (this.currentStatus.isDisabledByUser) { + return; + } + + this.currentStatus.terminationMessage = { + summary: message, + severity: 'error', + }; + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_WARNING_ICON); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + private handleSessionError(error: AdConnectionError, disableComponent = true): void { + this.loading = false; + const summary = this.getErrorSummary(error); + const detail = this.getErrorDetail(error, summary); + + this.activeDirectoryUiService.error(summary, detail); + console.error(detail); + + if (!disableComponent) { + return; + } + + this.currentStatus.terminationMessage = { + summary, + detail, + severity: 'error', + }; + + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_WARNING_ICON); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + private getErrorSummary(error: AdConnectionError): string { + const causeTranslationKey = this.getErrorTranslationKeyFromCause(error.cause); + if (causeTranslationKey) { + return this.activeDirectoryTranslator.translate(causeTranslationKey); + } + + if (error.code === 'other') { + return this.activeDirectoryTranslator.translate('AnUnexpectedErrorOccured'); + } + + return this.activeDirectoryTranslator.translate(matchTranslateKey(error.code)); + } + + private getErrorDetail(error: AdConnectionError, summary: string): string | undefined { + const causeMessage = typeof error.cause === 'string' ? error.cause : undefined; + const translatedMessage = this.activeDirectoryTranslator.translate(error.message); + + if (causeMessage && causeMessage !== translatedMessage) { + return causeMessage; + } + + return translatedMessage && translatedMessage !== summary ? translatedMessage : undefined; + } + + private getErrorTranslationKeyFromCause(cause: unknown): string | undefined { + if (typeof cause !== 'string') { + return undefined; + } + + const normalizedCause = cause.toLowerCase(); + + if ( + normalizedCause.includes('entry_exists') || + normalizedCause.includes('entryalreadyexists') || + normalizedCause.includes('entry already exists') || + normalizedCause.includes('problem 6005') + ) { + return 'AlreadyExists'; + } + + return undefined; + } + + private initializeStatus(): void { + this.currentStatus = { + id: this.webSessionId, + isInitialized: true, + isDisabled: false, + isDisabledByUser: false, + }; + } + + private handleConnectionSuccess(): void { + this.hideSpinnerOnly = true; + this.activeDirectoryUiService.success(this.activeDirectoryTranslator.translate('msgConnected')); + this.analyticHandle = this.analyticService.sendOpenEvent(this.getProtocol()); + } + + private disableComponentStatus(): void { + if (this.currentStatus.isDisabled) { + return; + } + + this.currentStatus.id ??= this.webSessionId; + this.currentStatus.isDisabled = true; + this.componentStatus.emit(this.currentStatus); + } + + private toAdConnectionError(error: unknown): AdConnectionError { + if (this.isAdConnectionError(error)) { + return error; + } + + return { + code: 'other', + message: error instanceof Error ? error.message : `${error}`, + cause: error, + }; + } + + private isAdConnectionError(error: unknown): error is AdConnectionError { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + 'message' in error && + typeof (error as AdConnectionError).message === 'string' + ); + } + + protected getProtocol(): ProtocolString { + return 'ActiveDirectory'; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html new file mode 100644 index 000000000..fc1628a80 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html @@ -0,0 +1,80 @@ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ @if (form.get('port').hasError('invalidPort') && form.get('port').touched) { +
+ Port must be between 1 and 65535. +
+ } +
+ +
+
+ + +
+
+ +
+ +
+ +
+
+
diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss new file mode 100644 index 000000000..591d871ea --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss @@ -0,0 +1,17 @@ +.active-directory-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + + .gateway-form-group { + margin-bottom: 0; + } + + .gateway-form-input { + margin-top: 0.25rem; + } + + .organizational-unit-form-group { + padding-bottom: 1rem; + } +} \ No newline at end of file diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts new file mode 100644 index 000000000..2d6820c26 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts @@ -0,0 +1,96 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms'; +import { BaseComponent } from '@shared/bases/base.component'; +import { ActiveDirectoryFormDataInput } from '@shared/interfaces/forms.interfaces'; +import { DefaultLdapPort, DefaultLdapsPort } from '@shared/services/web-client.service'; +import { WebFormService } from '@shared/services/web-form.service'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + standalone: false, + selector: 'active-directory-form', + templateUrl: 'active-directory-form.component.html', + styleUrls: ['active-directory-form.component.scss'], +}) +export class ActiveDirectoryFormComponent extends BaseComponent implements OnInit { + @Input() form: FormGroup; + @Input() inputFormData: ActiveDirectoryFormDataInput; + + constructor(private formService: WebFormService) { + super(); + } + + ngOnInit(): void { + this.addControlsToParentForm(); + this.subscribeToLdapsChanges(); + } + + private addControlsToParentForm(): void { + const useLdaps = this.inputFormData?.useLdaps ?? false; + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'domain', + inputFormData: this.inputFormData, + isRequired: false, + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'port', + inputFormData: this.inputFormData, + isRequired: true, + defaultValue: useLdaps ? DefaultLdapsPort : DefaultLdapPort, + additionalValidator: this.portValidator(), + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'useLdaps', + inputFormData: this.inputFormData, + isRequired: false, + defaultValue: false, + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'organizationalUnit', + inputFormData: this.inputFormData, + isRequired: false, + }); + } + + private subscribeToLdapsChanges(): void { + const useLdapsControl = this.form.get('useLdaps'); + const portControl = this.form.get('port'); + + if (!useLdapsControl || !portControl) { + return; + } + + let previousDefaultPort = useLdapsControl.value ? DefaultLdapsPort : DefaultLdapPort; + + useLdapsControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe((useLdaps: boolean) => { + const nextDefaultPort = useLdaps ? DefaultLdapsPort : DefaultLdapPort; + const currentPort = Number(portControl.value); + + if (currentPort === previousDefaultPort) { + portControl.setValue(nextDefaultPort); + } + + previousDefaultPort = nextDefaultPort; + }); + } + + private portValidator(): ValidatorFn { + return (control: AbstractControl): { [key: string]: unknown } | null => { + const port = Number(control.value); + + if (!Number.isInteger(port) || port < 1 || port > 65535) { + return { invalidPort: { value: control.value } }; + } + + return null; + }; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html index 1fd6391d7..747260805 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html @@ -48,7 +48,7 @@
- +
} + @if (isSelectedProtocolActiveDirectory()) { + + } +
> = { + [Protocol.ActiveDirectory]: 'Domain Controller', + }; + constructor( private fb: FormBuilder, private formService: WebFormService, @@ -248,6 +252,14 @@ export class WebClientFormComponent extends BaseSessionComponent implements OnIn return WebClientProtocol.isProtocolArd(this.getSelectedProtocol()); } + isSelectedProtocolActiveDirectory(): boolean { + return WebClientProtocol.isProtocolActiveDirectory(this.getSelectedProtocol()); + } + + getHostnameLabel(): string { + return this.hostnameLabels[this.getSelectedProtocol()] ?? 'Hostname'; + } + private addMessages(newMessages: ToastMessageOptions[]): void { const areThereNewMessages: boolean = newMessages.some( (newMsg) => diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts index 63c2fbf52..97c4aed91 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts @@ -2,8 +2,10 @@ import { NgOptimizedImage } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; +import { WebActiveDirectoryComponent } from '@devolutions/web-active-directory-gui'; import { ArdToolbarWrapperComponent } from '@gateway/modules/web-client/ard/ard-toolbar-wrapper.component'; import { WebClientArdComponent } from '@gateway/modules/web-client/ard/web-client-ard.component'; +import { ActiveDirectoryFormComponent } from '@gateway/modules/web-client/form/form-components/active-directory/active-directory-form.component'; import { ArdFormComponent } from '@gateway/modules/web-client/form/form-components/ard/ard-form.component'; import { RdpFormComponent } from '@gateway/modules/web-client/form/form-components/rdp/rdp-form.component'; import { SshFormComponent } from '@gateway/modules/web-client/form/form-components/ssh/ssh-form.component'; @@ -31,6 +33,7 @@ import { WebSessionService } from '@shared/services/web-session.service'; import { SharedModule } from '@shared/shared.module'; import { CheckboxModule } from 'primeng/checkbox'; import { KeyFilterModule } from 'primeng/keyfilter'; +import { WebClientActiveDirectoryComponent } from './active-directory/web-client-active-directory.component'; import { ArdQualityModeControlComponent } from './form/form-controls/ard-quality-mode-control/ard-quality-mode-control.component'; import { AutoClipboardControlComponent } from './form/form-controls/auto-clipboard-control/auto-clipboard-control.component'; import { ColorFormatControlComponent } from './form/form-controls/color-format-control/color-format-control.component'; @@ -72,6 +75,7 @@ const routes: Routes = [ ArdToolbarWrapperComponent, RdpToolbarWrapperComponent, VncToolbarWrapperComponent, + WebActiveDirectoryComponent, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ @@ -82,11 +86,13 @@ const routes: Routes = [ WebClientTelnetComponent, WebClientSshComponent, WebClientArdComponent, + WebClientActiveDirectoryComponent, WebClientFormComponent, RdpFormComponent, SshFormComponent, VncFormComponent, ArdFormComponent, + ActiveDirectoryFormComponent, UsernameControlComponent, PasswordControlComponent, ExtendedClipboardControlComponent, @@ -114,7 +120,6 @@ const routes: Routes = [ NetScanComponent, ], exports: [DynamicTabComponent, WebClientFormComponent, NetScanComponent], - providers: [], }) export class WebClientModule { constructor(webSessionService: WebSessionService) { @@ -124,6 +129,7 @@ export class WebClientModule { [Protocol.SSH]: WebClientSshComponent, [Protocol.VNC]: WebClientVncComponent, [Protocol.ARD]: WebClientArdComponent, + [Protocol.ActiveDirectory]: WebClientActiveDirectoryComponent, }); } } diff --git a/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts b/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts index 516e675e6..3b119b2a6 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts @@ -6,6 +6,7 @@ export enum Protocol { SSH = 2, VNC = 3, ARD = 4, + ActiveDirectory = 5, } enum Tooltips { @@ -14,11 +15,16 @@ enum Tooltips { 'Secure Shell' = 'SSH', 'Virtual Network Computing' = 'VNC', 'Apple Remote Desktop' = 'ARD', + 'Active Directory' = 'ActiveDirectory', } export type ProtocolControlMap = { [key in Protocol]?: string[] }; namespace WebClientProtocol { + const DisplayNames: Partial> = { + ActiveDirectory: 'Active Directory', + }; + export function getEnumKey(value: Protocol): string { return Protocol[value]; } @@ -33,7 +39,7 @@ namespace WebClientProtocol { return Object.keys(Protocol) .filter((key) => Number.isNaN(Number(key)) && typeof Protocol[key] === 'number') .map((key) => { - const label: string = key; + const label: string = DisplayNames[key as keyof typeof Protocol] ?? key; const value: Protocol = Protocol[key as keyof typeof Protocol]; const tooltipText = tooltipsLookup[key] || ''; @@ -56,5 +62,9 @@ namespace WebClientProtocol { export function isProtocolArd(protocol: Protocol): boolean { return protocol === Protocol.ARD; } + + export function isProtocolActiveDirectory(protocol: Protocol): boolean { + return protocol === Protocol.ActiveDirectory; + } } export { WebClientProtocol }; diff --git a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts index 354301157..a7616b79e 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts @@ -77,3 +77,26 @@ export interface SshConnectionParameters { privateKey?: string; privateKeyPassphrase?: string; } + +export interface ActiveDirectoryConnectionParameters { + host: string; + port: number; + username: string; + password: string; + domain?: string; + gatewayAddress: string; + token?: string; + kdcUrl?: string; + sessionId?: string; + useLdaps: boolean; + organizationalUnit?: string; +} + +export function isActiveDirectoryConnectionParameters(params: unknown): params is ActiveDirectoryConnectionParameters { + return ( + typeof params === 'object' && + params !== null && + 'useLdaps' in params && + typeof (params as ActiveDirectoryConnectionParameters).useLdaps === 'boolean' + ); +} diff --git a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts index 4b31e97fd..ec9a18233 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts @@ -19,7 +19,8 @@ export type FormDataUnion = | VncFormDataInput | ArdFormDataInput | SSHFormDataInput - | TelnetFormDataInput; + | TelnetFormDataInput + | ActiveDirectoryFormDataInput; export interface RdpFormDataInput { autoComplete: AutoCompleteInput; @@ -86,3 +87,15 @@ export interface SSHFormDataInput { sshPrivateKey?: string; }; } + +export interface ActiveDirectoryFormDataInput { + autoComplete: AutoCompleteInput; + hostname: string; + username: string; + password: string; + domain?: string; + port: number; + useLdaps: boolean; + organizationalUnit?: string; + protocol: number; +} diff --git a/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts b/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts index b96fa403d..617154c4f 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts @@ -1,4 +1,5 @@ import { ComponentRef, ElementRef, Type } from '@angular/core'; +import type { WebClientActiveDirectoryComponent } from '@gateway/modules/web-client/active-directory/web-client-active-directory.component'; import type { WebClientArdComponent } from '@gateway/modules/web-client/ard/web-client-ard.component'; import type { WebClientFormComponent } from '@gateway/modules/web-client/form/web-client-form.component'; import type { WebClientRdpComponent } from '@gateway/modules/web-client/rdp/web-client-rdp.component'; @@ -11,6 +12,7 @@ import { DesktopSize } from '@shared/models/desktop-size'; import { v4 as uuidv4 } from 'uuid'; import { BaseComponent } from '../bases/base.component'; import { + ActiveDirectoryFormDataInput, ArdFormDataInput, RdpFormDataInput, SSHFormDataInput, @@ -19,6 +21,7 @@ import { } from '../interfaces/forms.interfaces'; export type WebSessionComponentType = + | Type | Type | Type | Type @@ -30,6 +33,7 @@ export type WebSessionComponentType = export interface SessionDataTypeMap { WebClientFormComponent: never; MainPanelComponent: never; + WebClientActiveDirectoryComponent: ActiveDirectoryFormDataInput; WebClientArdComponent: ArdFormDataInput; WebClientRdpComponent: RdpFormDataInput; WebClientSshComponent: SSHFormDataInput; @@ -38,6 +42,7 @@ export interface SessionDataTypeMap { } export interface SessionTypeMap { + WebClientActiveDirectoryComponent: WebClientActiveDirectoryComponent; WebClientArdComponent: WebClientArdComponent; WebClientFormComponent: WebClientFormComponent; WebClientRdpComponent: WebClientRdpComponent; diff --git a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts index fe00ce865..f897ba961 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts @@ -4,9 +4,11 @@ import { BaseComponent } from '@shared/bases/base.component'; import { ScreenSize } from '@shared/enums/screen-size.enum'; import { Protocol, WebClientProtocol } from '@shared/enums/web-client-protocol.enum'; import { + ActiveDirectoryConnectionParameters, IronARDConnectionParameters, IronRDPConnectionParameters, IronVNCConnectionParameters, + isActiveDirectoryConnectionParameters, SessionTokenParameters, SshConnectionParameters, TelnetConnectionParameters, @@ -28,6 +30,8 @@ export const DefaultSshPort: number = 22; export const DefaultTelnetPort: number = 23; export const DefaultVncPort: number = 5900; export const DefaultArdPort: number = 5900; +export const DefaultLdapPort: number = 389; +export const DefaultLdapsPort: number = 636; @Injectable() export class WebClientService extends BaseComponent { @@ -75,18 +79,46 @@ export class WebClientService extends BaseComponent { return DefaultVncPort; case Protocol.ARD: return DefaultArdPort; + case Protocol.ActiveDirectory: + return DefaultLdapPort; default: throw new Error(`Getting default port, unsupported protocol: ${protocol}`); } } + private getApplicationProtocol( + protocol: Protocol, + connectionParameters: + | TelnetConnectionParameters + | SshConnectionParameters + | IronRDPConnectionParameters + | IronVNCConnectionParameters + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, + ): string { + if (protocol === Protocol.ActiveDirectory) { + if (!isActiveDirectoryConnectionParameters(connectionParameters)) { + console.error('invalid active directory connection settings', connectionParameters); + + const error = new Error('errConnectionFailed') as Error & { code: string }; + error.code = 'errConnectionFailed'; + throw error; + } + + return connectionParameters.useLdaps ? 'ldaps' : 'ldap'; + } + + return WebClientProtocol.getEnumKey(protocol).toLowerCase(); + } + private handleProtocolTokenRequest< T extends | TelnetConnectionParameters | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters - | IronARDConnectionParameters, + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, >(protocol: Protocol, connectionParameters: T): Observable { return this.generateProtocolToken(protocol, connectionParameters).pipe( takeUntil(this.destroyed$), @@ -102,15 +134,17 @@ export class WebClientService extends BaseComponent { | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters - | IronARDConnectionParameters, + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, ): Observable< | TelnetConnectionParameters | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters > { - const protocolStr: string = WebClientProtocol.getEnumKey(protocol).toLowerCase(); + const protocolStr: string = this.getApplicationProtocol(protocol, connectionParameters); const data: SessionTokenParameters = { content_type: 'ASSOCIATION', @@ -151,6 +185,12 @@ export class WebClientService extends BaseComponent { return this.handleProtocolTokenRequest(Protocol.ARD, connectionParameters); } + fetchActiveDirectoryToken( + connectionParameters: ActiveDirectoryConnectionParameters, + ): Observable { + return this.handleProtocolTokenRequest(Protocol.ActiveDirectory, connectionParameters); + } + fetchNetScanToken(): Observable { const data: SessionTokenParameters = { content_type: 'NETSCAN', diff --git a/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss b/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss new file mode 100644 index 000000000..c2b90758e --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss @@ -0,0 +1,254 @@ +:where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog +) { + --ad-color-primary: var(--blue-primary); + --ad-on-primary: #fff; + --ad-text-primary: var(--text-color-primary); + --ad-text-secondary: var(--text-color-secondary); + --ad-text-disabled: var(--grey-600); + --ad-border: var(--border-secondary-color); + --ad-border-color: var(--border-secondary-color); + --ad-input-border: var(--input-border-color); + --ad-bg: var(--connection-view-default-background-color); + --ad-bg-muted: var(--background-secondary-color); + --ad-error: var(--message-error-color); + --ad-toolbar-bg: var(--connection-view-default-background-color); + --ad-title: var(--title-color); + --ad-accent-text: var(--tree-selector-text-highlight-color); + --ad-splitter: var(--border-secondary-color); + --ad-tree-item-highlight-bg: var(--main-tree-item-highlight-color, #e7f4ff); + --ad-tab-list-border-color: var(--dialog-menu-border-color); + --ad-tab-text-color: var(--dialog-menu-text-color); + --ad-tab-hover-text-color: var(--dialog-menu-highlight-text-color); + --ad-tab-active-color: var(--fieldset-title-color); + --ad-tab-nav-button-color: var(--ad-tab-text-color); + --ad-tab-nav-button-hover-color: var(--ad-tab-hover-text-color); + --ad-table-border-color: var(--table-border-color); + --ad-table-header-bg: #f9f9f9; + --ad-table-header-hover-bg: #f2f2f2; + --ad-table-header-text: #404040; + --ad-table-row-text: var(--table-row-text-color); + --ad-table-row-bg: var(--table-row-background-color); + --ad-table-row-alt-bg: var(--table-child-row-background-color); + --ad-table-row-hover-bg: var(--hover-bg-color); + --ad-table-results-text: var(--table-row-text-color); + --ad-table-action-color: var(--table-row-text-color); + --ad-table-action-hover-bg: var(--hover-bg-color); + --ad-table-action-hover-color: var(--table-link-color); + --ad-attributes-grid-row-bg: var(--table-row-background-color); + --ad-attributes-grid-row-hover-bg: var(--table-row-background-color); + --ad-attributes-grid-header-bg: #f9f9f9; + --ad-attributes-grid-header-hover-bg: #f2f2f2; + --ad-attributes-grid-header-border-color: #f9f9f9; + --ad-attributes-grid-border-color: var(--table-border-color); + --ad-attributes-grid-text-color: var(--table-row-text-color); + --ad-attributes-grid-header-text-color: #404040; + --ad-attributes-grid-icon-color: var(--btn-icon-color, var(--table-row-text-color)); + --ad-attributes-grid-icon-hover-color: var(--btn-icon-color, var(--table-link-color)); + --ad-members-action-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-action-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-members-search-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-search-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-attributes-grid-paginator-page-bg: transparent; + --ad-attributes-grid-paginator-page-color: var(--table-row-text-color); + --ad-attributes-grid-paginator-page-selected-bg: var(--dropdown-item-highlight-background-color, #21374f); + --ad-attributes-grid-paginator-page-selected-color: var(--table-link-color, #3aa3ff); + --ad-attributes-grid-paginator-dropdown-bg: transparent; + --ad-attributes-grid-paginator-dropdown-color: var(--text-color-primary, #fff); + --ad-attributes-grid-paginator-dropdown-border-color: var(--input-border-color, var(--table-border-color)); + --ad-attributes-grid-paginator-dropdown-border-width: 0.8px; + --ad-attributes-grid-paginator-dropdown-border-radius: 8px; + --ad-attributes-grid-results-text: var(--table-column-title-color, var(--text-color-secondary)); + --ad-tree-bg: var(--connection-view-default-background-color, var(--background-secondary-color)); + --ad-tree-menu-bg: var(--panel-menu-background-color, var(--ad-tree-bg)); + --ad-tree-menu-item-color: var(--dialog-menu-text-color, var(--text-color-secondary)); + --ad-tree-menu-item-hover-bg: var(--panel-menu-hover-background-color, var(--hover-bg-color)); + --ad-tree-menu-item-active-bg: var(--panel-menu-highlight-background-color, var(--ad-tree-item-highlight-bg)); + --ad-tree-menu-item-active-color: var(--panel-menu-highlight-text-color, var(--ad-accent-text)); + --ad-tree-menu-muted-color: var(--text-color-secondary, var(--ad-text-secondary)); + --ad-font-family: var(--font-open-sans, 'Open Sans'); + --ad-accent-rgb: var(--base-accent-rgb); + --ad-sensitive: var(--sensitive-color, #ffa500); + --ad-label-readonly: var(--label-readonly-color); + + /* Button colors */ + --ad-button-primary-bg: var(--dialog-footer-default-button-background-color, var(--blue-primary)); + --ad-button-primary-bg-hover: var(--dialog-footer-default-button-background-hover-color, var(--blue-800)); + --ad-button-primary-text: var(--dialog-footer-default-button-text-color, var(--blue-primary-text-color, #fff)); + --ad-button-cancel-bg: var(--bg-100, #f2f7fc); + --ad-button-cancel-bg-hover: var(--bg-200, #e6edf5); + --ad-button-cancel-border-color: var(--border-neutral-200, #c2c3c6); + --ad-button-cancel-border-color-hover: var(--border-neutral-200, #c2c3c6); + --ad-button-cancel-border-width: 0.8px; + --ad-button-cancel-text: var(--text-brand, #0068c3); + + /* Secondary button — light */ + --ad-button-secondary-bg: var(--button-secondary-fill, color-mix(in srgb, var(--ad-color-primary) 10%, white)); + --ad-button-secondary-bg-hover: var(--button-secondary-hover-fill, color-mix(in srgb, var(--ad-color-primary) 18%, white)); + --ad-button-secondary-border-color: var(--button-secondary-border, color-mix(in srgb, var(--ad-color-primary) 30%, transparent)); + --ad-button-secondary-border-color-hover: var(--button-secondary-hover-border, color-mix(in srgb, var(--ad-color-primary) 50%, transparent)); + --ad-button-secondary-border-width: 1px; + --ad-button-secondary-text: var(--button-secondary-text, var(--ad-color-primary)); +} + +html[data-theme='dark'] + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +body[data-theme='dark'] + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +html.dark-theme + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +body.dark-theme + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ) { + --connection-view-default-background-color: #0c111d; + --ad-color-primary: var(--blue-800); + --ad-on-primary: #fff; + --ad-text-primary: var(--text-color-primary); + --ad-text-secondary: var(--text-color-secondary); + --ad-text-disabled: var(--grey-600); + --ad-border: var(--border-secondary-color); + --ad-border-color: var(--border-secondary-color); + --ad-input-border: var(--input-border-color); + --ad-bg: #0c111d; + --ad-bg-muted: var(--background-secondary-color); + --ad-error: var(--message-error-color); + --ad-toolbar-bg: var(--connection-view-default-background-color); + --ad-title: var(--title-color); + --ad-accent-text: var(--tree-selector-text-highlight-color); + --ad-splitter: var(--border-secondary-color); + --ad-tree-item-highlight-bg: var(--main-tree-item-highlight-color, #143857); + --ad-tab-list-border-color: var(--dialog-menu-border-color); + --ad-tab-text-color: var(--dialog-menu-text-color); + --ad-tab-hover-text-color: var(--dialog-menu-highlight-text-color); + --ad-tab-active-color: var(--fieldset-title-color); + --ad-tab-nav-button-color: var(--ad-tab-text-color); + --ad-tab-nav-button-hover-color: var(--ad-tab-hover-text-color); + --ad-table-border-color: var(--table-border-color); + --ad-table-header-bg: #0c111d; + --ad-table-header-hover-bg: #1c222e; + --ad-table-header-text: var(--table-column-title-color); + --ad-table-row-text: var(--table-row-text-color); + --ad-table-row-bg: var(--table-row-background-color); + --ad-table-row-alt-bg: var(--table-child-row-background-color); + --ad-table-row-hover-bg: var(--hover-bg-color); + --ad-table-results-text: var(--table-row-text-color); + --ad-table-action-color: var(--table-row-text-color); + --ad-table-action-hover-bg: var(--hover-bg-color); + --ad-table-action-hover-color: var(--table-link-color); + --ad-attributes-grid-row-bg: #161b26; + --ad-attributes-grid-row-hover-bg: #161b26; + --ad-attributes-grid-header-bg: #0c111d; + --ad-attributes-grid-header-hover-bg: #1c222e; + --ad-attributes-grid-header-border-color: #0c111d; + --ad-attributes-grid-border-color: #2a2e39; + --ad-attributes-grid-text-color: #c2c3c6; + --ad-attributes-grid-header-text-color: #c2c3c6; + --ad-attributes-grid-icon-color: var(--btn-icon-color, var(--table-row-text-color)); + --ad-attributes-grid-icon-hover-color: var(--btn-icon-color, var(--table-link-color)); + --ad-members-action-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-action-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-members-search-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-search-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-attributes-grid-paginator-page-bg: transparent; + --ad-attributes-grid-paginator-page-color: #c2c3c6; + --ad-attributes-grid-paginator-page-selected-bg: #21374f; + --ad-attributes-grid-paginator-page-selected-color: #3aa3ff; + --ad-attributes-grid-paginator-dropdown-bg: #0c111d; + --ad-attributes-grid-paginator-dropdown-color: #96989d; + --ad-attributes-grid-paginator-dropdown-border-color: #2f3441; + --ad-attributes-grid-paginator-dropdown-border-width: 0.8px; + --ad-attributes-grid-paginator-dropdown-border-radius: 8px; + --ad-attributes-grid-results-text: #96989d; + --ad-tree-bg: var(--background-secondary-color, var(--connection-view-default-background-color)); + --ad-tree-menu-bg: #0c111d; + --ad-tree-menu-item-color: #96989d; + --ad-tree-menu-item-hover-bg: #0f1f33; + --ad-tree-menu-item-active-bg: #102a45; + --ad-tree-menu-item-active-color: #3aa3ff; + --ad-tree-menu-muted-color: #96989d; + --ad-font-family: var(--font-open-sans, 'Open Sans'); + --ad-accent-rgb: var(--base-accent-rgb); + --ad-sensitive: var(--sensitive-color, #ffa500); + --ad-label-readonly: var(--label-readonly-color); + + /* Button colors */ + --ad-button-primary-bg: var(--dialog-footer-default-button-background-color, var(--blue-primary)); + --ad-button-primary-bg-hover: var(--dialog-footer-default-button-background-hover-color, var(--blue-800)); + --ad-button-primary-text: var(--dialog-footer-default-button-text-color, var(--blue-primary-text-color, #fff)); + --ad-button-cancel-bg: #161b26; + --ad-button-cancel-bg-hover: #1c222e; + --ad-button-cancel-border-color: #2f3441; + --ad-button-cancel-border-color-hover: #2f3441; + --ad-button-cancel-border-width: 0.8px; + --ad-button-cancel-text: #3aa3ff; + + /* Secondary button — exact values from computed dvl-button[variant=secondary] dark */ + --ad-button-secondary-bg: #1a2b40; + --ad-button-secondary-bg-hover: #1f3552; + --ad-button-secondary-border-color: rgba(58, 163, 255, 0.15); + --ad-button-secondary-border-color-hover: rgba(58, 163, 255, 0.28); + --ad-button-secondary-border-width: 0.8px; + --ad-button-secondary-text: #3aa3ff; +} diff --git a/webapp/pnpm-lock.yaml b/webapp/pnpm-lock.yaml index 07e635fa9..76c231983 100644 --- a/webapp/pnpm-lock.yaml +++ b/webapp/pnpm-lock.yaml @@ -69,9 +69,15 @@ importers: '@devolutions/iron-remote-desktop-vnc': specifier: 0.7.0 version: 0.7.0 + '@devolutions/ldap-wasm-js': + specifier: ^0.3.4 + version: 0.3.4 '@devolutions/terminal-shared': specifier: ^1.4.1 version: 1.4.1(typescript@5.9.3) + '@devolutions/web-active-directory-gui': + specifier: 0.0.13 + version: 0.0.13(ffa535f5f8fdad6835c93e9f6c4f1c9b) '@devolutions/web-ssh-gui': specifier: ^0.7.0 version: 0.7.0(@devolutions/terminal-shared@1.4.1(typescript@5.9.3)) @@ -85,11 +91,11 @@ importers: specifier: ^1.2.5 version: 1.2.5 '@primeuix/themes': - specifier: ^1.2.5 - version: 1.2.5 + specifier: 2.0.3 + version: 2.0.3 '@primeuix/utils': - specifier: ^0.6.3 - version: 0.6.3 + specifier: 0.7.2 + version: 0.7.2 '@xterm/addon-clipboard': specifier: ^0.1.0 version: 0.1.0(@xterm/xterm@5.5.0) @@ -448,6 +454,7 @@ packages: '@angular/animations@20.3.18': resolution: {integrity: sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.' peerDependencies: '@angular/core': 20.3.18 @@ -556,6 +563,7 @@ packages: '@angular/platform-browser-dynamic@20.3.18': resolution: {integrity: sha512-NyTobOGYVzGmPmtI+3lxMzxi0TbLq4SRNQ2ENEJAt6k2JnMmHBm483ppLRAM47nGlDdiraW0IX93EtYYNkiK3g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: '@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.' peerDependencies: '@angular/common': 20.3.18 '@angular/compiler': 20.3.18 @@ -1238,48 +1246,56 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64-musl@2.1.3': resolution: {integrity: sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-arm64@2.1.3': resolution: {integrity: sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64-musl@2.1.3': resolution: {integrity: sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64@2.1.3': resolution: {integrity: sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -1329,9 +1345,33 @@ packages: '@devolutions/iron-remote-desktop@0.11.0': resolution: {integrity: sha512-ZH8xfabDA+6rAXkW2V3XgRdj0LHOsHoXAzars/H22DmKsHM39LdMfheKFUR3qLaFDM8mjf6DTFngzjgr+Byx0Q==} + '@devolutions/ldap-wasm-js@0.3.4': + resolution: {integrity: sha512-9gI9j7yGL2MKEG1wEQExP6AF8ON31T1lGsmMGVmccbouXOoHI76ti7rqhkqYotTB0bDHA1yO9ljzVr80lXwubg==, tarball: https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/ldap-wasm-js/-/@devolutions/ldap-wasm-js-0.3.4.tgz} + '@devolutions/terminal-shared@1.4.1': resolution: {integrity: sha512-GLfuO3/sLKXwxvFczTPe+QSF8r2htV/2uvdfkxg4NzunFB9y4vLOnNjndCzN+1R5p0H1tv0wEnGTFcKbJQdvsQ==} + '@devolutions/web-active-directory-gui@0.0.13': + resolution: {integrity: sha512-2PxeVcXUrwowlqndjygOWBGZcvVgpElMrVZa97GSeEKbM5SXh8w10qjOweNWoiGsa154RkyfUBv7EMbfMADNmA==, tarball: https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/web-active-directory-gui/-/@devolutions/web-active-directory-gui-0.0.13.tgz} + peerDependencies: + '@angular/animations': 20.3.18 + '@angular/cdk': '>=19.2.19 <21.0.0' + '@angular/common': 20.3.18 + '@angular/core': 20.3.18 + '@angular/forms': 20.3.18 + '@angular/platform-browser': 20.3.18 + '@angular/platform-browser-dynamic': 20.3.18 + '@angular/router': 20.3.18 + '@devolutions/icons': ^5.0.7 + '@primeuix/styled': 0.7.4 + '@primeuix/themes': 2.0.3 + primeng: '>=19.1.4 <21.0.0' + rxjs: ~7.8.0 + zone.js: 0.15.0 + peerDependenciesMeta: + primeng: + optional: true + '@devolutions/web-ssh-gui@0.7.0': resolution: {integrity: sha512-OBXbCUMLBFknC7LisYk2iN0RSUJelRIxpJhHZiR7syOOZ/2Ga4+LlNWAH8gwwDoGyGpztgJbqHQzyEQWa6urAQ==} peerDependencies: @@ -2299,42 +2339,49 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -2450,36 +2497,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2510,11 +2563,15 @@ packages: '@primeuix/styles@1.2.5': resolution: {integrity: sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==} - '@primeuix/themes@1.2.5': - resolution: {integrity: sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ==} + '@primeuix/themes@2.0.3': + resolution: {integrity: sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==} + + '@primeuix/utils@0.6.4': + resolution: {integrity: sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==} + engines: {node: '>=12.11.0'} - '@primeuix/utils@0.6.3': - resolution: {integrity: sha512-/SLNQSKQ73WbBIsflKVqbpVjCfFYvQO3Sf1LMheXyxh8JqxO4M63dzP56wwm9OPGuCQ6MYOd2AHgZXz+g7PZcg==} + '@primeuix/utils@0.7.2': + resolution: {integrity: sha512-pmEbSfP0Phf9W9RweiM66zXnkn73ZeKyYINElbX3uZ2+stzzaba2svLAl3B1pHVcRw5t43O0VciaGe4ye2EXKw==} engines: {node: '>=12.11.0'} '@rolldown/pluginutils@1.0.0-beta.27': @@ -2593,121 +2650,145 @@ packages: resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.59.0': resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.5': resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.5': resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.5': resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.5': resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.53.5': resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.53.5': resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.5': resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.5': resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.5': resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.5': resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -2869,24 +2950,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -5053,24 +5138,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -8184,6 +8273,8 @@ snapshots: '@devolutions/iron-remote-desktop@0.11.0': {} + '@devolutions/ldap-wasm-js@0.3.4': {} + '@devolutions/terminal-shared@1.4.1(typescript@5.9.3)': dependencies: '@xterm/addon-fit': 0.10.0(@xterm/xterm@5.5.0) @@ -8193,6 +8284,25 @@ snapshots: transitivePeerDependencies: - typescript + '@devolutions/web-active-directory-gui@0.0.13(ffa535f5f8fdad6835c93e9f6c4f1c9b)': + dependencies: + '@angular/animations': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/cdk': 20.2.14(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/forms': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@devolutions/icons': 5.0.11(@types/react@19.2.7) + '@primeuix/styled': 0.7.4 + '@primeuix/themes': 2.0.3 + rxjs: 7.8.2 + tslib: 2.6.1 + zone.js: 0.15.0 + optionalDependencies: + primeng: 20.3.0(d23c4438b3ed4ab76af9fea6d9f471d1) + '@devolutions/web-ssh-gui@0.7.0(@devolutions/terminal-shared@1.4.1(typescript@5.9.3))': dependencies: '@devolutions/terminal-shared': 1.4.1(typescript@5.9.3) @@ -9273,17 +9383,19 @@ snapshots: '@primeuix/styled@0.7.4': dependencies: - '@primeuix/utils': 0.6.3 + '@primeuix/utils': 0.6.4 '@primeuix/styles@1.2.5': dependencies: '@primeuix/styled': 0.7.4 - '@primeuix/themes@1.2.5': + '@primeuix/themes@2.0.3': dependencies: '@primeuix/styled': 0.7.4 - '@primeuix/utils@0.6.3': {} + '@primeuix/utils@0.6.4': {} + + '@primeuix/utils@0.7.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -13010,7 +13122,7 @@ snapshots: '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@primeuix/styled': 0.7.4 '@primeuix/styles': 1.2.5 - '@primeuix/utils': 0.6.3 + '@primeuix/utils': 0.6.4 rxjs: 7.8.2 tslib: 2.6.1 From 258de089f92217f81219e3e77f8c467bf16b2f24 Mon Sep 17 00:00:00 2001 From: Krista House Date: Fri, 10 Jul 2026 14:19:42 -0400 Subject: [PATCH 2/2] fix(webapp): address Active Directory review feedback Redact invalid Active Directory connection logging, guard the AD port template control, use the LDAPS default port when needed, validate required AD connection parameters, and use the installed Active Directory icon class. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/client/app/app.constants.ts | 2 +- .../web-client-active-directory.component.ts | 5 +++-- .../active-directory-form.component.html | 2 +- .../connection-params.interfaces.ts | 19 +++++++++++++++---- .../app/shared/services/web-client.service.ts | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/webapp/apps/gateway-ui/src/client/app/app.constants.ts b/webapp/apps/gateway-ui/src/client/app/app.constants.ts index d620d6b6f..8d5e3b954 100644 --- a/webapp/apps/gateway-ui/src/client/app/app.constants.ts +++ b/webapp/apps/gateway-ui/src/client/app/app.constants.ts @@ -5,7 +5,7 @@ export const DVL_TELNET_ICON = 'dvl-icon-entry-session-telnet'; export const DVL_SSH_ICON = 'dvl-icon-entry-session-ssh'; export const DVL_VNC_ICON = 'dvl-icon-entry-session-vnc'; export const DVL_ARD_ICON = 'dvl-icon-entry-session-apple-remote-desktop'; -export const DVL_ACTIVE_DIRECTORY_ICON = 'dvl-icon-entry-ActiveDirectory'; +export const DVL_ACTIVE_DIRECTORY_ICON = 'dvl-icon-active-directory'; export const DVL_WARNING_ICON = 'dvl-icon-warning'; export const JET_RDP_URL = '/jet/rdp'; diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts index e6a829b9e..0e86523db 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts @@ -27,7 +27,7 @@ import { ActiveDirectoryFormDataInput } from '@shared/interfaces/forms.interface import { ComponentStatus } from '@shared/models/component-status.model'; import { AnalyticService, ProtocolString } from '@shared/services/analytic.service'; import { UtilsService } from '@shared/services/utils.service'; -import { DefaultLdapPort, WebClientService } from '@shared/services/web-client.service'; +import { DefaultLdapPort, DefaultLdapsPort, WebClientService } from '@shared/services/web-client.service'; import { WebSessionService } from '@shared/services/web-session.service'; import { EMPTY, from, Observable, of, Subscription } from 'rxjs'; import { catchError, map, switchMap, takeUntil } from 'rxjs/operators'; @@ -168,9 +168,10 @@ export class WebClientActiveDirectoryComponent extends WebClientBaseComponent im private fetchParameters(formData: ActiveDirectoryFormDataInput): Observable { const sessionId = uuidv4(); + const defaultPort = formData.useLdaps ? DefaultLdapsPort : DefaultLdapPort; const extractedData = this.utils.string.extractHostnameAndPort( formData.hostname, - Number(formData.port) || DefaultLdapPort, + Number(formData.port) || defaultPort, ); const gatewayAddress = new URL(JET_AD_URL, window.location.href).href; diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html index fc1628a80..f92b72ca1 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html @@ -41,7 +41,7 @@ required />
- @if (form.get('port').hasError('invalidPort') && form.get('port').touched) { + @if (form.get('port')?.hasError('invalidPort') && form.get('port')?.touched) {
Port must be between 1 and 65535.
diff --git a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts index a7616b79e..64d2e5929 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts @@ -93,10 +93,21 @@ export interface ActiveDirectoryConnectionParameters { } export function isActiveDirectoryConnectionParameters(params: unknown): params is ActiveDirectoryConnectionParameters { + if (typeof params !== 'object' || params === null) { + return false; + } + + const candidate = params as Partial; + return ( - typeof params === 'object' && - params !== null && - 'useLdaps' in params && - typeof (params as ActiveDirectoryConnectionParameters).useLdaps === 'boolean' + typeof candidate.host === 'string' && + typeof candidate.port === 'number' && + Number.isInteger(candidate.port) && + candidate.port >= 1 && + candidate.port <= 65535 && + typeof candidate.username === 'string' && + typeof candidate.password === 'string' && + typeof candidate.gatewayAddress === 'string' && + typeof candidate.useLdaps === 'boolean' ); } diff --git a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts index f897ba961..2230be0d1 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts @@ -98,7 +98,7 @@ export class WebClientService extends BaseComponent { ): string { if (protocol === Protocol.ActiveDirectory) { if (!isActiveDirectoryConnectionParameters(connectionParameters)) { - console.error('invalid active directory connection settings', connectionParameters); + console.error('invalid active directory connection settings'); const error = new Error('errConnectionFailed') as Error & { code: string }; error.code = 'errConnectionFailed';