All files / password-strength si-password-strength.directive.ts

100% Statements 34/34
85.71% Branches 30/35
100% Functions 3/3
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139              1x 1x 1x 1x 1x                                                                                               1x 16x 16x 16x                 16x                               16x                         16x       44x   44x 44x 5x   39x       44x 44x 33x 33x 33x     11x   11x 11x 11x 11x 11x 11x 11x 10x   11x   11x 11x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { computed, Directive, input, output } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
 
const RE_UPPER_CASE = /[A-Z]/;
const RE_LOWER_CASE = /[a-z]/;
const RE_DIGITS = /[0-9]/;
const RE_SPECIAL_CHARS = /[\x21-\x2F|\x3A-\x40|\x5B-\x60]/;
const RE_WHITESPACES = /\s/;
 
export interface PasswordPolicy {
  /**
   * Define minimal number of characters.
   */
  minLength: number;
  /**
   * Define if uppercase characters are required in password.
   */
  uppercase: boolean;
  /**
   * Define if lowercase characters are required in password.
   */
  lowercase: boolean;
  /**
   * Define if digits are required in password.
   */
  digits: boolean;
  /**
   * Define if special characters are required in password.
   */
  special: boolean;
  /**
   * Whether to allow whitespaces. By default whitespaces are not allowed.
   */
  allowWhitespace?: boolean;
  /**
   * Minimum required policies for valid password. When set to a number greater than 0,
   * defines the number of policies that must be met for the password to be valid.
   * E.g. when set to 3 and the policies uppercase/lowercase/digits/special are all set
   * and the password contains 3 out of these four, the password will be valid.
   */
  minRequiredPolicies?: number;
}
 
interface StrengthCheck {
  length: boolean;
  strength: number;
}
 
@Directive({
  selector: '[siPasswordStrength]',
  providers: [{ provide: NG_VALIDATORS, useExisting: SiPasswordStrengthDirective, multi: true }],
  host: {
    '[class.no-validation]': 'noValidation'
  }
})
export class SiPasswordStrengthDirective implements Validator {
  private readonly maxStrength = computed(() => {
    const strength = this.siPasswordStrength();
    return (
      1 +
      (strength.uppercase ? 1 : 0) +
      (strength.lowercase ? 1 : 0) +
      (strength.digits ? 1 : 0) +
      (strength.special ? 1 : 0)
    );
  });
 
  protected noValidation = false;
 
  /**
   * Define Siemens password strength.
   *
   * @defaultValue
   * ```
   * {
   *     minLength: 8,
   *     uppercase: true,
   *     lowercase: true,
   *     digits: true,
   *     special: true
   *   }
   * ```
   */
  readonly siPasswordStrength = input<PasswordPolicy>({
    minLength: 8,
    uppercase: true,
    lowercase: true,
    digits: true,
    special: true
  });
 
  /**
   * Output callback event called when the password changes. The number
   * indicated the number of rules which still can be met. (`-2` --\> 2 rules are
   * still unmet, `0` --\> all met)
   */
  readonly passwordStrengthChanged = output<number | void>();
 
  /** @internal */
  validate(control: AbstractControl): ValidationErrors {
    const strength = this.getStrength(control.value);
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
    const requiredStrength = this.siPasswordStrength().minRequiredPolicies || this.maxStrength();
    if (strength.length && strength.strength >= requiredStrength) {
      return {};
    }
    return { siPasswordStrength: true };
  }
 
  private getStrength(password: string): StrengthCheck {
    const policy = this.siPasswordStrength();
    if (!password) {
      this.noValidation = false;
      this.passwordStrengthChanged.emit();
      return { length: false, strength: 0 };
    }
 
    let strength = 0;
    // Strength check
    const length = password.length >= policy.minLength;
    strength += length ? 1 : 0;
    strength += policy.uppercase && password.match(RE_UPPER_CASE) ? 1 : 0;
    strength += policy.lowercase && password.match(RE_LOWER_CASE) ? 1 : 0;
    strength += policy.digits && password.match(RE_DIGITS) ? 1 : 0;
    strength += policy.special && password.match(RE_SPECIAL_CHARS) ? 1 : 0;
    if (policy.allowWhitespace !== true) {
      strength = password.match(RE_WHITESPACES) ? 0 : strength;
    }
    this.noValidation = true;
    // Notify listeners
    this.passwordStrengthChanged.emit(strength - this.maxStrength());
    return { length, strength };
  }
}