Search code examples
angularjsangularjs-directiveangularjs-controller

custom directive to mask sensitive data while keeping the original value


Using: angularjs 1.4.

I need to write a directive that hides sensitive data (replacing the middle section with asterisk '*') for ngModel and ngBind, but I'd also need to keep the original, unmasked data in order to send it back to the backend server.

The formatting part is not a problem, but I don't know how to keep the original data.

I've thought of returning an object the with obfuscated and original values, but since ngBind is involved, I can't rely on ngModelController to set formatters and parsers.

Here's the code I have so far:

app.factory('obfuscator', function() {
  return {
    obfuscate: obfuscate
  };

  function obfuscate(value) {
    // assume undef, empty, etc have all been taken care of
    let len = value.length;
    let chunk = Math.floor(len / 3);
    let masked = len - (chunk * 2);

    return {
      obfuscated: value.substr(0, chunk) + "*".repeat(masked) + value.substr(len - chunk, chunk),
      original: value
    };
  }
}).directive('sensitive', ['obfuscator', function(obfuscator) {
  return {
    restrict: 'A',
    priority: 1,
    scope: {
      model: '=ngModel'
    },
    link: function(scope, element, attrs, controller) {
      scope.$watch('model', function() {
        if (angular.isUndefined(scope.model)) return;
        let val = obfuscator.obfuscate(scope.model);
        scope.model = val.obfuscated;
      });

      attrs.$observe('ngBind', function() {
        let val = obfuscator.obfuscate(element.text());
        element.text(val.obfuscated);
      });
    }
  }
)]};

My question is: what to do with val.original?! If I replace the field with the object returned by obfuscator.obfuscate(), how can I detect that my field is decorated with 'sensitive' in my controller, in order to send field.original to the backend server? And how do I tell the view to use "field.obfuscated" instead of "field" in ng-model="field"?


Solution

  • So I solved the issue by using two directives, 'sensitive' for ngBind and 'obfuscated' for ngModel. Not 100% satisfied with this solution, as this means it'll be necessary for others and me to remember which directive to use for ng-model and for ng-bind...

    app.factory('obfuscator', function() {
      return {
        obfuscate: obfuscate
      };
    
      function obfuscate(value) {
        if (value) {
          let val = value.toString(); // now can be called on numbers too...
          let len = val.length;
          let chunk = Math.floor(len / 3);
          let masked = len - (chunk * 2);
    
          return val.substr(0, chunk) + "*".repeat(masked) + val.substr(len - chunk, chunk);
        }
    
        return value;
      }
    }).directive('sensitive', ['obfuscator', function(obfuscator) {
      // directive for 
      return {
        restrict: 'A',
        priority: 1,
        link: function(scope, element, attrs) {
          attrs.$observe('ngBind', function() {
            element.text(obfuscator.obfuscate(element.text()));
          });
        }
      }
    }]).directive('obfuscated', ['obfuscator', function(obfuscator) {
      return {
        restrict: 'A',
        priority: 1,
        require: 'ngModel',
        link: function(scope, element, attrs, controller) {
          controller.$formatters.push((value) => obfuscator.obfuscate(value));
    
          controller.$parsers.shift((value) => {
            element.text(obfuscator.obfuscate(value));
            return value;
          });
        }
      }
    }]);