瀏覽代碼

Merge remote-tracking branch 'joswhite/master' into ng-helpdesk

# Conflicts:
#	client/webpack.build.js
#	client/webpack.dev.js
jalbr74 7 年之前
父節點
當前提交
be241f7ef7
共有 86 個文件被更改,包括 4047 次插入473 次删除
  1. 23 0
      client/images/icons/m_check_thick.svg
  2. 23 0
      client/images/icons/m_lock_thin.svg
  3. 23 0
      client/images/icons/m_password_thin.svg
  4. 23 0
      client/images/icons/m_reload_refresh_thin.svg
  5. 23 0
      client/images/icons/m_unlock_thin.svg
  6. 二進制
      client/images/icons/wait_25.gif
  7. 1 1
      client/index.html
  8. 50 50
      client/package-lock.json
  9. 30 0
      client/src/helpdesk/date.filters.ts
  10. 62 0
      client/src/helpdesk/helpdesk-detail-dialog.template.html
  11. 154 0
      client/src/helpdesk/helpdesk-detail.component.html
  12. 127 0
      client/src/helpdesk/helpdesk-detail.component.scss
  13. 324 0
      client/src/helpdesk/helpdesk-detail.component.ts
  14. 65 0
      client/src/helpdesk/helpdesk-search.component.html
  15. 199 0
      client/src/helpdesk/helpdesk-search.component.scss
  16. 234 0
      client/src/helpdesk/helpdesk-search.component.ts
  17. 56 0
      client/src/helpdesk/helpdesk.module.ts
  18. 0 0
      client/src/helpdesk/helpdesk.scss
  19. 50 0
      client/src/helpdesk/main.dev.ts
  20. 62 0
      client/src/helpdesk/main.ts
  21. 53 0
      client/src/helpdesk/password-suggestions-dialog.html
  22. 15 0
      client/src/helpdesk/password-suggestions-dialog.scss
  23. 82 0
      client/src/helpdesk/password-suggestions.controller.ts
  24. 36 0
      client/src/helpdesk/recent-verifications-dialog.controller.ts
  25. 66 0
      client/src/helpdesk/recent-verifications-dialog.template.html
  26. 38 0
      client/src/helpdesk/routes.ts
  27. 163 0
      client/src/helpdesk/verifications-dialog.controller.ts
  28. 111 0
      client/src/helpdesk/verifications-dialog.template.html
  29. 51 10
      client/src/i18n/translations_en.json
  30. 1 1
      client/src/main.dev.ts
  31. 1 1
      client/src/main.ts
  32. 2 2
      client/src/peoplesearch/orgchart-search.component.ts
  33. 2 2
      client/src/peoplesearch/peoplesearch-base.component.ts
  34. 2 2
      client/src/peoplesearch/peoplesearch-cards.component.ts
  35. 2 2
      client/src/peoplesearch/peoplesearch-table.component.ts
  36. 2 2
      client/src/peoplesearch/person-details-dialog.component.ts
  37. 2 2
      client/src/routes.ts
  38. 40 0
      client/src/services/base-config.service.dev.ts
  39. 26 29
      client/src/services/base-config.service.ts
  40. 93 0
      client/src/services/helpdesk-config.service.dev.ts
  41. 150 0
      client/src/services/helpdesk-config.service.ts
  42. 480 0
      client/src/services/helpdesk.service.dev.ts
  43. 256 0
      client/src/services/helpdesk.service.ts
  44. 2 1
      client/src/services/local-storage.service.ts
  45. 49 0
      client/src/services/object.service.ts
  46. 21 0
      client/src/services/people.data.json
  47. 2 2
      client/src/services/people.service.ts
  48. 10 11
      client/src/services/peoplesearch-config.service.dev.ts
  49. 53 0
      client/src/services/peoplesearch-config.service.ts
  50. 9 1
      client/src/services/pwm.service.dev.ts
  51. 57 7
      client/src/services/pwm.service.ts
  52. 11 0
      client/src/ux/button.component.scss
  53. 24 0
      client/src/ux/ias-dialog.component.html
  54. 104 0
      client/src/ux/ias-dialog.component.scss
  55. 50 0
      client/src/ux/ias-dialog.component.ts
  56. 200 0
      client/src/ux/ias-dialog.service.ts
  57. 93 0
      client/src/ux/tabset.directive.scss
  58. 83 0
      client/src/ux/tabset.directive.ts
  59. 6 0
      client/src/ux/ux.module.ts
  60. 1 0
      client/tsconfig.json
  61. 2 1
      client/webpack.build.js
  62. 10 0
      client/webpack.common.js
  63. 2 1
      client/webpack.dev.js
  64. 22 0
      npm-debug.log
  65. 1 1
      server/src/main/resources/password/pwm/i18n/Display.properties
  66. 1 1
      server/src/main/resources/password/pwm/i18n/Display_ca.properties
  67. 1 1
      server/src/main/resources/password/pwm/i18n/Display_da.properties
  68. 1 1
      server/src/main/resources/password/pwm/i18n/Display_de.properties
  69. 1 1
      server/src/main/resources/password/pwm/i18n/Display_en_CA.properties
  70. 1 1
      server/src/main/resources/password/pwm/i18n/Display_es.properties
  71. 1 1
      server/src/main/resources/password/pwm/i18n/Display_fr.properties
  72. 1 1
      server/src/main/resources/password/pwm/i18n/Display_fr_CA.properties
  73. 1 1
      server/src/main/resources/password/pwm/i18n/Display_it.properties
  74. 1 1
      server/src/main/resources/password/pwm/i18n/Display_iw.properties
  75. 1 1
      server/src/main/resources/password/pwm/i18n/Display_ja.properties
  76. 1 1
      server/src/main/resources/password/pwm/i18n/Display_nl.properties
  77. 1 1
      server/src/main/resources/password/pwm/i18n/Display_no.properties
  78. 1 1
      server/src/main/resources/password/pwm/i18n/Display_pl.properties
  79. 1 1
      server/src/main/resources/password/pwm/i18n/Display_pt_BR.properties
  80. 1 1
      server/src/main/resources/password/pwm/i18n/Display_ru.properties
  81. 1 1
      server/src/main/resources/password/pwm/i18n/Display_sv.properties
  82. 1 1
      server/src/main/resources/password/pwm/i18n/Display_zh_CN.properties
  83. 1 1
      server/src/main/resources/password/pwm/i18n/Display_zh_TW.properties
  84. 0 296
      server/src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp
  85. 14 29
      server/src/main/webapp/WEB-INF/jsp/helpdesk.jsp
  86. 0 1
      server/src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

+ 23 - 0
client/images/icons/m_check_thick.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><polygon points="27.28 57.2 7.5 37.41 10.32 34.59 27.28 51.54 64.36 14.45 67.19 17.28 27.28 57.2" fill="gray"/></svg>

+ 23 - 0
client/images/icons/m_lock_thin.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M59.23,34.43a38,38,0,0,0-8-3.38V22.19a15.17,15.17,0,0,0-30.34,0v8.86A38.07,38.07,0,0,0,13,34.37a1.5,1.5,0,0,0-.76,1.3V57.93a1.5,1.5,0,0,0,.76,1.3c6.16,3.51,14.35,5.45,23.07,5.45s17-2,23.18-5.51a1.5,1.5,0,0,0,.75-1.3V35.73A1.5,1.5,0,0,0,59.23,34.43ZM23.88,22.19a12.17,12.17,0,1,1,24.34,0v8.08a55.87,55.87,0,0,0-24.34,0V22.19ZM57,57c-5.64,3-13,4.69-20.93,4.69S20.85,60,15.23,57V36.56c5.63-3,13-4.64,20.82-4.64S51.34,33.59,57,36.62V57Z" fill="gray"/></svg>

+ 23 - 0
client/images/icons/m_password_thin.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M62.7,57.32H28a21.38,21.38,0,0,1,0-42.75H62.7v3H28c-9.75,0-18,8.41-18,18.37s8.24,18.37,18,18.37H62.7v3Z" fill="gray"/><circle cx="27.2" cy="35.95" r="4.66" fill="gray"/><circle cx="58.34" cy="35.95" r="4.66" fill="gray"/><circle cx="42.69" cy="35.95" r="4.66" fill="gray"/></svg>

+ 23 - 0
client/images/icons/m_reload_refresh_thin.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_014_reload_refresh_thin</title><path d="M52.91,20.76a1.5,1.5,0,0,0-2.21,2,22.24,22.24,0,1,1-11.36-6.65l-6.51,6.51A1.52,1.52,0,0,0,35,24.76l9.12-9.12a1.5,1.5,0,0,0,0-2.12l-9-9A1.5,1.5,0,0,0,33,6.64L39.36,13a25.26,25.26,0,1,0,13.55,7.74Z" fill="gray"/></svg>

+ 23 - 0
client/images/icons/m_unlock_thin.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M59.23,34.43c-6.17-3.55-14.41-5.51-23.18-5.51a54.71,54.71,0,0,0-12.17,1.34V22.18a12.17,12.17,0,0,1,24.34,0h3a15.17,15.17,0,0,0-30.34,0V31A38.09,38.09,0,0,0,13,34.36a1.5,1.5,0,0,0-.76,1.3V57.92a1.5,1.5,0,0,0,.76,1.3c6.16,3.51,14.35,5.45,23.07,5.45s17-2,23.18-5.51a1.5,1.5,0,0,0,.75-1.3V35.73A1.5,1.5,0,0,0,59.23,34.43ZM57,57c-5.64,3-13,4.69-20.93,4.69S20.85,60,15.23,57V36.55c5.63-3,13-4.64,20.82-4.64S51.34,33.58,57,36.61V57Z" fill="gray"/></svg>

二進制
client/images/icons/wait_25.gif


+ 1 - 1
client/index.html

@@ -25,7 +25,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="initial-scale=1, maximum-scale=1">
-    <title>SSPR Development</title>
+    <title>PWM Development</title>
 
     <style>
         html, body {

+ 50 - 50
client/package-lock.json

@@ -49,7 +49,7 @@
     "@types/jquery": {
       "version": "3.2.12",
       "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.2.12.tgz",
-      "integrity": "sha512-xZzTbUju6AYFE/088UcH2+dB7yTLHlujDju9pfncD1WLl2LWa6Mn+WzKjFfhn8YA+he53j5K0Rfdw89BN0kDug==",
+      "integrity": "sha1-9JaCMQjDh0yXyagi5nWjkm7mS0Y=",
       "dev": true
     },
     "@types/node": {
@@ -61,7 +61,7 @@
     "@uirouter/angularjs": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.5.tgz",
-      "integrity": "sha512-VXNgZxBSfgntz9XsxbB0vkzkUQGUYJnF3S7clVtD4YCzRJnRNWywyYjx7zP2JJc6yBMLYmi8fjSUsQZp39uosg==",
+      "integrity": "sha1-ulWTeKyYElX5qWISt+9R+4LsSbg=",
       "dev": true,
       "requires": {
         "@uirouter/core": "5.0.5"
@@ -70,7 +70,7 @@
     "@uirouter/core": {
       "version": "5.0.5",
       "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-5.0.5.tgz",
-      "integrity": "sha512-z7zOXZKEFOloIeSMtsDpudWWfXd7L2qmhyxOAve4ZGFYwBn98zYBd2R4CIlPWMpcm4ZwfhIMTVUxCDgSSXrPKw==",
+      "integrity": "sha1-T6+ui4nhvuMhsO0y5pq2ZUfAVcY=",
       "dev": true
     },
     "abbrev": {
@@ -170,7 +170,7 @@
     "anymatch": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
-      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=",
       "dev": true,
       "requires": {
         "micromatch": "2.3.11",
@@ -180,7 +180,7 @@
     "aproba": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz",
-      "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==",
+      "integrity": "sha1-RcZikJTeTpb2k+9+q3SuB5wkD8E=",
       "dev": true
     },
     "are-we-there-yet": {
@@ -214,7 +214,7 @@
     "arr-flatten": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
-      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
       "dev": true
     },
     "array-find-index": {
@@ -368,7 +368,7 @@
     "base64-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
-      "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
+      "integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=",
       "dev": true
     },
     "base64id": {
@@ -745,7 +745,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -868,7 +868,7 @@
     "commander": {
       "version": "2.11.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
-      "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+      "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=",
       "dev": true
     },
     "component-bind": {
@@ -972,7 +972,7 @@
     "connect": {
       "version": "3.6.3",
       "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.3.tgz",
-      "integrity": "sha512-GLSZqgjVxPvGYVD/2vz//gS201MEXk4b7t3nHV6OVnTdDNWi/Gm7Rpxs/ybvljPWvULys/wrzIV3jB3YvEc3nQ==",
+      "integrity": "sha1-9zINRqJbS+e0g6IjZRfySx4n4wE=",
       "dev": true,
       "requires": {
         "debug": "2.6.8",
@@ -1082,7 +1082,7 @@
     "cosmiconfig": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz",
-      "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==",
+      "integrity": "sha1-YXPOvVb6wELB9DkO33r2wHx8uJI=",
       "dev": true,
       "requires": {
         "is-directory": "0.3.1",
@@ -1115,7 +1115,7 @@
         "lru-cache": {
           "version": "4.1.1",
           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
-          "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
+          "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=",
           "dev": true,
           "requires": {
             "pseudomap": "1.0.2",
@@ -1807,7 +1807,7 @@
         "qs": {
           "version": "6.5.0",
           "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz",
-          "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==",
+          "integrity": "sha1-jQSVTTZN7z78VbWgeT4eLIsebkk=",
           "dev": true
         }
       }
@@ -1923,7 +1923,7 @@
     "finalhandler": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz",
-      "integrity": "sha512-16l/r8RgzlXKmFOhZpHBztvye+lAhC5SU7hXavnerC9UfZqZxxXl3BzL8MhffPT3kF61lj9Oav2LKEzh0ei7tg==",
+      "integrity": "sha1-GFdPLnxLmLiuOyMMIfIB8xvbP7c=",
       "dev": true,
       "requires": {
         "debug": "2.6.8",
@@ -2075,7 +2075,7 @@
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=",
       "dev": true
     },
     "gauge": {
@@ -2193,7 +2193,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -2336,7 +2336,7 @@
     "hosted-git-info": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
-      "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
+      "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=",
       "dev": true
     },
     "html-comment-regex": {
@@ -2788,7 +2788,7 @@
     "is-my-json-valid": {
       "version": "2.16.1",
       "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz",
-      "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==",
+      "integrity": "sha1-WoRnd+LCYg0eaRBOXToDsfYIjxE=",
       "dev": true,
       "requires": {
         "generate-function": "2.0.0",
@@ -2915,7 +2915,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -3756,7 +3756,7 @@
     "mime": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.0.tgz",
-      "integrity": "sha512-n9ChLv77+QQEapYz8lV+rIZAW3HhAPW2CXnzb1GN5uMkuczshwvkW7XPsbzU0ZQN3sP47Er2KVkp2p3KyqZKSQ==",
+      "integrity": "sha1-aeng21HUTyo7VuSLeBfX0Tfxo0M=",
       "dev": true
     },
     "mime-db": {
@@ -3777,7 +3777,7 @@
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
       "dev": true,
       "requires": {
         "brace-expansion": "1.1.8"
@@ -3892,7 +3892,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -3983,7 +3983,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -4008,7 +4008,7 @@
     "normalize-package-data": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
-      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
       "dev": true,
       "requires": {
         "hosted-git-info": "2.5.0",
@@ -4047,7 +4047,7 @@
     "npmlog": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
-      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
       "dev": true,
       "requires": {
         "are-we-there-yet": "1.1.4",
@@ -4377,7 +4377,7 @@
         "async": {
           "version": "2.5.0",
           "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
-          "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
+          "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
           "dev": true,
           "requires": {
             "lodash": "4.17.4"
@@ -4495,7 +4495,7 @@
         "async": {
           "version": "2.5.0",
           "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
-          "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
+          "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
           "dev": true,
           "requires": {
             "lodash": "4.17.4"
@@ -4903,7 +4903,7 @@
         "ansi-styles": {
           "version": "3.2.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
           "dev": true,
           "requires": {
             "color-convert": "1.9.0"
@@ -4912,7 +4912,7 @@
         "chalk": {
           "version": "2.1.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+          "integrity": "sha1-rFvs8U+iG5nGySynp9fP1bF+dD4=",
           "dev": true,
           "requires": {
             "ansi-styles": "3.2.0",
@@ -4961,7 +4961,7 @@
         "ansi-styles": {
           "version": "3.2.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
           "dev": true,
           "requires": {
             "color-convert": "1.9.0"
@@ -4970,7 +4970,7 @@
         "chalk": {
           "version": "2.1.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+          "integrity": "sha1-rFvs8U+iG5nGySynp9fP1bF+dD4=",
           "dev": true,
           "requires": {
             "ansi-styles": "3.2.0",
@@ -5030,7 +5030,7 @@
         "ansi-styles": {
           "version": "3.2.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
           "dev": true,
           "requires": {
             "color-convert": "1.9.0"
@@ -5039,7 +5039,7 @@
         "chalk": {
           "version": "2.1.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+          "integrity": "sha1-rFvs8U+iG5nGySynp9fP1bF+dD4=",
           "dev": true,
           "requires": {
             "ansi-styles": "3.2.0",
@@ -5099,7 +5099,7 @@
         "ansi-styles": {
           "version": "3.2.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
-          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=",
           "dev": true,
           "requires": {
             "color-convert": "1.9.0"
@@ -5108,7 +5108,7 @@
         "chalk": {
           "version": "2.1.0",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
-          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+          "integrity": "sha1-rFvs8U+iG5nGySynp9fP1bF+dD4=",
           "dev": true,
           "requires": {
             "ansi-styles": "3.2.0",
@@ -5373,7 +5373,7 @@
     "randomatic": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
-      "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
+      "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
       "dev": true,
       "requires": {
         "is-number": "3.0.0",
@@ -5485,7 +5485,7 @@
     "readable-stream": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
-      "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
+      "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=",
       "dev": true,
       "requires": {
         "core-util-is": "1.0.2",
@@ -5506,7 +5506,7 @@
         "string_decoder": {
           "version": "1.0.3",
           "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+          "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
           "dev": true,
           "requires": {
             "safe-buffer": "5.1.1"
@@ -5754,7 +5754,7 @@
     "resolve": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
-      "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==",
+      "integrity": "sha1-p1vgHFPaJdk0qY69DkxKcxL5KoY=",
       "dev": true,
       "requires": {
         "path-parse": "1.0.5"
@@ -5781,7 +5781,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -5803,7 +5803,7 @@
     "safe-buffer": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
-      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+      "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",
       "dev": true
     },
     "sass-graph": {
@@ -5838,7 +5838,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -5886,7 +5886,7 @@
         "async": {
           "version": "2.5.0",
           "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
-          "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
+          "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
           "dev": true,
           "requires": {
             "lodash": "4.17.4"
@@ -5897,7 +5897,7 @@
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+      "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
       "dev": true
     },
     "scss-tokenizer": {
@@ -6618,7 +6618,7 @@
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
-      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=",
       "dev": true,
       "requires": {
         "os-tmpdir": "1.0.2"
@@ -6668,7 +6668,7 @@
         "semver": {
           "version": "5.4.1",
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
-          "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
+          "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=",
           "dev": true
         }
       }
@@ -6691,7 +6691,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
           "dev": true,
           "requires": {
             "fs.realpath": "1.0.0",
@@ -6994,7 +6994,7 @@
     "uuid": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
-      "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==",
+      "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=",
       "dev": true
     },
     "validate-npm-package-license": {
@@ -7286,7 +7286,7 @@
     "which": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
-      "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
+      "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
       "dev": true,
       "requires": {
         "isexe": "2.0.0"
@@ -7301,7 +7301,7 @@
     "wide-align": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
-      "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
+      "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=",
       "dev": true,
       "requires": {
         "string-width": "1.0.2"

+ 30 - 0
client/src/helpdesk/date.filters.ts

@@ -0,0 +1,30 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+// Convert ISO8601 date to human-readable format
+export function DateFilter(): (isoDate: string) => string {
+    return (isoDate: string): string => {
+        let date = new Date(isoDate);
+        return date.toString();
+    };
+}

+ 62 - 0
client/src/helpdesk/helpdesk-detail-dialog.template.html

@@ -0,0 +1,62 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ias-dialog>
+    <div ng-switch="status">
+        <div class="ias-actions" ng-switch-default>
+            <div class="WaitDialogBlank"></div>
+        </div>
+
+        <div ng-switch-when="confirm">
+            <div class="ias-dialog-header">
+                <div class="ias-title" ng-bind="title"></div>
+            </div>
+            <div class="ias-dialog-body">
+                <p ng-bind="text"></p>
+                <p ng-bind="secondaryText" ng-if="!!secondaryText"></p>
+            </div>
+            <div class="ias-actions">
+                <mf-button ng-click="confirm()">{{ 'Button_OK' | translate }}</mf-button>
+                <mf-button ng-click="close()">{{ 'Button_Cancel' | translate }}</mf-button>
+            </div>
+        </div>
+
+        <div ng-switch-when="success">
+            <div class="ias-dialog-header">
+                <div class="ias-title" ng-bind="title"></div>
+            </div>
+            <div class="ias-dialog-body">
+                <p ng-bind="text"></p>
+            </div>
+            <div class="ias-actions">
+                <mf-button ng-click="close()">{{ 'Button_OK' | translate }}</mf-button>
+            </div>
+        </div>
+    </div>
+
+    <mf-icon-button class="ias-dialog-close-button"
+                    icon="close_thick"
+                    id="close-icon"
+                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                    ng-click="close()">
+    </mf-icon-button>
+</ias-dialog>

+ 154 - 0
client/src/helpdesk/helpdesk-detail.component.html

@@ -0,0 +1,154 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<mf-app-bar>
+    <div id="page-content-title" class="page-content-title" translate="Title_HelpDesk">Help Desk</div>
+    <mf-icon-button
+        icon="password_thin"
+        ng-attr-title="{{ 'Button_ChangePassword' | translate }}"
+        ng-click=""
+        id="password-icon"></mf-icon-button>
+    <mf-icon-button
+        icon="unlock_thin"
+        ng-attr-title="{{ 'Button_Unlock' | translate }}"
+        ng-click=""
+        id="unlock-icon"></mf-icon-button>
+    <mf-icon-button
+        icon="reload_refresh_thin"
+        ng-attr-title="{{ 'Display_CaptchaRefresh' | translate }}"
+        ng-click=""
+        id="reload-refresh-icon"></mf-icon-button>
+</mf-app-bar>
+
+
+
+<person-card person="$ctrl.personCard" show-image="$ctrl.photosEnabled">
+</person-card>
+
+<div class="help-desk-content">
+    <div class="person-details-content">
+        <mf-tabset>
+            <mf-tab id="Field_Profile" label="Profile">
+                <table>
+                    <tbody>
+                    <tr ng-repeat="item in $ctrl.person.profileData">
+                        <td ng-bind="item.label"></td>
+                        <td ng-bind="item.value | dateFilter" ng-if="item.type==='timestamp'"></td>
+                        <td ng-bind="item.value" ng-if="item.type==='string' || item.type==='number'"></td>
+                        <td ng-bind="value" ng-if="item.type==='multiString'" ng-repeat="value in item.values"></td>
+                    </tr>
+                    </tbody>
+                </table>
+            </mf-tab>
+            <mf-tab id="Title_Status" label="Status">
+                <table>
+                    <tbody>
+                    <tr ng-repeat="item in $ctrl.person.statusData">
+                        <td ng-bind="item.label"></td>
+                        <td ng-bind="item.value | dateFilter" ng-if="item.type==='timestamp'"></td>
+                        <td ng-bind="item.value" ng-if="item.type==='string' || item.type==='number'"></td>
+                        <td ng-bind="value" ng-if="item.type==='multiString'" ng-repeat="value in item.values"></td>
+                    </tr>
+                    </tbody>
+                </table>
+            </mf-tab>
+            <mf-tab ng-if="!!$ctrl.person.userHistory" id="Title_UserEventHistory" label="Password History">
+                <table>
+                    <tbody>
+                    <tr ng-repeat="item in $ctrl.person.userHistory">
+                        <td ng-bind="item.timestamp | dateFilter"></td>
+                        <td ng-bind="item.label"></td>
+                    </tr>
+                    </tbody>
+                </table>
+            </mf-tab>
+            <mf-tab id="Title_PasswordPolicy" label="Password Policy">
+                <table>
+                    <tbody>
+                    <tr>
+                        <td ng-bind="'Field_Policy' | translate"></td>
+                        <td ng-bind="$ctrl.person.passwordPolicyDN"></td>
+                    </tr>
+                    <tr>
+                        <td ng-bind="'Field_Profile' | translate"></td>
+                        <td ng-bind="$ctrl.person.passwordPolicyID"></td>
+                    </tr>
+                    <tr class="bottom-border">
+                        <td ng-bind="'Field_Display' | translate"></td>
+                        <td>
+                            <ul>
+                                <li ng-repeat="item in $ctrl.person.passwordRequirements" ng-bind="item"></li>
+                            </ul>
+                        </td>
+                    </tr>
+                    <tr ng-repeat="(key, item) in $ctrl.person.passwordPolicyRules">
+                        <td ng-bind="key"></td>
+                        <td ng-bind="item"></td>
+                    </tr>
+                    </tbody>
+                </table>
+            </mf-tab>
+            <mf-tab id="Title_SecurityResponses" label="Security Responses">
+                <table>
+                    <tbody>
+                    <tr ng-repeat="item in $ctrl.person.helpdeskResponses">
+                        <td ng-bind="item.label"></td>
+                        <td ng-bind="item.value | dateFilter" ng-if="item.type==='timestamp'"></td>
+                        <td ng-bind="item.value" ng-if="item.type==='string' || item.type==='number'"></td>
+                        <td ng-bind="value" ng-if="item.type==='multiString'" ng-repeat="value in item.values"></td>
+                    </tr>
+                    </tbody>
+                </table>
+            </mf-tab>
+        </mf-tabset>
+    </div>
+
+    <div class="help-desk-buttons">
+        <mf-button ng-click="$ctrl.gotoSearch()"
+                   ng-disabled="$ctrl.buttonDisabled('back')"
+                   ng-if="$ctrl.buttonVisible('back')">{{ 'Button_GoBack' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.refresh()"
+                   ng-disabled="$ctrl.buttonDisabled('refresh')"
+                   ng-if="$ctrl.buttonVisible('refresh')">{{ 'Display_CaptchaRefresh' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.changePassword()"
+                   ng-disabled="$ctrl.buttonDisabled('changePassword')"
+                   ng-if="$ctrl.buttonVisible('changePassword')">{{ 'Button_ChangePassword' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.unlockUser()"
+                   ng-disabled="$ctrl.buttonDisabled('unlock')"
+                   ng-if="$ctrl.buttonVisible('unlock')">{{ 'Button_Unlock' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.clearResponses()"
+                   ng-disabled="$ctrl.buttonDisabled('clearResponses')"
+                   ng-if="$ctrl.buttonVisible('clearResponses')">{{ 'Button_ClearResponses' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.clearOtpSecret()"
+                   ng-disabled="$ctrl.buttonDisabled('clearOtpSecret')"
+                   ng-if="$ctrl.buttonVisible('clearOtpSecret')">{{ 'Button_HelpdeskClearOtpSecret' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.verifyUser()"
+                   ng-disabled="$ctrl.buttonDisabled('verification')"
+                   ng-if="$ctrl.buttonVisible('verification')">{{ 'Button_Verify' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.deleteUser()"
+                   ng-disabled="$ctrl.buttonDisabled('deleteUser')"
+                   ng-if="$ctrl.buttonVisible('deleteUser')">{{ 'Button_Delete' | translate }}</mf-button>
+        <mf-button ng-click="$ctrl.clickCustomButton(button)"
+                   ng-repeat="button in $ctrl.person.customButtons"
+                   ng-attr-title="{{button.description}}">{{ button.label }}</mf-button>
+    </div>
+</div>

+ 127 - 0
client/src/helpdesk/helpdesk-detail.component.scss

@@ -0,0 +1,127 @@
+/*!
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+help-desk-detail {
+  .page-content-title {
+    margin-right: 20px;
+  }
+
+  mf-icon-button {
+    margin-left: 5px;
+  }
+
+  person-card {
+    margin-top: 20px;
+  }
+
+  > .help-desk-content {
+    > .person-details-content,
+    > .help-desk-buttons {
+      display: inline-block;
+      vertical-align: top;
+    }
+
+    > .person-details-content {
+      min-width: 580px;
+      max-width: 700px;
+      padding: 10px;
+
+      .mf-tab-pane-container {
+        max-height: 500px;
+        overflow: auto;
+
+        table {
+          border: none;
+          border-collapse: collapse;
+          max-width: 560px;
+          max-height: 500px;
+          width: 100%;
+
+          tr {
+            height: 25px;
+
+            &.bottom-border {
+              border-bottom: 1px solid #949494;
+            }
+
+            td {
+              border: none;
+              font-size: 12px;
+              height: 19px;
+              text-align: left;
+
+              &:first-child {
+                color: #949494;
+                width: 200px;
+                text-align: right;
+                padding: 3px 0;
+              }
+
+              &:last-child {
+                padding: 3px 10px;
+
+                > .detail-container {
+                  > ul {
+                    > li {
+                      > mf-icon-button {
+                        display: inline-block;
+                        height: 16px;
+                        width: 16px;
+
+                        > button {
+                          > mf-icon {
+                            font-size: 16px;
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+
+              ul {
+                list-style: none;
+                margin: 0;
+                padding: 0;
+
+                > li {
+                  margin: 0;
+                  padding: 0;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    > .help-desk-buttons {
+      > mf-button {
+        display: block;
+
+        > button {
+          margin: 0 auto;
+        }
+      }
+    }
+  }
+}

+ 324 - 0
client/src/helpdesk/helpdesk-detail.component.ts

@@ -0,0 +1,324 @@
+/*
+ * Password Management Servlets (PWM)
+  htt://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {Component} from '../component';
+import {IButtonInfo, IHelpDeskService, ISuccessResponse} from '../services/helpdesk.service';
+import {IScope, ui} from '@types/angular';
+import {IActionButtons, IHelpDeskConfigService} from '../services/helpdesk-config.service';
+import DialogService from '../ux/ias-dialog.service';
+import {IPeopleService} from '../services/people.service';
+import {IPerson} from '../models/person.model';
+import IasDialogComponent from '../ux/ias-dialog.component';
+
+let helpdeskDetailDialogTemplateUrl = require('./helpdesk-detail-dialog.template.html');
+let passwordSuggestionsDialogTemplateUrl = require('./password-suggestions-dialog.html');
+let verificationsDialogTemplateUrl = require('./verifications-dialog.template.html');
+
+const STATUS_WAIT = 'wait';
+const STATUS_CONFIRM = 'confirm';
+const STATUS_SUCCESS = 'success';
+
+@Component({
+    stylesheetUrl: require('helpdesk/helpdesk-detail.component.scss'),
+    templateUrl: require('helpdesk/helpdesk-detail.component.html')
+})
+export default class HelpDeskDetailComponent {
+    actionButtons: IActionButtons;
+    person: any;
+    personCard: IPerson;
+    photosEnabled: boolean;
+
+    static $inject = [
+        '$state',
+        '$stateParams',
+        'ConfigService',
+        'HelpDeskService',
+        'IasDialogService',
+        'PeopleService'
+    ];
+    constructor(private $state: ui.IStateService,
+                private $stateParams: ui.IStateParamsService,
+                private configService: IHelpDeskConfigService,
+                private helpDeskService: IHelpDeskService,
+                private IasDialogService: DialogService,
+                private peopleService: IPeopleService) {
+    }
+
+    $onInit(): void {
+        this.initialize();
+    }
+
+    buttonDisabled(buttonName: string): boolean {
+        if (!this.person || !this.person.enabledButtons) {
+            return false;
+        }
+
+        return (this.person.enabledButtons.indexOf(buttonName) === -1);
+    }
+
+    buttonVisible(buttonName: string): boolean {
+        if (!this.person || !this.person.visibleButtons) {
+            return false;
+        }
+
+        return (this.person.visibleButtons.indexOf(buttonName) !== -1);
+    }
+
+    changePassword(): void {
+        this.IasDialogService
+            .open({
+                controller: 'PasswordSuggestionsDialogController as $ctrl',
+                templateUrl: passwordSuggestionsDialogTemplateUrl,
+                locals: {
+                    personUserKey: this.getUserKey(),
+                }
+            });
+    }
+
+    clearOtpSecret(): void {
+        if (this.buttonDisabled('clearOtpSecret')) {
+            return;
+        }
+
+        let userKey = this.getUserKey();
+
+        this.IasDialogService
+            .open({
+                controller: [
+                    '$scope',
+                    'HelpDeskService',
+                    'translateFilter',
+                    function ($scope: IScope,
+                              helpDeskService: IHelpDeskService,
+                              translateFilter: (id: string) => string) {
+                        $scope.status = STATUS_CONFIRM;
+                        $scope.title = translateFilter('Button_HelpdeskClearOtpSecret');
+                        $scope.text = translateFilter('Confirm');
+                        $scope.confirm = () => {
+                            $scope.status = STATUS_WAIT;
+                            helpDeskService.clearResponses(userKey).then((data: ISuccessResponse) => {
+                                // TODO - error dialog?
+                                $scope.status = STATUS_SUCCESS;
+                                $scope.text = data.successMessage;
+                            });
+                        };
+                    }
+                ],
+                templateUrl: helpdeskDetailDialogTemplateUrl
+            });
+    }
+
+    clearResponses(): void {
+        if (this.buttonDisabled('clearResponses')) {
+            return;
+        }
+
+        let userKey = this.getUserKey();
+
+        this.IasDialogService
+            .open({
+                controller: [
+                    '$scope',
+                    'HelpDeskService',
+                    'translateFilter',
+                    function ($scope: IScope,
+                              helpDeskService: IHelpDeskService,
+                              translateFilter: (id: string) => string) {
+                        $scope.status = STATUS_CONFIRM;
+                        $scope.title = translateFilter('Button_ClearResponses');
+                        $scope.text = translateFilter('Confirm');
+                        $scope.confirm = () => {
+                            $scope.status = STATUS_WAIT;
+                            helpDeskService.clearResponses(userKey).then((data: ISuccessResponse) => {
+                                // TODO - error dialog?
+                                $scope.status = STATUS_SUCCESS;
+                                $scope.text = data.successMessage;
+                            });
+                        };
+                    }
+                ],
+                templateUrl: helpdeskDetailDialogTemplateUrl
+            });
+    }
+
+    clickCustomButton(button: IButtonInfo): void {
+        // Custom buttons are never disabled
+
+        let userKey = this.getUserKey();
+
+        this.IasDialogService
+            .open({
+                controller: [
+                    '$scope',
+                    'HelpDeskService',
+                    'translateFilter',
+                    function ($scope: IScope,
+                              helpDeskService: IHelpDeskService,
+                              translateFilter: (id: string) => string) {
+                        $scope.status = STATUS_CONFIRM;
+                        $scope.title = translateFilter('Button_Confirm') + ' ' + button.label;
+                        $scope.text = button.description;
+                        $scope.secondaryText = translateFilter('Confirm');
+                        $scope.confirm = () => {
+                            $scope.status = STATUS_WAIT;
+                            helpDeskService.customAction(button.name, userKey).then((data: ISuccessResponse) => {
+                                // TODO - error dialog? (note that this error dialog is slightly different)
+                                $scope.status = STATUS_SUCCESS;
+                                $scope.title = translateFilter('Title_Success');
+                                $scope.secondaryText = null;
+                                $scope.text = data.successMessage;
+                            });
+                        };
+                    }
+                ],
+                templateUrl: helpdeskDetailDialogTemplateUrl
+            });
+    }
+
+    deleteUser(): void {
+        if (this.buttonDisabled('deleteUser')) {
+            return;
+        }
+
+        let self = this;
+        let userKey = this.getUserKey();
+
+        this.IasDialogService
+            .open({
+                controller: [
+                    '$scope',
+                    'HelpDeskService',
+                    'IasDialogService',
+                    'translateFilter',
+                    function ($scope: IScope,
+                              helpDeskService: IHelpDeskService,
+                              IasDialogService: DialogService,
+                              translateFilter: (id: string) => string) {
+                        $scope.status = STATUS_CONFIRM;
+                        $scope.title = translateFilter('Button_Confirm');
+                        $scope.text = translateFilter('Confirm_DeleteUser');
+                        $scope.confirm = () => {
+                            $scope.status = STATUS_WAIT;
+                            helpDeskService.deleteUser(userKey).then((data: ISuccessResponse) => {
+                                // TODO - error dialog?
+                                $scope.status = STATUS_SUCCESS;
+                                $scope.title = translateFilter('Title_Success');
+                                $scope.text = data.successMessage;
+                                $scope.close = () => {
+                                    IasDialogService.close();
+                                    self.gotoSearch();
+                                };
+                            });
+                        };
+                    }
+                ],
+                templateUrl: helpdeskDetailDialogTemplateUrl
+            });
+    }
+
+
+
+    getUserKey(): string {
+        return this.$stateParams['personId'];
+    }
+
+    gotoSearch(): void {
+        this.$state.go('search');
+    }
+
+    initialize(): void {
+        const personId = this.getUserKey();
+
+        this.configService.photosEnabled().then((photosEnabled: boolean) => {
+            this.photosEnabled = photosEnabled;
+        }); // TODO: always necessary?
+
+        this.peopleService.getPerson(personId).then((personCard: IPerson) => {
+            this.personCard = personCard;
+        });
+
+        this.helpDeskService
+            .getPerson(personId)
+            .then((person: any) => {
+                this.person = person;
+            }, (error) => {
+                // TODO: Handle error. NOOP for now will not assign person
+            });
+
+        this.configService
+            .getActionButtons()
+            .then((actionButtons: IActionButtons) => {
+                this.actionButtons = actionButtons;
+            });     // TODO: remove this code
+    }
+
+    refresh(): void {
+        this.person = null;
+        this.initialize();
+    }
+
+    unlockUser(): void {
+        if (this.buttonDisabled('unlock')) {
+            return;
+        }
+
+        let userKey = this.getUserKey();
+
+        this.IasDialogService
+            .open({
+                controller: [
+                    '$scope',
+                    'HelpDeskService',
+                    'translateFilter',
+                    function ($scope: IScope,
+                              helpDeskService: IHelpDeskService,
+                              translateFilter: (id: string) => string) {
+                        $scope.status = STATUS_CONFIRM;
+                        $scope.title = translateFilter('Button_Unlock');
+                        $scope.text = translateFilter('Confirm');
+                        $scope.confirm = () => {
+                            $scope.status = STATUS_WAIT;
+                            helpDeskService.unlockIntruder(userKey).then((data: ISuccessResponse) => {
+                                // TODO - error dialog?
+                                $scope.status = STATUS_SUCCESS;
+                                $scope.text = data.successMessage;
+                            });
+                        };
+                    }
+                ],
+                templateUrl: helpdeskDetailDialogTemplateUrl
+            });
+    }
+
+    verifyUser(): void {
+        this.IasDialogService
+            .open({
+                controller: 'VerificationsDialogController as $ctrl',
+                templateUrl: verificationsDialogTemplateUrl,
+                locals: {
+                    personUserKey: this.getUserKey(),
+                    search: false
+                }
+            });
+    }
+}

+ 65 - 0
client/src/helpdesk/helpdesk-search.component.html

@@ -0,0 +1,65 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<mf-app-bar>
+    <div id="page-content-title" translate="Title_HelpDesk">Help Desk</div>
+    <mf-search-bar search-text="$ctrl.query"
+                   on-search-text-change="$ctrl.onSearchTextChange(value)"
+                   auto-focus></mf-search-bar>
+    <span flex></span>
+    <mf-icon-button
+        icon="view-tile_thin"
+        ng-attr-title="{{ 'Title_HelpDeskCard' | translate }}"
+        ng-class="{selected: $ctrl.view === 'cards'}"
+        ng-click="$ctrl.gotoCardsView()"
+        ng-disabled="$ctrl.view === 'cards'"
+        id="view-tile-icon"></mf-icon-button>
+    <mf-icon-button
+        icon="view-list_thin"
+        ng-attr-title="{{ 'Title_HelpDeskTable' | translate }}"
+        ng-class="{selected: $ctrl.view === 'table'}"
+        ng-click="$ctrl.gotoTableView()"
+        ng-disabled="$ctrl.view === 'table'"
+        id="view-list-icon"></mf-icon-button>
+</mf-app-bar>
+<mf-button ng-if="$ctrl.verificationsEnabled"
+           ng-click="$ctrl.showVerifications()">{{ 'Button_Verifications' | translate }}</mf-button>
+
+<div class="people-search-component-content">
+    <div class="person-card-list" ng-show="$ctrl.view === 'cards'">
+        <person-card person="person"
+                     show-image="$ctrl.photosEnabled"
+                     ng-repeat="person in $ctrl.searchResult.people | orderBy:'displayNames[0]'"
+                     ng-click="$ctrl.selectPerson(person)">
+        </person-card>
+    </div>
+
+    <mf-table data="person in $ctrl.searchResult.people"
+              ng-show="$ctrl.view === 'table' && $ctrl.searchResult.people.length"
+              search-highlight="$ctrl.query"
+              on-click-item="$ctrl.selectPerson(person)">
+        <mf-table-column ng-repeat="(key, value) in $ctrl.columnConfiguration"
+                         label="value"
+                         value="'person.' + key">
+        </mf-table-column>
+    </mf-table>
+</div>

+ 199 - 0
client/src/helpdesk/helpdesk-search.component.scss

@@ -0,0 +1,199 @@
+/*!
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+help-desk-search {
+  display: flex;
+  flex-flow: column nowrap;
+  height: 100%;
+
+  // At medium size, cards are centered and no longer take up 100% width
+  &.medium {
+    > .people-search-component-content {
+      > .person-card-list {
+        > person-card {
+          margin: 0 auto;
+          display: block;
+          width: 272px;
+        }
+      }
+    }
+  }
+
+  // At large size, cards fit next to each other
+  &.large {
+    > .people-search-component-content {
+      > .person-card-list {
+        text-align: left;
+        margin: 0;
+
+        > person-card {
+          display: inline-block;
+          margin-right: 5px;
+        }
+      }
+    }
+  }
+
+  > .people-search-component-content {
+    flex: 1 1;
+    overflow: auto;
+    text-align: center;
+
+    > .person-card-list {
+      > person-card {
+        display: inline-block;
+        width: 100%;
+
+        &:not(:last-child) {
+          margin-bottom: 5px;
+        }
+      }
+    }
+  }
+}
+
+ias-dialog {
+  p,
+  .paragraph {
+    max-width: 400px;
+  }
+
+  .aligned-inputs {
+    margin-top: 8px;
+
+    input,
+    label,
+    .first-col,
+    .second-col {
+      display: inline-block;
+    }
+
+    input,
+    .second-col {
+      min-width: 200px;
+    }
+
+    input {
+      background-color: transparent;
+      border: 1px solid #dae1e1;
+      border-radius: 3px;
+      font-size: 15px;
+      padding: 8px;
+    }
+
+    label,
+    .first-col {
+      color: #808080;
+      font-size: 13px;
+      font-weight: normal;
+      line-height: 21px;
+      margin-right: 5px;
+      min-width: 150px;
+      text-align: right;
+      user-select: none;
+    }
+
+    mf-button {
+      margin: 0;
+
+      > button {
+        min-width: 90px;
+      }
+    }
+  }
+
+  .ias-actions {
+    mf-icon {
+      .icon_m_check_thick,
+      .icon_m_close_thick {
+        margin-left: 5px;
+      }
+
+      .icon_m_check_thick {
+        color: green;
+      }
+
+      .icon_m_close_thick {
+        color: red;
+      }
+    }
+
+    .loading-gif-25 {
+      background-image: url('../../images/icons/wait_25.gif');
+      display: inline-block;
+      height: 25px;
+      margin-left: 5px;
+      vertical-align: top;
+      width: 25px;
+    }
+  }
+
+  table {
+    border: 1px solid #dae1e1;
+    border-collapse: collapse;
+    width: 100%;
+
+    > tbody {
+      > tr {
+        height: 25px;
+      }
+    }
+
+    th, td {
+      font-weight: normal;
+      overflow: hidden;
+      padding: 5px;
+      text-align: left;
+      vertical-align: top;
+    }
+
+    th {
+      background-color: #eeeeee;
+      color: #697c87;
+      user-select: none;
+    }
+
+    tr {
+      > th,
+      > td {
+        border-bottom: 1px solid #dae1e1;
+      }
+    }
+  }
+}
+
+[dir="rtl"] {
+  people-search-cards {
+    &.large {
+      > .people-search-component-content {
+        .person-card-list {
+          text-align: right;
+
+          > person-card {
+            margin-right: auto;
+            margin-left: 5px;
+          }
+        }
+      }
+    }
+  }
+}

+ 234 - 0
client/src/helpdesk/helpdesk-search.component.ts

@@ -0,0 +1,234 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {Component} from '../component';
+import {IPeopleService} from '../services/people.service';
+import SearchResult from '../models/search-result.model';
+import {IPromise, IQService} from 'angular';
+import {IPerson} from '../models/person.model';
+import DialogService from '../ux/ias-dialog.service';
+import {IHelpDeskConfigService} from '../services/helpdesk-config.service';
+
+let verificationsDialogTemplateUrl = require('./verifications-dialog.template.html');
+let recentVerificationsDialogTemplateUrl = require('./recent-verifications-dialog.template.html');
+
+@Component({
+    stylesheetUrl: require('helpdesk/helpdesk-search.component.scss'),
+    templateUrl: require('helpdesk/helpdesk-search.component.html')
+})
+export default class HelpDeskSearchComponent {
+    columnConfiguration: any;
+    protected pendingRequests: IPromise<any>[] = [];
+    photosEnabled: boolean;
+    query: string;
+    searchResult: SearchResult;
+    verificationsEnabled: boolean;
+    view: string;
+
+    static $inject = ['$q',
+        'ConfigService',
+        'IasDialogService',
+        'PeopleService'
+    ];
+
+    constructor(private $q: IQService,
+                private configService: IHelpDeskConfigService,
+                private IasDialogService: DialogService,
+                private peopleService: IPeopleService) {
+    }
+
+    $onInit(): void {
+        this.view = 'cards';
+
+        this.configService.photosEnabled().then((photosEnabled: boolean) => {
+            this.photosEnabled = photosEnabled;
+        }); // TODO: only if in cards view (some other things are like that too)
+
+        this.configService.verificationsEnabled().then((verificationsEnabled: boolean) => {
+            this.verificationsEnabled = verificationsEnabled;
+        });
+
+        this.fetchData();
+    }
+
+    private fetchData() {
+        let searchResultPromise = this.fetchSearchData();
+        if (searchResultPromise) {
+
+            searchResultPromise.then(this.onSearchResult.bind(this));
+        }
+    }
+
+    private fetchSearchData(): IPromise<SearchResult> {
+        // this.abortPendingRequests();
+        this.searchResult = null;
+
+        if (!this.query) {
+            // this.clearSearch();
+            return null;
+        }
+
+        const self = this;
+
+        let promise = this.peopleService.search(this.query);
+
+        this.pendingRequests.push(promise);
+
+        return promise
+            .then(
+                (searchResult: SearchResult) => {
+                    // self.clearErrorMessage();
+                    // self.clearSearchMessage();
+
+                    // Aborted request
+                    if (!searchResult) {
+                        return;
+                    }
+
+                    // Too many results returned
+                    // if (searchResult.sizeExceeded) {
+                    //     self.setSearchMessage('Display_SearchResultsExceeded');
+                    // }
+
+                    // No results returned. Not an else if statement so that the more important message is presented
+                    // if (!searchResult.people.length) {
+                    //     self.setSearchMessage('Display_SearchResultsNone');
+                    // }
+
+                    return this.$q.resolve(searchResult);
+                },
+                (error) => {
+                    /*self.setErrorMessage(error);
+                    self.clearSearchMessage();*/
+                })
+            .finally(() => {
+                // self.removePendingRequest(promise);
+            });
+    }
+
+    gotoCardsView(): void {
+        if (this.view !== 'cards') {
+            this.view = 'cards';
+            this.fetchData();
+        }
+    }
+
+    gotoTableView(): void {
+        if (this.view !== 'table') {
+            this.view = 'table';
+            this.fetchData();
+        }
+
+        let self = this;
+
+        // The table columns are dynamic and configured via a service
+        this.configService.getColumnConfig().then(
+            (columnConfiguration: any) => {
+                self.columnConfiguration = columnConfiguration;
+            },
+            (error) => {
+                // self.setErrorMessage(error);
+            }); // TODO: remove self
+    }
+
+    private onSearchResult(searchResult: SearchResult): void {
+        if (this.view === 'table') {
+            this.searchResult = searchResult;
+            return;
+        }
+
+        // Aborted request
+        if (!searchResult) {
+            return;
+        }
+
+        this.searchResult = new SearchResult({
+            sizeExceeded: searchResult.sizeExceeded,
+            searchResults: []
+        });
+
+        let self = this;
+
+        this.pendingRequests = searchResult.people.map(
+            (person: IPerson) => {
+                // Store this promise because it is abortable
+                let promise = this.peopleService.getPerson(person.userKey);
+
+                promise
+                    .then((person: IPerson) => {
+                            // Aborted request
+                            if (!person) {
+                                return;
+                            }
+
+                            // searchResult may be overwritten by ESC->[LETTER] typed in after a search
+                            // has started but before all calls to peopleService.getPerson have resolved
+                            if (self.searchResult) {
+                                self.searchResult.people.push(person);
+                            }
+                        },
+                        (error) => {
+                            // self.setErrorMessage(error);
+                        })
+                    .finally(() => {
+                        // self.removePendingRequest(promise);
+                    });
+
+                return promise;
+            }
+        );  // TODO: this arg
+    }
+
+    onSearchTextChange(value: string): void {
+        if (value === this.query) {
+            return;
+        }
+
+        this.query = value;
+
+        // this.storeSearchText();
+        // this.clearSearchMessage();
+        // this.clearErrorMessage();
+        this.fetchData();
+    }
+
+    selectPerson(person: IPerson): void {
+        this.IasDialogService
+            .open({
+                controller: 'VerificationsDialogController as $ctrl',
+                templateUrl: verificationsDialogTemplateUrl,
+                locals: {
+                    personUserKey: person.userKey,
+                    search: true
+                }
+            });
+    }
+
+    showVerifications(): void {
+        this.IasDialogService
+            .open({
+                controller: 'RecentVerificationsDialogController as $ctrl',
+                templateUrl: recentVerificationsDialogTemplateUrl
+            });
+    }
+}

+ 56 - 0
client/src/helpdesk/helpdesk.module.ts

@@ -0,0 +1,56 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { module } from 'angular';
+import { DateFilter } from './date.filters';
+import HelpDeskDetailComponent from './helpdesk-detail.component';
+import HelpDeskSearchComponent from './helpdesk-search.component';
+import LocalStorageService from '../services/local-storage.service';
+import ObjectService from '../services/object.service';
+import PasswordSuggestionsDialogController from './password-suggestions.controller';
+import PersonCardComponent from '../peoplesearch/person-card.component';
+import PromiseService from '../services/promise.service';
+import RecentVerificationsDialogController from './recent-verifications-dialog.controller';
+import uxModule from '../ux/ux.module';
+import VerificationsDialogController from './verifications-dialog.controller';
+
+require('../peoplesearch/peoplesearch.scss');
+
+const moduleName = 'help-desk';
+
+module(moduleName, [
+    uxModule
+])
+
+    .component('helpDeskSearch', HelpDeskSearchComponent)
+    .component('helpDeskDetail', HelpDeskDetailComponent)
+    .component('personCard', PersonCardComponent)
+    .controller('PasswordSuggestionsDialogController', PasswordSuggestionsDialogController)
+    .controller('RecentVerificationsDialogController', RecentVerificationsDialogController)
+    .controller('VerificationsDialogController', VerificationsDialogController)
+    .filter('dateFilter', DateFilter)
+    .service('ObjectService', ObjectService)
+    .service('PromiseService', PromiseService)
+    .service('LocalStorageService', LocalStorageService);
+
+export default moduleName;

+ 0 - 0
client/src/helpdesk/helpdesk.scss


+ 50 - 0
client/src/helpdesk/main.dev.ts

@@ -0,0 +1,50 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+import { bootstrap, module } from 'angular';
+import helpDeskModule from './helpdesk.module';
+import routes from './routes';
+import uiRouter from '@uirouter/angularjs';
+import PeopleService from '../services/people.service.dev';
+import HelpDeskConfigService from '../services/helpdesk-config.service.dev';
+import HelpDeskService from '../services/helpdesk.service.dev';
+
+// fontgen-loader needs this :(
+require('../icons.json');
+
+module('app', [
+    uiRouter,
+    helpDeskModule,
+    'pascalprecht.translate'
+])
+    .config(['$translateProvider', ($translateProvider: angular.translate.ITranslateProvider) => {
+        $translateProvider.translations('en', require('i18n/translations_en.json'));
+        $translateProvider.useSanitizeValueStrategy('escapeParameters');
+        $translateProvider.preferredLanguage('en');
+    }])
+    .config(routes)
+    .service('HelpDeskService', HelpDeskService)
+    .service('PeopleService', PeopleService)
+    .service('ConfigService', HelpDeskConfigService);
+
+// Attach to the page document
+bootstrap(document, ['app']);

+ 62 - 0
client/src/helpdesk/main.ts

@@ -0,0 +1,62 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+import { bootstrap, module } from 'angular';
+import helpDeskModule from './helpdesk.module';
+import PeopleService from '../services/people.service';
+import PwmService from '../services/pwm.service';
+import routes from './routes';
+import TranslationsLoaderFactory from '../services/translations-loader.factory';
+import uiRouter from '@uirouter/angularjs';
+import HelpDeskConfigService from '../services/helpdesk-config.service';
+import HelpDeskService from '../services/helpdesk.service';
+
+// fontgen-loader needs this :(
+require('../icons.json');
+
+module('app', [
+    uiRouter,
+    helpDeskModule,
+    'pascalprecht.translate'
+])
+    .config(routes)
+    .config([
+        '$translateProvider',
+        ($translateProvider: angular.translate.ITranslateProvider) => {
+            $translateProvider
+                .translations('fallback', require('i18n/translations_en.json'))
+                .useLoader('translationsLoader')
+                .useSanitizeValueStrategy('escapeParameters')
+                .preferredLanguage('en')
+                .fallbackLanguage('fallback')
+                .forceAsyncReload(true);
+        }])
+    .service('HelpDeskService', HelpDeskService)
+    .service('PeopleService', PeopleService)
+    .service('PwmService', PwmService)
+    .service('ConfigService', HelpDeskConfigService)
+    .factory('translationsLoader', TranslationsLoaderFactory);
+
+// Attach to the page document, wait for PWM to load first
+window['PWM_GLOBAL'].startupFunctions.push(() => {
+    bootstrap(document, ['app'], { strictDi: true });
+});

+ 53 - 0
client/src/helpdesk/password-suggestions-dialog.html

@@ -0,0 +1,53 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ias-dialog class="password-suggestions-dialog">
+    <div class="ias-dialog-header">
+        <div class="ias-title" ng-bind="'Title_RandomPasswords' | translate"></div>
+    </div>
+
+    <div class="ias-dialog-body">
+        <p ng-bind="'Display_PasswordGeneration' | translate"></p>
+        <table>
+            <tbody>
+            <tr ng-repeat="i in [0,2,4,6,8,10,12,14,16,18]">
+                <td ng-repeat="j in [i, i+1]">
+                    <div href="#" ng-bind="$ctrl.passwordSuggestions[j]" ng-click="$ctrl.onChoosePassword(j)">
+                    </div>
+                </td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+
+    <div class="ias-actions">
+        <mf-button ng-click="$ctrl.onMoreRandomsButtonClick()"
+                   ng-disabled="$ctrl.fetchingRandoms">{{ 'Button_More' | translate }}</mf-button>
+        <mf-button ng-click="close()">{{ 'Button_Cancel' | translate }}</mf-button>
+    </div>
+    <mf-icon-button class="ias-dialog-close-button"
+                    icon="close_thick"
+                    id="close-icon"
+                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                    ng-click="close()">
+    </mf-icon-button>
+</ias-dialog>

+ 15 - 0
client/src/helpdesk/password-suggestions-dialog.scss

@@ -0,0 +1,15 @@
+ias-dialog {
+  &.password-suggestions-dialog {
+    table {
+      td {
+        min-width: 160px;
+        height: 15px;
+
+        div {
+          cursor: pointer;
+          font-family: monospace;
+        }
+      }
+    }
+  }
+}

+ 82 - 0
client/src/helpdesk/password-suggestions.controller.ts

@@ -0,0 +1,82 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {IHelpDeskService, IRandomPasswordResponse} from '../services/helpdesk.service';
+import {IPromise, IQService} from 'angular';
+
+let RANDOM_MAPPING_LENGTH = 20;
+
+require('helpdesk/password-suggestions-dialog.scss');
+
+export default class PasswordSuggestionsDialogController {
+    fetchingRandoms: boolean;
+    passwordSuggestions: string[];
+
+    static $inject = [ '$q', 'HelpDeskService', 'personUserKey' ];
+    constructor(private $q: IQService, private HelpDeskService: IHelpDeskService, private personUserKey: string) {
+        this.passwordSuggestions = Array(20).fill('');
+        this.fetchRandoms();
+    }
+
+    onChoosePassword(index: number) {
+        let password = this.passwordSuggestions[index];
+        // change password
+    }
+
+    onMoreRandomsButtonClick() {
+        this.fetchRandoms();
+    }
+
+    fetchRandoms() {
+        this.fetchingRandoms = true;
+        let ordering = this.generateRandomMapping();
+        let promiseChain: IPromise<any> = this.$q.when();
+        ordering.forEach((index: number) => {
+            promiseChain = promiseChain.then(this.passwordSuggestionFactory(index));
+        });
+        promiseChain.then(() => {
+            this.fetchingRandoms = false;
+        });
+    }
+
+    generateRandomMapping(): number[] {
+        let map: number[] = [];
+        for (let i = 0; i < RANDOM_MAPPING_LENGTH; i++) {
+            map.push(i);
+        }
+        let randomComparatorFunction = () => 0.5 - Math.random();
+        map.sort(randomComparatorFunction);
+        map.sort(randomComparatorFunction);
+        return map;
+    }
+
+    passwordSuggestionFactory(index: number): any {
+        return () => {
+            return this.HelpDeskService.getRandomPassword(this.personUserKey).then(
+                (result: IRandomPasswordResponse) => {
+                    this.passwordSuggestions[index] = result.password;
+                }
+            );
+        };
+    }
+}

+ 36 - 0
client/src/helpdesk/recent-verifications-dialog.controller.ts

@@ -0,0 +1,36 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {IHelpDeskService, IRecentVerifications} from '../services/helpdesk.service';
+
+export default class RecentVerificationsDialogController {
+    recentVerifications: IRecentVerifications;
+
+    static $inject = [ 'HelpDeskService' ];
+    constructor(helpDeskService: IHelpDeskService) {
+        helpDeskService.getRecentVerifications()
+            .then((recentVerifications) => {
+                this.recentVerifications = recentVerifications;
+            });
+    }
+}

+ 66 - 0
client/src/helpdesk/recent-verifications-dialog.template.html

@@ -0,0 +1,66 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ias-dialog>
+    <div class="ias-dialog-header">
+        <div class="ias-title" ng-bind="'Title_RecentVerifications' | translate"></div>
+    </div>
+
+    <div class="ias-dialog-body">
+        <p ng-bind="'Display_SearchResultsNone' | translate"
+           ng-if="!$ctrl.recentVerifications.length"></p>
+        <table ng-if="!!$ctrl.recentVerifications.length">
+            <thead>
+            <tr>
+                <th ng-bind="'Field_LdapProfile' | translate"></th>
+                <th ng-bind="'Field_Username' | translate"></th>
+                <th ng-bind="'Field_DateTime' | translate"></th>
+                <th ng-bind="'Field_Method' | translate"></th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-repeat="record in $ctrl.recentVerifications">
+                <td ng-bind="record.profile"></td>
+                <td ng-bind="record.username"></td>
+                <td ng-bind="record.timestamp | dateFilter"></td>
+                <td ng-bind="record.method"></td>
+            </tr>
+            </tbody>
+        </table>
+        <div ng-repeat="method in $ctrl.availableVerificationMethods" class="aligned-inputs">
+            <div class="first-col"></div>
+            <mf-button ng-click="$ctrl.selectVerificationMethod(method.name)">
+                {{ method.label | translate }}
+            </mf-button>
+        </div>
+    </div>
+
+    <div class="ias-actions">
+        <mf-button ng-click="close()">{{ 'Button_OK' | translate }}</mf-button>
+    </div>
+    <mf-icon-button class="ias-dialog-close-button"
+                    icon="close_thick"
+                    id="close-icon"
+                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                    ng-click="close()">
+    </mf-icon-button>
+</ias-dialog>

+ 38 - 0
client/src/helpdesk/routes.ts

@@ -0,0 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+export default [
+    '$stateProvider',
+    '$urlRouterProvider',
+    (
+        $stateProvider: angular.ui.IStateProvider,
+        $urlRouterProvider: angular.ui.IUrlRouterProvider
+    ) => {
+        $urlRouterProvider.otherwise(
+            ($injector: angular.auto.IInjectorService, $location: angular.ILocationService) => {
+                $location.url('search');
+            });
+
+        $stateProvider.state('search', { url: '/search?query', component: 'helpDeskSearch' });
+        $stateProvider.state('details', { url: '/details/{personId}', component: 'helpDeskDetail' });
+    }];

+ 163 - 0
client/src/helpdesk/verifications-dialog.controller.ts

@@ -0,0 +1,163 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {ui, ITimeoutService} from 'angular';
+import {
+    IHelpDeskConfigService, IVerificationMap, TOKEN_CHOICE,
+    VERIFICATION_METHOD_NAMES
+} from '../services/helpdesk-config.service';
+import {IHelpDeskService, IVerificationTokenResponse} from '../services/helpdesk.service';
+import DialogService from '../ux/ias-dialog.service';
+import {IPerson} from '../models/person.model';
+import ObjectService from '../services/object.service';
+
+const STATUS_FAILED = 'failed';
+const STATUS_NONE = 'none';
+const STATUS_PASSED = 'passed';
+const STATUS_SELECT = 'select';
+const STATUS_VERIFY = 'verify';
+const STATUS_WAIT = 'wait';
+
+export default class VerificationsDialogController {
+    availableVerificationMethods: IVerificationMap;
+    formData: any = {};
+    inputs: { name: string, label: string }[];
+    isDetailsView: boolean;
+    status: string;
+    tokenData: IVerificationTokenResponse;
+    viewDetailsEnabled: boolean;
+    verificationMethod: string;
+    verificationStatus: string;
+
+    static $inject = [
+        '$state',
+        '$timeout',
+        'ConfigService',
+        'HelpDeskService',
+        'IasDialogService',
+        'ObjectService',
+        'personUserKey',
+    ];
+    constructor(private $state: ui.IStateService,
+                private $timeout: ITimeoutService,
+                private configService: IHelpDeskConfigService,
+                private helpDeskService: IHelpDeskService,
+                private IasDialogService: DialogService,
+                private objectService: ObjectService,
+                private personUserKey: string,
+                private search: boolean) {
+        this.isDetailsView = (this.$state.current.name === 'details');
+        this.status = STATUS_WAIT;
+        this.verificationStatus = STATUS_NONE;
+        this.viewDetailsEnabled = false;
+
+        if (this.isDetailsView) {
+            this.loadVerificationOptions();
+        }
+        else {
+            this.helpDeskService
+                .checkVerification(this.personUserKey)
+                .then((response) => {
+                    if (response.passed) {
+                        this.gotoDetailsPage();
+                    }
+                    else {
+                        this.loadVerificationOptions();
+                    }
+                });
+        }
+    }
+
+    clickOkButton() {
+        if (this.verificationStatus === STATUS_PASSED) {
+            this.IasDialogService.close();
+        }
+    }
+
+    private gotoDetailsPage() {
+        this.$timeout(() => {
+            this.IasDialogService.close();
+            this.$state.go('details', {personId: this.personUserKey});
+        });
+    }
+
+    private loadVerificationOptions() {
+        this.configService
+            .getVerificationMethods()
+            .then((methods) => {
+                this.status = STATUS_SELECT;
+                this.availableVerificationMethods = methods;
+            });
+    }
+
+    selectVerificationMethod(method: string) {
+        this.verificationMethod = method;
+
+        if (method === VERIFICATION_METHOD_NAMES.ATTRIBUTES) {
+            this.configService.getVerificationAttributes()
+                .then((response) => {
+                    this.status = STATUS_VERIFY;
+                    this.inputs = response;
+                });
+        }
+        else if (method === VERIFICATION_METHOD_NAMES.SMS || method === VERIFICATION_METHOD_NAMES.EMAIL) {
+            this.status = STATUS_WAIT;
+            this.configService.getTokenSendMethod()
+                .then((tokenSendMethod) => {
+                    let choice = (tokenSendMethod === TOKEN_CHOICE) ? method : null;
+                    return this.helpDeskService.sendVerificationToken(this.personUserKey, choice);
+                })
+                .then((response) => {
+                    this.status = STATUS_VERIFY;
+                    this.tokenData = response;
+                });
+        }
+        else if (method === VERIFICATION_METHOD_NAMES.OTP) {
+            this.status = STATUS_VERIFY;
+        }
+    }
+
+    sendVerificationData() {
+        this.verificationStatus = STATUS_WAIT;
+        let data = {};
+        this.objectService.assign(data, this.formData);
+        if (this.tokenData) {
+            this.objectService.assign(data, this.tokenData);
+        }
+        this.helpDeskService.validateVerificationData(this.personUserKey, data, this.verificationMethod)
+            .then((response) => {
+                if (response.passed) {
+                    this.verificationStatus = STATUS_PASSED;
+                }
+                else {
+                    this.verificationStatus = STATUS_FAILED;
+                }
+            });
+    }
+
+    viewDetails() {
+        if (this.verificationStatus === STATUS_PASSED) {
+            this.gotoDetailsPage();
+        }
+    }
+}

+ 111 - 0
client/src/helpdesk/verifications-dialog.template.html

@@ -0,0 +1,111 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ias-dialog>
+    <div ng-switch="$ctrl.status">
+        <div class="ias-actions" ng-switch-default>
+            <div class="WaitDialogBlank"></div>
+        </div>
+
+        <div ng-switch-when="select">
+            <div class="ias-dialog-header">
+                <div class="ias-title" ng-bind="'Title_VerificationSend' | translate"></div>
+            </div>
+            <div class="ias-dialog-body">
+                <p ng-bind="'Long_Title_VerificationSend' | translate"></p>
+                <div ng-repeat="method in $ctrl.availableVerificationMethods" class="aligned-inputs">
+                    <div class="first-col"></div>
+                    <mf-button ng-click="$ctrl.selectVerificationMethod(method.name)">
+                        {{ method.label | translate }}
+                    </mf-button>
+                </div>
+            </div>
+            <div class="ias-actions">
+                <mf-button ng-click="close()">{{ 'Button_Cancel' | translate }}</mf-button>
+            </div>
+        </div>
+
+        <div ng-switch-when="verify">
+            <div class="ias-dialog-header">
+                <div class="ias-title" ng-bind="'Title_ValidateCode' | translate"></div>
+            </div>
+
+            <div ng-switch="$ctrl.verificationMethod">
+                <div class="ias-dialog-body" ng-switch-when="ATTRIBUTES">
+                    <div ng-repeat="input in $ctrl.inputs" class="aligned-inputs">
+                        <label ng-attr-for="{{input.name}}" ng-bind="input.label"></label>
+                        <input ng-attr-id="{{input.name}}" type="text" ng-model="$ctrl.formData[input.name]">
+                    </div>
+                </div>
+                <div class="ias-dialog-body" ng-switch-when="EMAIL|SMS" ng-switch-when-separator="|">
+                    <div class="aligned-inputs">
+                        <div class="first-col" ng-bind="'Display_TokenDestination' | translate"></div>
+                        <div class="second-col" ng-bind="$ctrl.tokenData.destination"></div>
+                    </div>
+                    <div class="aligned-inputs">
+                        <div class="first-col">Token</div>
+                        <input class="second-col" id="code" type="text" ng-model="$ctrl.formData.code">
+                    </div>
+                </div>
+                <div class="ias-dialog-body" ng-switch-when="OTP">
+                    <p ng-bind="'Display_HelpdeskOtpValidation' | translate"></p>
+                    <div class="aligned-inputs">
+                        <div class="first-col">Code</div>
+                        <input id="code" type="text" ng-model="$ctrl.formData.code">
+                    </div>
+                </div>
+            </div>
+
+            <div class="ias-actions">
+                    <div class="aligned-inputs">
+                        <div class="first-col"></div>
+                        <mf-button ng-click="$ctrl.sendVerificationData()">{{ 'Button_Verify' | translate }}</mf-button>
+                        <div class="loading-gif-25" ng-if="$ctrl.verificationStatus==='wait'"></div>
+                        <mf-icon icon="check_thick" ng-if="$ctrl.verificationStatus==='passed'"></mf-icon>
+                        <mf-icon icon="close_thick" ng-if="$ctrl.verificationStatus==='failed'"></mf-icon>
+                    </div>
+                <br>
+                <div class="paragraph" ng-if="$ctrl.verificationStatus==='failed'">
+                    Viewing details only available after a user has been successfully verified
+                </div>
+                <mf-button ng-disabled="$ctrl.verificationStatus!=='passed'"
+                           ng-click="$ctrl.viewDetails()"
+                           ng-if="!$ctrl.isDetailsView">
+                    View Details
+                </mf-button>
+                <mf-button ng-disabled="$ctrl.verificationStatus!=='passed'"
+                           ng-click="$ctrl.clickOkButton()"
+                           ng-if="$ctrl.isDetailsView">
+                    {{ 'Button_OK' | translate }}
+                </mf-button>
+                <mf-button ng-click="close()">{{ 'Button_Cancel' | translate }}</mf-button>
+            </div>
+        </div>
+    </div>
+
+    <mf-icon-button class="ias-dialog-close-button"
+                    icon="close_thick"
+                    id="close-icon"
+                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                    ng-click="close()">
+    </mf-icon-button>
+</ias-dialog>

+ 51 - 10
client/src/i18n/translations_en.json

@@ -1,15 +1,56 @@
 {
-  "Title_PeopleSearch": "People Search",
-  "Title_Management": "Management",
-  "Title_DirectReports": "Direct Report(s)",
-  "Title_Organization": "Organization",
-  "Title_OrgChart": "Organizational Chart",
-  "Title_Details": "Details",
-
+  "Button_Attributes": "User Data",
+  "Button_Cancel": "Cancel",
+  "Button_ChangePassword": "Change Password",
+  "Button_ClearResponses": "Clear Answers",
+  "Button_CloseWindow": "Close Window",
+  "Button_Confirm": "Confirm",
+  "Button_Delete": "Delete",
+  "Button_Email": "Email",
+  "Button_GoBack": "Go Back",
+  "Button_HelpdeskClearOtpSecret": "Clear OTP Secret",
+  "Button_More": "More",
+  "Button_OK": "OK",
+  "Button_OTP": "OTP",
+  "Button_SMS": "SMS",
+  "Button_Unlock": "Unlock",
+  "Button_Verifications": "Verifications",
+  "Button_Verify": "Verify",
+  "Confirm": "Are you sure you wish to proceed?",
+  "Confirm_DeleteUser": "Are you sure you wish to proceed?  If you continue, the selected user will be deleted permanently.  This can not be undone.",
+  "Display_CaptchaRefresh": "Refresh",
+  "Display_HelpdeskOtpValidation": "Instruct the user to load their mobile authentication app and share the current pass code.",
+  "Display_PasswordGeneration": "The following passwords have been randomly generated for you.  These passwords are based on real words to make them easier to remember, but have been modified to make them difficult to guess.",
   "Display_PleaseWait": "Loading...",
-
   "Display_SearchResultsExceeded": "Search results exceeded maximum search size",
   "Display_SearchResultsNone": "No results",
-
-  "Placeholder_Search": "Search"
+  "Display_TokenDestination": "Token Destination",
+  "Field_DateTime": "Date/Time",
+  "Field_Display": "Display",
+  "Field_LdapProfile": "LDAP Profile",
+  "Field_Method": "Method",
+  "Field_Policy": "Policy",
+  "Field_Profile": "Profile",
+  "Field_Username": "User Name",
+  "Long_Title_VerificationSend": "Before this user can be selected, the user's identity must be verified.  Please select a verification method.",
+  "Placeholder_Search": "Search",
+  "Title_Details": "Details",
+  "Title_DirectReports": "Direct Report(s)",
+  "Title_HelpDesk": "Help Desk",
+  "Title_HelpDeskCard": "Help Desk Cards",
+  "Title_HelpDeskTable": "Help Desk Table",
+  "Title_Management": "Management",
+  "Title_Organization": "Organization",
+  "Title_OrgChart": "Organizational Chart",
+  "Title_PeopleSearch": "People Search",
+  "Title_PeopleSearchCard": "People Search Cards",
+  "Title_PeopleSearchTable": "People Search Table",
+  "Title_RandomPasswords": "Random Passwords",
+  "Title_RecentVerifications": "Recent Verifications",
+  "Title_SecurityResponses": "Security Responses",
+  "Title_Status": "Status",
+  "Title_Success": "Success",
+  "Title_UserEventHistory": "Password History",
+  "Title_ValidateCode": "Validate Code",
+  "Title_VerificationSend": "Select verification method"
 }

+ 1 - 1
client/src/main.dev.ts

@@ -22,7 +22,7 @@
 
 
 import { bootstrap, module } from 'angular';
-import ConfigService from './services/config.service.dev';
+import ConfigService from './services/peoplesearch-config.service.dev';
 import peopleSearchModule from './peoplesearch/peoplesearch.module';
 import PeopleService from './services/people.service.dev';
 import PwmService from './services/pwm.service.dev';

+ 1 - 1
client/src/main.ts

@@ -22,7 +22,7 @@
 
 
 import { bootstrap, module } from 'angular';
-import ConfigService from './services/config.service';
+import ConfigService from './services/peoplesearch-config.service';
 import peopleSearchModule from './peoplesearch/peoplesearch.module';
 import PeopleService from './services/people.service';
 import PwmService from './services/pwm.service';

+ 2 - 2
client/src/peoplesearch/orgchart-search.component.ts

@@ -22,7 +22,7 @@
 
 
 import { Component } from '../component';
-import { IConfigService } from '../services/config.service';
+import { IPeopleSearchConfigService } from '../services/peoplesearch-config.service';
 import { IPeopleService } from '../services/people.service';
 import IPwmService from '../services/pwm.service';
 import { isArray, isString, IPromise, IQService, IScope } from 'angular';
@@ -57,7 +57,7 @@ export default class OrgChartSearchComponent {
                 private $scope: IScope,
                 private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
-                private configService: IConfigService,
+                private configService: IPeopleSearchConfigService,
                 private localStorageService: LocalStorageService,
                 private peopleService: IPeopleService,
                 private pwmService: IPwmService) {

+ 2 - 2
client/src/peoplesearch/peoplesearch-base.component.ts

@@ -22,7 +22,7 @@
 
 
 import { isArray, isString, IPromise, IQService, IScope } from 'angular';
-import { IConfigService } from '../services/config.service';
+import { IPeopleSearchConfigService } from '../services/peoplesearch-config.service';
 import { IPeopleService } from '../services/people.service';
 import IPwmService from '../services/pwm.service';
 import LocalStorageService from '../services/local-storage.service';
@@ -48,7 +48,7 @@ abstract class PeopleSearchBaseComponent {
                 protected $state: angular.ui.IStateService,
                 protected $stateParams: angular.ui.IStateParamsService,
                 protected $translate: angular.translate.ITranslateService,
-                protected configService: IConfigService,
+                protected configService: IPeopleSearchConfigService,
                 protected localStorageService: LocalStorageService,
                 protected peopleService: IPeopleService,
                 protected promiseService: PromiseService,

+ 2 - 2
client/src/peoplesearch/peoplesearch-cards.component.ts

@@ -23,7 +23,7 @@
 
 import { Component } from '../component';
 import ElementSizeService from '../ux/element-size.service';
-import IConfigService from '../services/config.service';
+import IPeopleSearchConfigService from '../services/peoplesearch-config.service';
 import IPeopleService from '../services/people.service';
 import IPwmService from '../services/pwm.service';
 import { isString, IAugmentedJQuery, IQService, IScope } from 'angular';
@@ -66,7 +66,7 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
-                configService: IConfigService,
+                configService: IPeopleSearchConfigService,
                 localStorageService: LocalStorageService,
                 private elementSizeService: ElementSizeService,
                 peopleService: IPeopleService,

+ 2 - 2
client/src/peoplesearch/peoplesearch-table.component.ts

@@ -22,7 +22,7 @@
 
 
 import { Component } from '../component';
-import { IConfigService } from '../services/config.service';
+import { IPeopleSearchConfigService } from '../services/peoplesearch-config.service';
 import IPeopleService from '../services/people.service';
 import IPwmService from '../services/pwm.service';
 import { IQService, IScope } from 'angular';
@@ -55,7 +55,7 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
-                configService: IConfigService,
+                configService: IPeopleSearchConfigService,
                 localStorageService: LocalStorageService,
                 peopleService: IPeopleService,
                 promiseService: PromiseService,

+ 2 - 2
client/src/peoplesearch/person-details-dialog.component.ts

@@ -22,7 +22,7 @@
 
 
 import { Component } from '../component';
-import { IConfigService } from '../services/config.service';
+import { IPeopleSearchConfigService } from '../services/peoplesearch-config.service';
 import { IPeopleService } from '../services/people.service';
 import { IAugmentedJQuery, ITimeoutService } from 'angular';
 import { IPerson } from '../models/person.model';
@@ -42,7 +42,7 @@ export default class PersonDetailsDialogComponent {
                 private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
                 private $timeout: ITimeoutService,
-                private configService: IConfigService,
+                private configService: IPeopleSearchConfigService,
                 private peopleService: IPeopleService) {
     }
 

+ 2 - 2
client/src/routes.ts

@@ -21,7 +21,7 @@
  */
 
 
-import { IConfigService } from './services/config.service';
+import { IPeopleSearchConfigService } from './services/peoplesearch-config.service';
 import { IQService } from 'angular';
 import LocalStorageService from './services/local-storage.service';
 
@@ -70,7 +70,7 @@ export default [
                 enabled: [
                     '$q',
                     'ConfigService',
-                    ($q: IQService, configService: IConfigService) => {
+                    ($q: IQService, configService: IPeopleSearchConfigService) => {
                         let deferred = $q.defer();
 
                         configService

+ 40 - 0
client/src/services/base-config.service.dev.ts

@@ -0,0 +1,40 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {IConfigService} from './base-config.service';
+import {IPromise, IQService} from 'angular';
+
+export abstract class ConfigBaseService implements IConfigService {
+    abstract getColumnConfig(): IPromise<any>;
+
+    constructor(protected $q: IQService) {
+    }
+
+    getValue(key: string): IPromise<any> {
+        return null;
+    }
+
+    photosEnabled(): IPromise<boolean> {
+        return this.$q.resolve(true);
+    }
+}

+ 26 - 29
client/src/services/config.service.ts → client/src/services/base-config.service.ts

@@ -20,48 +20,30 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import {IHttpService, ILogService, IPromise, IQService} from 'angular';
+import {IPwmService} from './pwm.service';
 
-import { IHttpService, ILogService, IPromise, IQService } from 'angular';
-import IPwmService from './pwm.service';
-import PwmService from './pwm.service';
-
-const COLUMN_CONFIG = 'peoplesearch_search_columns';
 const PHOTO_ENABLED = 'peoplesearch_enablePhoto';
-const ORGCHART_ENABLED = 'peoplesearch_orgChartEnabled';
 
 export interface IConfigService {
     getColumnConfig(): IPromise<any>;
-    photosEnabled(): IPromise<boolean>;
-    orgChartEnabled(): IPromise<boolean>;
     getValue(key: string): IPromise<any>;
+    photosEnabled(): IPromise<boolean>;
 }
 
-export default class ConfigService implements IConfigService {
+export abstract class ConfigBaseService implements IConfigService {
 
-    static $inject = ['$http', '$log', '$q', 'PwmService' ];
-    constructor(private $http: IHttpService,
-                private $log: ILogService,
-                private $q: IQService,
-                private pwmService: IPwmService) {
+    constructor(protected $http: IHttpService,
+                protected $log: ILogService,
+                protected $q: IQService,
+                protected pwmService: IPwmService) {
     }
 
-    getColumnConfig(): IPromise<any> {
-        return this.getValue(COLUMN_CONFIG);
-    }
-
-    photosEnabled(): IPromise<boolean> {
-        return this.getValue(PHOTO_ENABLED)
-            .then(null, () => { return this.$q.resolve(true); }); // On error use default
-    }
-
-    orgChartEnabled(): IPromise<boolean> {
-        return this.getValue(ORGCHART_ENABLED)
-            .then(null, () => { return this.$q.resolve(true); }); // On error use default
-    }
+    abstract getColumnConfig(): IPromise<any>;
 
-    getValue(key: string): IPromise<any> {
+    private getEndpointValue(endpoint: string, key: string): IPromise<any> {
         return this.$http
-            .get(this.pwmService.getServerUrl('clientData'), { cache: true })
+            .get(endpoint, { cache: true })
             .then((response) => {
                 if (response.data['error']) {
                     return this.handlePwmError(response);
@@ -71,6 +53,16 @@ export default class ConfigService implements IConfigService {
             }, this.handleHttpError);
     }
 
+    private getPeopleSearchValue(key: string): IPromise<any> {
+        let endpoint: string = this.pwmService.getPeopleSearchServerUrl('clientData');
+        return this.getEndpointValue(endpoint, key);
+    }
+
+    getValue(key: string): IPromise<any> {
+        let endpoint: string = this.pwmService.getServerUrl('clientData');
+        return this.getEndpointValue(endpoint, key);
+    }
+
     private handleHttpError(error): void {
         this.$log.error(error);
     }
@@ -81,4 +73,9 @@ export default class ConfigService implements IConfigService {
 
         return this.$q.reject(response.data['errorMessage']);
     }
+
+    photosEnabled(): IPromise<boolean> {
+        return this.getPeopleSearchValue(PHOTO_ENABLED)
+            .then(null, () => { return this.$q.resolve(true); }); // On error use default
+    }
 }

+ 93 - 0
client/src/services/helpdesk-config.service.dev.ts

@@ -0,0 +1,93 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IPromise, IQService } from 'angular';
+import {ConfigBaseService} from './base-config.service.dev';
+import {IConfigService} from './base-config.service';
+import {
+    IActionButtons,
+    IHelpDeskConfigService, IVerificationMap, TOKEN_CHOICE, VERIFICATION_METHOD_LABELS,
+    VERIFICATION_METHOD_NAMES
+} from './helpdesk-config.service';
+
+
+export default class HelpDeskConfigService extends ConfigBaseService implements IConfigService, IHelpDeskConfigService {
+    static $inject = [ '$q' ];
+    constructor($q: IQService) {
+        super($q);
+    }
+
+    getActionButtons(): IPromise<IActionButtons> {
+        return this.$q.resolve({
+            'Confirm New User Generation': {
+                name: 'Generate a New User',
+                description: 'Clones the current user'
+            },
+            'Confirm User Merge': {
+                name: 'Merge Two Users',
+                description: 'Merges the current user with another user'
+            }
+        });
+    }
+
+    getPasswordUiMode(): IPromise<string> {
+        return this.$q.resolve('both');
+    }
+
+    getColumnConfig(): IPromise<any> {
+        return this.$q.resolve({
+            givenName: 'First Name',
+            sn: 'Last Name',
+            title: 'Title',
+            mail: 'Email',
+            telephoneNumber: 'Telephone',
+            workforceId: 'Workforce ID'
+        });
+    }
+
+    getTokenSendMethod(): IPromise<string> {
+        return this.$q.resolve(TOKEN_CHOICE);
+    }
+
+    getVerificationAttributes(): IPromise<IVerificationMap> {
+        return this.$q.resolve([
+            { name: 'workforceID', label: 'Workforce ID' },
+            { name: 'mail', label: 'Email Address' }
+        ]);
+    }
+
+    getVerificationMethods(): IPromise<IVerificationMap> {
+        return this.$q.resolve([
+            { name: VERIFICATION_METHOD_NAMES.ATTRIBUTES, label: VERIFICATION_METHOD_LABELS.ATTRIBUTES },
+            { name: VERIFICATION_METHOD_NAMES.SMS, label: VERIFICATION_METHOD_LABELS.SMS }
+        ]);
+    }
+
+    maskPasswordsEnabled(): IPromise<boolean> {
+        return this.$q.resolve(true);
+    }
+
+    verificationsEnabled(): IPromise<boolean> {
+        return this.$q.resolve(true);
+    }
+}

+ 150 - 0
client/src/services/helpdesk-config.service.ts

@@ -0,0 +1,150 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IHttpService, ILogService, IPromise, IQService } from 'angular';
+import IPwmService from './pwm.service';
+import PwmService from './pwm.service';
+import {ConfigBaseService, IConfigService} from './base-config.service';
+
+const ACTION_BUTTONS_CONFIG = 'actions';
+const COLUMN_CONFIG = 'helpdesk_search_columns';
+const MASK_PASSWORDS_CONFIG = 'helpdesk_setting_maskPasswords';
+const PASSWORD_UI_MODE_CONFIG = 'helpdesk_setting_PwUiMode';
+const TOKEN_SEND_METHOD_CONFIG = 'helpdesk_setting_tokenSendMethod';
+const TOKEN_VERIFICATION_METHOD = 'TOKEN';
+const TOKEN_SMS_ONLY = 'SMSONLY';
+const TOKEN_EMAIL_ONLY = 'EMAILONLY';
+const VERIFICATION_FORM_CONFIG = 'verificationForm';
+const VERIFICATION_METHODS_CONFIG = 'verificationMethods';
+export const TOKEN_CHOICE = 'CHOICE_SMS_EMAIL';
+
+export const VERIFICATION_METHOD_NAMES = {
+    ATTRIBUTES: 'ATTRIBUTES',
+    EMAIL: 'EMAIL',
+    SMS: 'SMS',
+    OTP: 'OTP'
+};
+
+export const VERIFICATION_METHOD_LABELS = {
+    ATTRIBUTES: 'Button_Attributes',
+    EMAIL: 'Button_Email',
+    SMS: 'Button_SMS',
+    OTP: 'Button_OTP'
+};
+
+export interface IActionButtons {
+    [key: string]: {name: string, description: string};
+}
+
+interface IVerificationResponse {
+    optional: string[];
+    required: string[];
+}
+
+export type IVerificationMap = {name: string, label: string}[];
+
+export interface IHelpDeskConfigService extends IConfigService {
+    getActionButtons(): IPromise<IActionButtons>;
+    getPasswordUiMode(): IPromise<string>;
+    getTokenSendMethod(): IPromise<string>;
+    getVerificationAttributes(): IPromise<IVerificationMap>;
+    getVerificationMethods(): IPromise<IVerificationMap>;
+    maskPasswordsEnabled(): IPromise<boolean>;
+    verificationsEnabled(): IPromise<boolean>;
+}
+
+export default class HelpDeskConfigService extends ConfigBaseService implements IConfigService, IHelpDeskConfigService {
+
+    static $inject = ['$http', '$log', '$q', 'PwmService' ];
+    constructor($http: IHttpService, $log: ILogService, $q: IQService, pwmService: IPwmService) {
+        super($http, $log, $q, pwmService);
+    }
+
+    getActionButtons(): IPromise<IActionButtons> {
+        return this.getValue(ACTION_BUTTONS_CONFIG);
+    }
+
+    getColumnConfig(): IPromise<any> {
+        return this.getValue(COLUMN_CONFIG);
+    }
+
+    getPasswordUiMode(): IPromise<string> {
+        return this.getValue(PASSWORD_UI_MODE_CONFIG);
+    }
+
+    getTokenSendMethod(): IPromise<string> {
+        return this.getValue(TOKEN_SEND_METHOD_CONFIG);
+    }
+
+    getVerificationAttributes(): IPromise<IVerificationMap> {
+        return this.getValue(VERIFICATION_FORM_CONFIG);
+    }
+
+    private getVerificationMethod(methodName): {name: string, label: string} {
+        return {
+            name: methodName,
+            label: VERIFICATION_METHOD_LABELS[methodName]
+        };
+    }
+
+    getVerificationMethods(): IPromise<IVerificationMap> {
+        let promise = this.$q.all([
+            this.getValue(VERIFICATION_METHODS_CONFIG),
+            this.getTokenSendMethod()
+        ]);
+
+        return promise.then((result) => {
+            let methods: IVerificationResponse = result[0];
+            let tokenSendMethod: string = result[1];
+
+            let verificationMethods: IVerificationMap = [];
+            methods.required.forEach((method) => {
+                if (method === TOKEN_VERIFICATION_METHOD) {
+                    if (tokenSendMethod === TOKEN_EMAIL_ONLY || tokenSendMethod === TOKEN_CHOICE) {
+                        verificationMethods.push(this.getVerificationMethod(VERIFICATION_METHOD_NAMES.EMAIL));
+                    }
+
+                    if (tokenSendMethod === TOKEN_SMS_ONLY || tokenSendMethod === TOKEN_CHOICE) {
+                        verificationMethods.push(this.getVerificationMethod(VERIFICATION_METHOD_NAMES.SMS));
+                    }
+                }
+                else {
+                    verificationMethods.push(this.getVerificationMethod(method));
+                }
+            });
+
+            return verificationMethods;
+        });
+    }
+
+    maskPasswordsEnabled(): IPromise<boolean> {
+        return this.getValue(MASK_PASSWORDS_CONFIG);
+    }
+
+    verificationsEnabled(): IPromise<boolean> {
+        return this.getValue(VERIFICATION_METHODS_CONFIG)
+            .then((result: IVerificationResponse) => {
+                return !!result.required.length;
+            });
+    }
+}

+ 480 - 0
client/src/services/helpdesk.service.dev.ts

@@ -0,0 +1,480 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+import {
+    IHelpDeskService, IRandomPasswordResponse, IRecentVerifications, ISuccessResponse, IVerificationStatus,
+    IVerificationTokenResponse
+} from './helpdesk.service';
+import {IPromise, IQService, ITimeoutService, IWindowService} from 'angular';
+
+const SIMULATED_RESPONSE_TIME = 300;
+
+export default class HelpDeskService implements IHelpDeskService {
+    PWM_GLOBAL: any;
+
+    static $inject = [ '$q', '$timeout', '$window' ];
+    constructor(private $q: IQService, private $timeout: ITimeoutService, private $window: IWindowService) {
+    }
+
+    checkVerification(userKey: string): IPromise<IVerificationStatus> {
+        return this.simulateResponse({ passed: false });
+    }
+
+    clearOtpSecret(userKey: string): IPromise<ISuccessResponse> {
+        return this.simulateResponse({ successMessage: 'OTP Secret successfully cleared.' });
+    }
+
+    clearResponses(userKey: string): IPromise<ISuccessResponse> {
+        return this.simulateResponse({ successMessage: 'Security answers successfully cleared.' });
+    }
+
+    customAction(actionName: string, userKey: string): IPromise<ISuccessResponse> {
+        if (actionName === 'custom_0') {
+            return this.simulateResponse({ successMessage: 'User successfully cloned.' });
+        }
+        else if (actionName === 'custom_1') {
+            return this.simulateResponse({ successMessage: 'User successfully merged.' });
+        }
+        else {
+            this.$q.reject('Error! Action name doesn\'t exist.');
+        }
+    }
+
+    deleteUser(userKey: string): IPromise<ISuccessResponse> {
+        return this.simulateResponse({ successMessage: 'User successfully deleted.' });
+    }
+
+    getPerson(userKey: string): IPromise<any> {
+        return this.simulateResponse({
+            'userDisplayName': 'Andrew Astin - aastin - aastin@ad.utopia.netiq.com',
+            'userHistory': [
+                {
+                    'timestamp': '2017-12-13T18:36:50Z',
+                    'label': 'Help Desk Set Password'
+                },
+                {
+                    'timestamp': '2017-12-13T18:47:11Z',
+                    'label': 'Help Desk Set Password'
+                },
+                {
+                    'timestamp': '2017-12-13T18:50:35Z',
+                    'label': 'Setup Password Responses'
+                },
+                {
+                    'timestamp': '2017-12-13T19:19:33Z',
+                    'label': 'Help Desk Set Password'
+                },
+                {
+                    'timestamp': '2017-12-13T19:23:50Z',
+                    'label': 'Change Password'
+                },
+                {
+                    'timestamp': '2017-12-13T20:47:57Z',
+                    'label': 'Clear Responses'
+                },
+                {
+                    'timestamp': '2017-12-13T20:48:38Z',
+                    'label': 'Setup Password Responses'
+                }
+            ],
+            'passwordPolicyRules': {
+                'Policy Enabled': 'True',
+                'Minimum Length': '2',
+                'Maximum Length': '64',
+                'Minimum Upper Case': '0',
+                'Maximum Upper Case': '0',
+                'Minimum Lower Case': '0',
+                'Maximum Lower Case': '0',
+                'Allow Numeric': 'True',
+                'Minimum Numeric': '0',
+                'Maximum Numeric': '0',
+                'Minimum Unique': '0',
+                'Allow First Character Numeric': 'True',
+                'Allow Last Character Numeric': 'True',
+                'Allow Special': 'True',
+                'Minimum Special': '0',
+                'Maximum Special': '0',
+                'Allow First Character Special': 'True',
+                'Allow Last Character Special': 'True',
+                'Maximum Repeat': '0',
+                'Maximum Sequential Repeat': '0',
+                'Change Message': '',
+                'Expiration Interval': '0',
+                'Minimum Lifetime': '0',
+                'Case Sensitive': 'True',
+                'Unique Required': 'False',
+                'Disallowed Values': 'password\ntest',
+                'Disallowed Attributes': 'givenName\ncn\nsn',
+                'Disallow Current': 'True',
+                'Maximum AD Complexity Violations': '2',
+                'Regular Expression Match': '',
+                'Regular Expression No Match': '',
+                'Minimum Alpha': '0',
+                'Maximum Alpha': '0',
+                'Minimum Non-Alpha': '0',
+                'Maximum Non-Alpha': '0',
+                'Enable Word List': 'True',
+                'Minimum Strength': '0',
+                'Maximum Consecutive': '0',
+                'Character Groups Minimum Required': '0',
+                'Character Group Values': '.*[0-9]\n.*[^A-Za-z0-9]\n.*[A-Z]\n.*[a-z]',
+                'Rule_AllowMacroInRegExSetting': 'True'
+            },
+            'passwordRequirements': [
+                'Password is case sensitive.',
+                'Must be at least 2 characters long.',
+                'Must not include any of the following values:  password test',
+                'Must not include part of your name or user name.',
+                'Must not include a common word or commonly used sequence of characters.'
+            ],
+            'passwordPolicyDN': 'cn=SSPR,cn=Password Policies,cn=Security',
+            'passwordPolicyID': 'n/a',
+            'statusData': [
+                {
+                    'key': 'Field_Username',
+                    'type': 'string',
+                    'label': 'User Name',
+                    'value': 'aastin'
+                },
+                {
+                    'key': 'Field_UserDN',
+                    'type': 'string',
+                    'label': 'User DN',
+                    'value': 'cn=aastin,ou=users,o=novell'
+                },
+                {
+                    'key': 'Field_UserEmail',
+                    'type': 'string',
+                    'label': 'Email',
+                    'value': 'aastin@ad.utopia.netiq.com'
+                },
+                {
+                    'key': 'Field_UserSMS',
+                    'type': 'string',
+                    'label': 'SMS',
+                    'value': 'n/a'
+                },
+                {
+                    'key': 'Field_AccountEnabled',
+                    'type': 'string',
+                    'label': 'Account Enabled',
+                    'value': 'True'
+                },
+                {
+                    'key': 'Field_AccountExpired',
+                    'type': 'string',
+                    'label': 'Account Expired',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_UserGUID',
+                    'type': 'string',
+                    'label': 'User GUID',
+                    'value': 'ae95c9790234624d9848ae95c9790234'
+                },
+                {
+                    'key': 'Field_AccountExpirationTime',
+                    'type': 'timestamp',
+                    'label': 'Account Expiration Time',
+                    'value': 'n/a'
+                },
+                {
+                    'key': 'Field_LastLoginTime',
+                    'type': 'timestamp',
+                    'label': 'Last Login Time',
+                    'value': '2017-12-13T20:48:33Z'
+                },
+                {
+                    'key': 'Field_LastLoginTimeDelta',
+                    'type': 'string',
+                    'label': 'Last Login Time Delta',
+                    'value': '4 days, 22 hours, 49 minutes, 3 seconds'
+                },
+                {
+                    'key': 'Field_PasswordExpired',
+                    'type': 'string',
+                    'label': 'Password Expired',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_PasswordPreExpired',
+                    'type': 'string',
+                    'label': 'Password Pre-Expired',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_PasswordWithinWarningPeriod',
+                    'type': 'string',
+                    'label': 'Within Warning Period',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_PasswordSetTime',
+                    'type': 'timestamp',
+                    'label': 'Password Set Time',
+                    'value': '2017-12-13T19:23:49Z'
+                },
+                {
+                    'key': 'Field_PasswordSetTimeDelta',
+                    'type': 'string',
+                    'label': 'Password Set Time Delta',
+                    'value': '5 days, 13 minutes, 47 seconds'
+                },
+                {
+                    'key': 'Field_PasswordExpirationTime',
+                    'type': 'timestamp',
+                    'label': 'Password Expiration Time',
+                    'value': 'n/a'
+                },
+                {
+                    'key': 'Field_PasswordLocked',
+                    'type': 'string',
+                    'label': 'Password Locked (Intruder Detect)',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_ResponsesStored',
+                    'type': 'string',
+                    'label': 'Responses Stored',
+                    'value': 'True'
+                },
+                {
+                    'key': 'Field_ResponsesNeeded',
+                    'type': 'string',
+                    'label': 'Response Updates are Needed',
+                    'value': 'False'
+                },
+                {
+                    'key': 'Field_ResponsesTimestamp',
+                    'type': 'timestamp',
+                    'label': 'Stored Responses Timestamp',
+                    'value': '2017-12-13T20:48:37Z'
+                },
+                {
+                    'key': 'Field_ResponsesTimestamp',
+                    'type': 'timestamp',
+                    'label': 'Stored Responses Timestamp',
+                    'value': '2017-12-13T20:48:37Z'
+                }
+            ],
+            'profileData': [
+                {
+                    'key': 'cn',
+                    'type': 'string',
+                    'label': 'CN',
+                    'value': 'aastin'
+                },
+                {
+                    'key': 'givenName',
+                    'type': 'string',
+                    'label': 'First Name',
+                    'value': 'Andrew'
+                },
+                {
+                    'key': 'sn',
+                    'type': 'string',
+                    'label': 'Last Name',
+                    'value': 'Astin'
+                },
+                {
+                    'key': 'mail',
+                    'type': 'string',
+                    'label': 'Email Address',
+                    'value': 'aastin@ad.utopia.netiq.com'
+                },
+                {
+                    'key': 'telephoneNumber',
+                    'type': 'multiString',
+                    'label': 'Telephone Number',
+                    'values': [
+                        '801-802-0259'
+                    ]
+                },
+                {
+                    'key': 'title',
+                    'type': 'string',
+                    'label': 'Title',
+                    'value': 'Identity Administrator'
+                },
+                {
+                    'key': 'ou',
+                    'type': 'string',
+                    'label': 'Department',
+                    'value': 'Information Technology'
+                },
+                {
+                    'key': 'businessCategory',
+                    'type': 'string',
+                    'label': 'Business Category',
+                    'value': 'Identity'
+                },
+                {
+                    'key': 'company',
+                    'type': 'string',
+                    'label': 'Company',
+                    'value': 'Utopia Corp'
+                },
+                {
+                    'key': 'street',
+                    'type': 'string',
+                    'label': 'Street',
+                    'value': '50 Upper 5th'
+                },
+                {
+                    'key': 'physicalDeliveryOfficeName',
+                    'type': 'string',
+                    'label': 'City',
+                    'value': 'New York City'
+                },
+                {
+                    'key': 'st',
+                    'type': 'string',
+                    'label': 'State',
+                    'value': 'New York'
+                },
+                {
+                    'key': 'l',
+                    'type': 'string',
+                    'label': 'Location',
+                    'value': 'Manhattan'
+                },
+                {
+                    'key': 'employeeStatus',
+                    'type': 'string',
+                    'label': 'Employee Status',
+                    'value': 'A'
+                },
+                {
+                    'key': 'workforceID',
+                    'type': 'string',
+                    'label': 'Workforce ID',
+                    'value': 'E000259'
+                }
+            ],
+            'helpdeskResponses': [
+                {
+                    'key': 'item_1',
+                    'type': 'string',
+                    'label': 'How many years old are you?',
+                    'value': '25RRR'
+                }
+            ],
+            'visibleButtons': [
+                'refresh',
+                'back',
+                'changePassword',
+                'unlock',
+                'clearResponses',
+                'clearOtpSecret',
+                'verification',
+                'deleteUser'
+            ],
+            'enabledButtons': [
+                'refresh',
+                'back',
+                'changePassword',
+                'unlock',
+                'clearResponses',
+                'clearOtpSecret',
+                'verification',
+                'deleteUser'
+            ],
+            'customButtons': [
+                {
+                    'name': 'custom_0',
+                    'label': 'Clone User',
+                    'description': 'Clones the current user'
+                },
+                {
+                    'name': 'custom_1',
+                    'label': 'Merge User',
+                    'description': 'Merges the current user with another user'
+                }
+            ]
+        });
+    }
+
+    getRandomPassword(userKey: string): IPromise<IRandomPasswordResponse> {
+        let randomNumber = Math.floor(Math.random() * Math.floor(100));
+        let passwordSuggestion = 'suggestion' + randomNumber;
+        return this.simulateResponse({ password: passwordSuggestion });
+    }
+
+    getRecentVerifications(): IPromise<IRecentVerifications> {
+        return this.simulateResponse([
+            {
+                timestamp: '2017-12-06T23:19:07Z',
+                profile: 'default',
+                username: 'aastin',
+                method: 'Personal Data'
+            },
+            {
+                timestamp: '2017-12-03T22:11:07Z',
+                profile: 'default',
+                username: 'bjroach',
+                method: 'Personal Data'
+            },
+            {
+                timestamp: '2017-12-02T13:09:07Z',
+                profile: 'default',
+                username: 'rrhoads',
+                method: 'Personal Data'
+            }
+        ]);
+    }
+
+    sendVerificationToken(userKey: string, choice: string): IPromise<IVerificationTokenResponse> {
+        return this.simulateResponse({ destination: 'bcarrolj@paypal.com' });
+    }
+
+    private simulateResponse<T>(data: T): IPromise<T> {
+        let self = this;
+
+        let deferred = this.$q.defer();
+        let deferredAbort = this.$q.defer();
+
+        let timeoutPromise = this.$timeout(() => {
+            deferred.resolve(data);
+        }, SIMULATED_RESPONSE_TIME);
+
+        // To simulate an abortable promise, edit SIMULATED_RESPONSE_TIME
+        deferred.promise['_httpTimeout'] = deferredAbort;
+        deferredAbort.promise.then(() => {
+            self.$timeout.cancel(timeoutPromise);
+            deferred.resolve();
+        });
+
+        return deferred.promise;
+    }
+
+    unlockIntruder(userKey: string): IPromise<ISuccessResponse> {
+        return this.simulateResponse({ successMessage: 'Unlock successful.' });
+    }
+
+    validateVerificationData(userKey: string, data: any, method: string): IPromise<IVerificationStatus> {
+        return this.simulateResponse({ passed: true });
+    }
+
+    get showStrengthMeter(): boolean {
+        return false;
+    }
+}

+ 256 - 0
client/src/services/helpdesk.service.ts

@@ -0,0 +1,256 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {IPwmService} from './pwm.service';
+import {ILogService, IPromise, IQService, IWindowService} from 'angular';
+import LocalStorageService from './local-storage.service';
+import ObjectService from './object.service';
+
+const VERIFICATION_PROCESS_ACTIONS = {
+    ATTRIBUTES: 'validateAttributes',
+    EMAIL: 'verifyVerificationToken',
+    SMS: 'verifyVerificationToken',
+    OTP: 'validateOtpCode'
+};
+
+const DEFAULT_SHOW_STRENGTH_METER = false;
+
+export interface IHelpDeskService {
+    checkVerification(userKey: string): IPromise<IVerificationStatus>;
+    clearOtpSecret(userKey: string): IPromise<ISuccessResponse>;
+    clearResponses(userKey: string): IPromise<ISuccessResponse>;
+    customAction(actionName: string, userKey: string): IPromise<ISuccessResponse>;
+    deleteUser(userKey: string): IPromise<ISuccessResponse>;
+    getPerson(userKey: string): IPromise<any>;
+    getRandomPassword(userKey: string): IPromise<IRandomPasswordResponse>;
+    getRecentVerifications(): IPromise<IRecentVerifications>;
+    sendVerificationToken(userKey: string, choice: string): IPromise<IVerificationTokenResponse>;
+    unlockIntruder(userKey: string): IPromise<ISuccessResponse>;
+    validateVerificationData(userKey: string, formData: any, tokenData: any): IPromise<IVerificationStatus>;
+    showStrengthMeter: boolean;
+}
+
+export interface IButtonInfo {
+    description: string;
+    label: string;
+    name: string;
+}
+
+export type IRecentVerifications = IRecentVerification[];
+
+type IRecentVerification = {
+    profile: string,
+    username: string,
+    timestamp: string,
+    method: string
+}
+
+export interface IRandomPasswordResponse {
+    password: string;
+}
+
+export interface ISuccessResponse {
+    successMessage: string;
+}
+
+interface IValidationStatus extends IVerificationStatus {
+    verificationState: string;
+}
+
+export interface IVerificationStatus {
+    passed: boolean;
+}
+
+export interface IVerificationTokenResponse {
+    destination: string;
+}
+
+export default class HelpDeskService implements IHelpDeskService {
+    PWM_GLOBAL: any;
+
+    static $inject = [ '$log', '$q', 'LocalStorageService', 'ObjectService', 'PwmService', '$window' ];
+    constructor(private $log: ILogService,
+                private $q: IQService,
+                private localStorageService: LocalStorageService,
+                private objectService: ObjectService,
+                private pwmService: IPwmService,
+                $window: IWindowService) {
+        if ($window['PWM_GLOBAL']) {
+            this.PWM_GLOBAL = $window['PWM_GLOBAL'];
+        }
+        else {
+            this.$log.warn('PWM_GLOBAL is not defined on window');
+        }
+    }
+
+    checkVerification(userKey: string): IPromise<IVerificationStatus> {
+        let url: string = this.pwmService.getServerUrl('checkVerification');
+        let data = {
+            userKey: userKey,
+            verificationState: this.localStorageService.getItem(this.localStorageService.keys.VERIFICATION_STATE)
+        };
+
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: IVerificationStatus) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    clearOtpSecret(userKey: string): IPromise<ISuccessResponse> {
+        let url: string = this.pwmService.getServerUrl('clearOtpSecret');
+        let data: any = { userKey: userKey };
+
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: ISuccessResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    clearResponses(userKey: string): IPromise<ISuccessResponse> {
+        let url: string = this.pwmService.getServerUrl('clearResponses');
+        url += `&userKey=${userKey}`;
+
+        return this.pwmService
+            .httpRequest(url, {})
+            .then((result: ISuccessResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    customAction(actionName: string, userKey: string): IPromise<ISuccessResponse> {
+        let url: string = this.pwmService.getServerUrl('executeAction');
+        url += `&name=${actionName}`;
+        let data: any = { userKey: userKey };
+
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: ISuccessResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    deleteUser(userKey: string): IPromise<ISuccessResponse> {
+        let url: string = this.pwmService.getServerUrl('deleteUser');
+        url += `&userKey=${userKey}`;
+
+        return this.pwmService
+            .httpRequest(url, {})
+            .then((result: ISuccessResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    getPerson(userKey: string): IPromise<any> {
+        let url: string = this.pwmService.getServerUrl('detail');
+        let verificationState = this.localStorageService.getItem(this.localStorageService.keys.VERIFICATION_STATE);
+        url += `&userKey=${userKey}`;
+        url += `&verificationState=${verificationState}`;
+
+        return this.pwmService
+            .httpRequest(url, {})
+            .then((result: any) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    getRandomPassword(userKey: string): IPromise<IRandomPasswordResponse> {
+        let url: string = this.pwmService.getServerUrl('randomPassword');
+        let data = {
+            username: userKey,
+            strength: 0
+        };
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: IRandomPasswordResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    getRecentVerifications(): IPromise<IRecentVerifications> {
+        let url: string = this.pwmService.getServerUrl('showVerifications');
+        let data = {
+            verificationState: this.localStorageService.getItem(this.localStorageService.keys.VERIFICATION_STATE)
+        };
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: {records: IRecentVerifications}) => {
+                return this.$q.resolve(result.records);
+            });
+    }
+
+    sendVerificationToken(userKey: string, choice: string): IPromise<IVerificationTokenResponse> {
+        let url: string = this.pwmService.getServerUrl('sendVerificationToken');
+        let data: any = { userKey: userKey };
+
+        if (choice) {
+            data.method = choice;
+        }
+
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: IVerificationTokenResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    unlockIntruder(userKey: string): IPromise<ISuccessResponse> {
+        let url: string = this.pwmService.getServerUrl('unlockIntruder');
+        url += `&userKey=${userKey}`;
+
+        return this.pwmService
+            .httpRequest(url, {})
+            .then((result: ISuccessResponse) => {
+                return this.$q.resolve(result);
+            });
+    }
+
+    validateVerificationData(userKey: string, data: any, method: string): IPromise<IVerificationStatus> {
+        let processAction = VERIFICATION_PROCESS_ACTIONS[method];
+        let url: string = this.pwmService.getServerUrl(processAction);
+        let content = {
+            userKey: userKey,
+            verificationState: this.localStorageService.getItem(this.localStorageService.keys.VERIFICATION_STATE)
+        };
+        this.objectService.assign(data, content);
+
+        return this.pwmService
+            .httpRequest(url, { data: data })
+            .then((result: IValidationStatus) => {
+                this.localStorageService.setItem(
+                    this.localStorageService.keys.VERIFICATION_STATE,
+                    result.verificationState
+                );
+                return this.$q.resolve(result);
+            });
+    }
+
+    get showStrengthMeter(): boolean {
+        if (this.PWM_GLOBAL) {
+            return this.PWM_GLOBAL['setting-showStrengthMeter'] || DEFAULT_SHOW_STRENGTH_METER;
+        }
+
+        return DEFAULT_SHOW_STRENGTH_METER;
+    }
+}

+ 2 - 1
client/src/services/local-storage.service.ts

@@ -26,7 +26,8 @@ import { ILogService, IWindowService } from 'angular';
 const PWM_PREFIX = 'PWM_';
 const KEYS = {
     SEARCH_TEXT: 'searchText',
-    SEARCH_VIEW: 'searchView'
+    SEARCH_VIEW: 'searchView',
+    VERIFICATION_STATE: 'verificationState'
 };
 
 export default class LocalStorageService {

+ 49 - 0
client/src/services/object.service.ts

@@ -0,0 +1,49 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+export default class ObjectService {
+    // ES5 implementation of Object.assign
+    // Source from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
+    assign(target, varArgs) { // .length of function is 2
+        'use strict';
+        if (target == null) { // TypeError if undefined or null
+            throw new TypeError('Cannot convert undefined or null to object');
+        }
+
+        const to = Object(target);
+
+        for (let index = 1; index < arguments.length; index++) {
+            const nextSource = arguments[index];
+
+            if (nextSource != null) { // Skip over if undefined or null
+                for (let nextKey in nextSource) {
+                    // Avoid bugs when hasOwnProperty is shadowed
+                    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+                        to[nextKey] = nextSource[nextKey];
+                    }
+                }
+            }
+        }
+        return to;
+    }
+}

+ 21 - 0
client/src/services/people.data.json

@@ -7,6 +7,7 @@
     "mail": "jryan0@csmonitor.com",
     "telephoneNumber": "(564) 683-6597",
     "title": "Senior Sales Associate",
+    "workforceId": "E84-001",
     "managerId": null,
     "photoURL": "/images/avatars/1.jpg",
     "_displayName": "Jean Ryan - Senior Sales Associate",
@@ -87,6 +88,7 @@
     "mail": "rbowman1@pinterest.com",
     "telephoneNumber": "(567) 798-6155",
     "title": "Quality Engineer",
+    "workforceId": "E84-002",
     "managerId": 1,
     "photoURL": "/images/avatars/2.jpg",
     "_displayName": "Ruby Bowman - Quality Engineer",
@@ -167,6 +169,7 @@
     "mail": "wcarter2@delicious.com",
     "telephoneNumber": "(523) 622-4293",
     "title": "Community Outreach Specialist",
+    "workforceId": "E84-003",
     "managerId": 1,
     "photoURL": "/images/avatars/3.jpg",
     "_displayName": "William Carter - Community Outreach Specialist",
@@ -247,6 +250,7 @@
     "mail": "asnyder3@blinklist.com",
     "telephoneNumber": "(345) 682-1430",
     "title": "Research Nurse",
+    "workforceId": "E84-004",
     "managerId": 1,
     "photoURL": "/images/avatars/4.jpg",
     "_displayName": "Alan Snyder - Research Nurse",
@@ -327,6 +331,7 @@
     "mail": "aalvarez4@ezinearticles.com",
     "telephoneNumber": "(457) 877-1797",
     "title": "Financial Analyst",
+    "workforceId": "E84-005",
     "managerId": 2,
     "photoURL": "/images/avatars/5.jpg",
     "_displayName": "Aaron Alvarez - Financial Analyst",
@@ -407,6 +412,7 @@
     "mail": "dmorrison5@nytimes.com",
     "telephoneNumber": "(268) 336-2705",
     "title": "Accountant II",
+    "workforceId": "E84-006",
     "managerId": 2,
     "photoURL": "",
     "_displayName": "Deborah Morrison - Accountant II",
@@ -487,6 +493,7 @@
     "mail": "mhayes6@house.gov",
     "telephoneNumber": "(638) 951-3305",
     "title": "Design Engineer",
+    "workforceId": "E84-007",
     "managerId": 2,
     "photoURL": "/images/avatars/7.jpg",
     "_displayName": "Mildred Hayes - Design Engineer",
@@ -567,6 +574,7 @@
     "mail": "mholmes7@nbcnews.com",
     "telephoneNumber": "(564) 818-6794",
     "title": "Structural Engineer",
+    "workforceId": "E84-008",
     "managerId": 3,
     "photoURL": "/images/avatars/8.jpg",
     "_displayName": "Margaret Holmes - Structural Engineer",
@@ -647,6 +655,7 @@
     "mail": "jjackson8@thetimes.co.uk",
     "telephoneNumber": "(206) 987-9763",
     "title": "Junior Executive",
+    "workforceId": "E84-009",
     "managerId": 3,
     "photoURL": "/images/avatars/9.jpg",
     "_displayName": "Jack Jackson - Junior Executive",
@@ -738,6 +747,7 @@
     "mail": "jbutler9@reference.com",
     "telephoneNumber": "(751) 250-5973",
     "title": "Chief Design Engineer",
+    "workforceId": "E84-010",
     "managerId": 3,
     "photoURL": "/images/avatars/10.jpg",
     "_displayName": "Judy Butler - Chief Design Engineer",
@@ -818,6 +828,7 @@
     "mail": "tgutierreza@godaddy.com",
     "telephoneNumber": "(205) 653-6795",
     "title": "Engineer IV",
+    "workforceId": "E84-011",
     "managerId": 9,
     "photoURL": "/images/avatars/11.jpg",
     "_displayName": "Tina Gutierrez - Engineer IV",
@@ -898,6 +909,7 @@
     "mail": "jcoxb@stumbleupon.com",
     "telephoneNumber": "(816) 816-8474",
     "title": "Human Resources Manager",
+    "workforceId": "E84-012",
     "managerId": 9,
     "photoURL": "/images/avatars/12.jpg",
     "_displayName": "Jose Cox - Human Resources Manager",
@@ -978,6 +990,7 @@
     "mail": "pbarnesc@mediafire.com",
     "telephoneNumber": "(207) 691-7625",
     "title": "Internal Auditor",
+    "workforceId": "E84-013",
     "managerId": 4,
     "photoURL": "",
     "_displayName": "Paul Barnes - Internal Auditor",
@@ -1058,6 +1071,7 @@
     "mail": "jstevensd@ocn.ne.jp",
     "telephoneNumber": "(133) 596-4078",
     "title": "Automation Specialist I",
+    "workforceId": "E84-014",
     "managerId": 9,
     "photoURL": "/images/avatars/14.jpg",
     "_displayName": "Joe Stevens - Automation Specialist I",
@@ -1138,6 +1152,7 @@
     "mail": "rgrante@europa.eu",
     "telephoneNumber": "(343) 776-3486",
     "title": "Safety Technician I",
+    "workforceId": "E84-015",
     "managerId": 20,
     "photoURL": "/images/avatars/15.jpg",
     "_displayName": "Randy Grant - Safety Technician I",
@@ -1218,6 +1233,7 @@
     "mail": "mmasonf@who.int",
     "telephoneNumber": "(285) 576-0850",
     "title": "Compensation Analyst",
+    "workforceId": "E84-016",
     "managerId": 9,
     "photoURL": "/images/avatars/16.jpg",
     "_displayName": "Martin Mason - Compensation Analyst",
@@ -1298,6 +1314,7 @@
     "mail": "cporterg@a8.net",
     "telephoneNumber": "(594) 905-7773",
     "title": "Data Coordiator",
+    "workforceId": "E84-017",
     "managerId": 16,
     "photoURL": "/images/avatars/17.jpg",
     "_displayName": "Cynthia Porter - Data Coordiator",
@@ -1378,6 +1395,7 @@
     "mail": "nburnsh@wordpress.org",
     "telephoneNumber": "(444) 231-5492",
     "title": "Business Systems Development Analyst",
+    "workforceId": "E84-018",
     "managerId": 17,
     "photoURL": "/images/avatars/18.jpg",
     "_displayName": "Nancy Burns - Business Systems Development Analyst",
@@ -1458,6 +1476,7 @@
     "mail": "jmontgomeryi@addtoany.com",
     "telephoneNumber": "(675) 799-8793",
     "title": "Structural Engineer",
+    "workforceId": "E84-019",
     "managerId": 18,
     "photoURL": "/images/avatars/19.jpg",
     "_displayName": "Jimmy Montgomery - Structural Engineer",
@@ -1538,6 +1557,7 @@
     "mail": "bcarrollj@paypal.com",
     "telephoneNumber": "(658) 289-4550",
     "title": "Desktop Support Technician",
+    "workforceId": "E84-020",
     "managerId": 19,
     "photoURL": "/images/avatars/20.jpg",
     "_displayName": "Bruce Carroll - Desktop Support Technician",
@@ -1618,6 +1638,7 @@
     "mail": "orphan.user@gmail.com",
     "telephoneNumber": "(454) 249-4440",
     "title": "No Real Position",
+    "workforceId": "E84-021",
     "managerId": null,
     "photoURL": "/images/avatars/21.jpg",
     "_displayName": "Orphan User - No Real Position",

+ 2 - 2
client/src/services/people.service.ts

@@ -21,7 +21,7 @@
  */
 
 
-import { isString, IHttpService, ILogService, IPromise, IQService, IWindowService } from 'angular';
+import { IHttpService, ILogService, IPromise, IQService, IWindowService } from 'angular';
 import { IPerson } from '../models/person.model';
 import IPwmService from './pwm.service';
 import IOrgChartData from '../models/orgchart-data.model';
@@ -149,7 +149,7 @@ export default class PeopleService implements IPeopleService {
         let httpTimeout = this.$q.defer();
 
         let request = this.$http
-            .get(this.pwmService.getServerUrl('detail'), {
+            .get(this.pwmService.getPeopleSearchServerUrl('detail'), {
                 cache: true,
                 params: { userKey: id },
                 timeout: httpTimeout.promise

+ 10 - 11
client/src/services/config.service.dev.ts → client/src/services/peoplesearch-config.service.dev.ts

@@ -21,12 +21,19 @@
  */
 
 
-import { IConfigService } from './config.service';
 import { IPromise, IQService } from 'angular';
+import {ConfigBaseService} from './base-config.service.dev';
+import {IConfigService} from './base-config.service';
+import {IPeopleSearchConfigService} from './peoplesearch-config.service';
 
-export default class ConfigService implements IConfigService {
+
+export default class ConfigService
+                     extends ConfigBaseService
+                     implements IConfigService, IPeopleSearchConfigService {
     static $inject = [ '$q' ];
-    constructor(private $q: IQService) {}
+    constructor($q: IQService) {
+        super($q);
+    }
 
     getColumnConfig(): IPromise<any> {
         return this.$q.resolve({
@@ -38,15 +45,7 @@ export default class ConfigService implements IConfigService {
         });
     }
 
-    photosEnabled(): IPromise<boolean> {
-        return this.$q.resolve(true);
-    }
-
     orgChartEnabled(): IPromise<boolean> {
         return this.$q.resolve(true);
     };
-
-    getValue(key: string): IPromise<any> {
-        return null;
-    }
 }

+ 53 - 0
client/src/services/peoplesearch-config.service.ts

@@ -0,0 +1,53 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IHttpService, ILogService, IPromise, IQService } from 'angular';
+import IPwmService from './pwm.service';
+import PwmService from './pwm.service';
+import {ConfigBaseService, IConfigService} from './base-config.service';
+
+const COLUMN_CONFIG = 'peoplesearch_search_columns';
+const ORGCHART_ENABLED = 'peoplesearch_orgChartEnabled';
+
+export interface IPeopleSearchConfigService extends IConfigService {
+    orgChartEnabled(): IPromise<boolean>;
+}
+
+export default class PeopleSearchConfigService
+                     extends ConfigBaseService
+                     implements IConfigService, IPeopleSearchConfigService {
+
+    static $inject = ['$http', '$log', '$q', 'PwmService' ];
+    constructor($http: IHttpService, $log: ILogService, $q: IQService, pwmService: IPwmService) {
+        super($http, $log, $q, pwmService);
+    }
+
+    getColumnConfig(): IPromise<any> {
+        return this.getValue(COLUMN_CONFIG);
+    }
+
+    orgChartEnabled(): IPromise<boolean> {
+        return this.getValue(ORGCHART_ENABLED)
+            .then(null, () => { return this.$q.resolve(true); }); // On error use default
+    }
+}

+ 9 - 1
client/src/services/pwm.service.dev.ts

@@ -21,13 +21,21 @@
  */
 
 
-import { IPwmService } from './pwm.service';
+import {IHttpRequestOptions, IPwmService} from './pwm.service';
 
 export default class PwmService implements IPwmService {
+    getPeopleSearchServerUrl(processAction: string, additionalParameters?: any): string {
+        return null;
+    }
+
     getServerUrl(processAction: string, additionalParameters?: any): string {
         return null;
     }
 
+    httpRequest<T>(url: string, options: IHttpRequestOptions): angular.IPromise<T> {
+        return null;
+    }
+
     get ajaxTypingWait(): number {
         return 300;
     }

+ 57 - 7
client/src/services/pwm.service.ts

@@ -21,10 +21,17 @@
  */
 
 
-import { ILogService, IWindowService } from 'angular';
+import {IHttpService, ILogService, IPromise, IQService, IWindowService} from 'angular';
+
+export interface IHttpRequestOptions {
+    data?: any;
+    preventCache?: boolean;
+}
 
 export interface IPwmService {
+    getPeopleSearchServerUrl(processAction: string, additionalParameters?: any): string;
     getServerUrl(processAction: string, additionalParameters?: any): string;
+    httpRequest<T>(url: string, options: IHttpRequestOptions): IPromise<T>;
     ajaxTypingWait: number;
     localeStrings: any;
     startupFunctions: any[];
@@ -38,8 +45,11 @@ export default class PwmService implements IPwmService {
 
     urlContext: string;
 
-    static $inject = [ '$log', '$window' ];
-    constructor(private $log: ILogService, $window: IWindowService) {
+    static $inject = [ '$http', '$log', '$q', '$window' ];
+    constructor(private $http: IHttpService,
+                private $log: ILogService,
+                private $q: IQService,
+                $window: IWindowService) {
         this.urlContext = '';
 
         // Search window references to PWM_GLOBAL and PWM_MAIN add by legacy PWM code
@@ -59,13 +69,54 @@ export default class PwmService implements IPwmService {
         }
     }
 
-    getServerUrl(processAction: string, additionalParameters?: any): string {
-        let url: string = window.location.pathname + '?processAction=' + processAction;
+    private getApiPathname(route: string) {
+        return this.urlContext + route;
+    }
+
+    private getEndpointServerUrl(pathname: string, processAction: string, additionalParameters?: any): string {
+        let url: string = pathname + '?processAction=' + processAction;
         url = this.addParameters(url, additionalParameters);
 
         return url;
     }
 
+    getPeopleSearchServerUrl(processAction: string, additionalParameters?: any): string {
+        let pathname: string = this.getApiPathname('/private/peoplesearch');
+        return this.getEndpointServerUrl(pathname, processAction, additionalParameters);
+    }
+
+    getServerUrl(processAction: string, additionalParameters?: any): string {
+        return this.getEndpointServerUrl(window.location.pathname, processAction, additionalParameters);
+    }
+
+    private handlePwmError(response): IPromise<any> {
+        // TODO: show error dialog (like PWM_MAIN.ajaxRequest)
+        const errorMessage = `${response.data['errorCode']}: ${response.data['errorMessage']}`;
+        this.$log.error(errorMessage);
+
+        return this.$q.reject(response.data['errorMessage']);
+    }
+
+    httpRequest<T>(url: string, options: IHttpRequestOptions): IPromise<T> {
+        // TODO: implement alternate http method, no Content-Type if no options.data
+        let formID: string = encodeURIComponent('&pwmFormID=' + this.PWM_GLOBAL['pwmFormID']);
+        url += '&pwmFormID=' + this.PWM_GLOBAL['pwmFormID'];
+        let promise = this.$http
+            .post(url, options.data, {
+                cache: !options.preventCache,
+                headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
+            })
+            .then((response) => {
+                if (response.data['error']) {
+                    return this.handlePwmError(response);
+                }
+
+                return this.$q.resolve(response.data['data']);
+            });
+
+        return promise;
+    }
+
     get ajaxTypingWait(): number {
         if (this.PWM_GLOBAL) {
             return this.PWM_GLOBAL['client.ajaxTypingWait'] || DEFAULT_AJAX_TYPING_WAIT;
@@ -90,14 +141,13 @@ export default class PwmService implements IPwmService {
         return [];
     }
 
-
     private addParameters(url: string, params: any): string {
         if (!this.PWM_MAIN) {
             return url;
         }
 
         if (params) {
-            for (var name in params) {
+            for (let name in params) {
                 if (params.hasOwnProperty(name)) {
                     url = this.PWM_MAIN.addParamToUrl(url, name, params[name]);
                 }

+ 11 - 0
client/src/ux/button.component.scss

@@ -51,4 +51,15 @@ mf-button {
       border-color: #dae1e1;
     }
   }
+
+  &[disabled] {
+    > button {
+      background-color: #f6f9f8;
+      border-color: #dae1e1;
+      color: #434c50;
+      cursor: default;
+      opacity: 0.4;
+      outline: none;
+    }
+  }
 }

+ 24 - 0
client/src/ux/ias-dialog.component.html

@@ -0,0 +1,24 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ias-dialog-content ng-click="$event.stopPropagation()" ng-transclude>
+</ias-dialog-content>

+ 104 - 0
client/src/ux/ias-dialog.component.scss

@@ -0,0 +1,104 @@
+/*!
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+@keyframes fadein {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+
+.ias-title {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: #808080;
+  font-size: 15px;
+  margin: 0;
+  padding: 0;
+  text-transform: uppercase;
+}
+
+ias-dialog {
+  animation: fadein 500ms;
+  animation-fill-mode: forwards;
+  align-items: center;
+  background-color: rgba(128, 128, 128, .7);
+  display: flex;
+  height: 100vh;
+  justify-content: center;
+  opacity: 0;
+  position: fixed;
+  top: 0;
+  width: 100vw;
+  z-index: 100;
+}
+
+ias-dialog-content {
+  background-color: #fff;
+  border-radius: 3px;
+  display: block;
+  max-height: 100%;
+  max-width: 100%;
+  overflow: auto;
+  position: relative;
+
+  .ias-dialog-close-button {
+    position: absolute;
+    right: 10px;
+    top: 10px;
+  }
+
+  .ias-actions {
+    padding: 25px;
+  }
+
+  .ias-dialog-body {
+    padding: 0 25px;
+    //width: 100%;
+  }
+
+  .ias-dialog-header {
+    padding: 25px 2 * 25px 10px 25px;
+  }
+
+  input:focus {
+    color: #434c50;
+  }
+}
+
+@media (min-width: 768px) {
+  ias-dialog-content {
+    min-width: 320px;
+  }
+}
+
+[dir="rtl"] {
+  ias-dialog-content {
+    .ias-dialog-close-button {
+      left: 10px;
+      right: auto;
+    }
+
+    > .ias-dialog-header {
+      padding: 25px 25px 10px 50px;
+    }
+  }
+}

+ 50 - 0
client/src/ux/ias-dialog.component.ts

@@ -0,0 +1,50 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IAugmentedJQuery } from 'angular';
+import { Component } from '../component';
+import DialogService from './ias-dialog.service';
+
+@Component({
+    stylesheetUrl: require('./ias-dialog.component.scss'),
+    templateUrl: require('./ias-dialog.component.html'),
+    transclude: true
+})
+export default class IasDialogComponent {
+    static $inject = [ '$element', 'IasDialogService' ];
+    constructor(private $element: IAugmentedJQuery, private dialogService: DialogService) {
+        // $element.on('click', this.cancel.bind(this));
+    }
+
+    $destroy(): void {
+        // this.$element.off();
+    }
+
+    cancel(): void {
+        this.dialogService.cancel();
+    }
+
+    close(): void {
+        this.dialogService.close();
+    }
+}

+ 200 - 0
client/src/ux/ias-dialog.service.ts

@@ -0,0 +1,200 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {
+    element,
+    IAugmentedJQuery,
+    ICompileService,
+    IControllerService,
+    IDeferred,
+    IDocumentService,
+    IHttpService,
+    IPromise,
+    IQService,
+    IRootScopeService,
+    IScope,
+    ITemplateCacheService
+} from 'angular';
+import DialogComponent from './dialog.component';
+
+interface IDialogScope extends IScope {
+    $ctrl: DialogComponent;
+    cancel: () => void;
+    cancelText: string;
+    close: () => void;
+    okText: string;
+    prompt: boolean;
+    data: any;
+    textContent: string;
+    title: string;
+}
+
+interface IDialogOptions {
+    cancel?: string;
+    controller?: any;
+    ok?: string;
+    prompt?: boolean;
+    response?: string;
+    scope?: IScope;
+    template?: string;
+    templateUrl?: string;
+    textContent?: string;
+    title?: string;
+    locals?: any;
+}
+
+export default class DialogService {
+    private compiledDialogElement: IAugmentedJQuery;
+    private dialogController: any;
+    private dialogDeferred: IDeferred<any>;
+
+    static $inject = [ '$compile', '$controller', '$document', '$http', '$q', '$rootScope', '$templateCache' ];
+    constructor(private $compile: ICompileService,
+                private $controller: IControllerService,
+                private $document: IDocumentService,
+                private $http: IHttpService,
+                private $q: IQService,
+                private $rootScope: IRootScopeService,
+                private $templateCache: ITemplateCacheService) {
+    }
+
+    alert(options: IDialogOptions): IPromise<any> {
+        options.cancel = null;
+        options.ok = options.ok || 'OK';
+
+        return this.open(options);
+    }
+
+    cancel(response?: any): void {
+        this.dialogDeferred.reject(response);
+        this.destroy();
+    }
+
+    close(response?: any): void {
+        this.dialogDeferred.resolve(response);
+        this.destroy();
+    }
+
+    confirm(options: IDialogOptions): IPromise<any> {
+        options.cancel = options.cancel || 'No';
+        options.ok = options.ok || 'Yes';
+
+        return this.open(options);
+    }
+
+    private destroy() {
+        this.compiledDialogElement.detach();
+        this.dialogController = null;
+        this.dialogDeferred = null;
+    }
+
+    open(options: IDialogOptions): IPromise<any> {
+        let self = this;
+
+        // Initialize scope
+        let scope = options.scope ? options.scope.$new(false) : <IDialogScope>(this.$rootScope.$new(true));
+        scope.cancel = () => { self.cancel(); };
+        scope.cancelText = options.cancel;
+        scope.close = () => { self.close(scope.data.response); };
+        scope.okText = options.ok;
+        scope.prompt = options.prompt;
+        scope.data = { response: options.response };
+        scope.textContent = options.textContent;
+        scope.title = options.title;
+        let locals = options.locals || {};
+        locals.$scope = scope;
+
+        // Instantiate controller if provided
+        if (options.controller) {
+            this.dialogController = this.$controller(options.controller, locals);
+        }
+
+        // Compile template
+        this.loadTemplate(options)
+            .then((template) => {
+                self.compiledDialogElement = self.$compile(template)(locals.$scope);
+
+                // Insert element into DOM
+                element(self.$document.find('body')).append(self.compiledDialogElement);
+            });
+
+        this.dialogDeferred = this.$q.defer();
+        return this.dialogDeferred.promise;
+    }
+
+    prompt(options: IDialogOptions): IPromise<any> {
+        options.cancel = options.cancel || 'Cancel';
+        options.ok = options.ok || 'OK';
+        options.prompt = true;
+
+        return this.open(options);
+    }
+
+    private loadTemplate(options: IDialogOptions): IPromise<string> {
+
+        if (options.template) {
+            return this.$q.resolve(options.template);
+        }
+
+        else if (options.templateUrl) {
+            let template: string = this.$templateCache.get<string>(options.templateUrl);
+            let self = this;
+
+            if (template) {
+                return this.$q.resolve(template);
+            }
+
+            return this.$http
+                .get(options.templateUrl)
+                .then((response) => {
+                    self.$templateCache.put(options.templateUrl, response.data);
+                    return response.data;
+                });
+        }
+
+        else {
+            return this.$q.resolve(
+                '<ias-dialog>' +
+                '   <div class="ias-dialog-header">' +
+                '       <div ng-if="!!title" class="ias-title">{{title}}</div>' +
+                '   </div>' +
+                '   <div class="ias-dialog-body">' +
+                '       <div ng-if="!prompt">{{textContent}}</div>' +
+                '       <div ng-if="prompt">' +
+                '           <ias-input-container>' +
+                '               <label for="response">{{textContent}}</label>' +
+                '               <input id="response" name="response" type="text" ng-model="data.response">' +
+                '           </ias-input-container>' +
+                '       </div>' +
+                '   </div>' +
+                '   <div class="ias-actions">' +
+                '      <mf-button ng-if="!!okText" ng-click="close()">{{okText}}</mf-button>' +
+                '      <mf-button ng-if="!!cancelText" ng-click="cancel()">{{cancelText}}</mf-button>' +
+                '   </div>' +
+                '   <mf-icon-button class="ias-dialog-close-button" icon="close_thick" ng-click="cancel()">' +
+                '   </mf-icon-button>' +
+                '</ias-dialog>'
+            );
+        }
+    }
+}

+ 93 - 0
client/src/ux/tabset.directive.scss

@@ -0,0 +1,93 @@
+/*!
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+.mf-tabset {
+  box-sizing: content-box;
+  color: #575b5d;
+  display: flex;
+  flex-flow: row nowrap;
+  font-size: 15px;
+  height: 28px;
+  margin-bottom: 15px;
+  max-width: 100%;
+  overflow: hidden;
+  padding-right: 15px;
+  padding-top: 2px;
+  position: relative;
+}
+
+.mf-tab-base {
+  border-bottom: 1px solid #6a6f71;
+  left: 0;
+  position: absolute;
+  top: 29px;
+  width: 100%;
+}
+
+.mf-tab {
+  &:first-child {
+    margin-left: 10px;
+  }
+
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  background-color: #f6f9f8;
+  border: 1px solid #dae1e1;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+  box-sizing: content-box;
+  color: #434c50;
+  margin-right: 5px;
+  min-width: 40px;
+  padding: 5px 10px;
+  text-align: center;
+
+  &:not([disabled]):not(.mf-selected) {
+    &:focus,
+    &:hover {
+      background-color: #f6f9f8;
+      border-color: #01a9e7;
+      color: #007cd0;
+      cursor: pointer;
+      outline: none;
+    }
+  }
+
+  &[disabled],
+  &.mf-disabled {
+    color: grey;
+    cursor: default;
+  }
+
+  &.mf-selected {
+    color: #434c50;
+    cursor: default;
+    font-weight: 500;
+    background-color: white;
+    border-color: #808080;
+    border-bottom: 4px solid white;
+    outline: none;
+    z-index: 1;
+  }
+}

+ 83 - 0
client/src/ux/tabset.directive.ts

@@ -0,0 +1,83 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { element, forEach, IAttributes, IAugmentedJQuery, IScope } from 'angular';
+
+interface ITabsetController {
+    activeTab: number;
+    activateTab(tabIndex: number): void;
+}
+
+export class TabsetController implements ITabsetController {
+    activeTab;
+
+    activateTab(tabIndex): void {
+        this.activeTab = tabIndex;
+    };
+}
+
+require('ux/tabset.directive.scss');
+
+export default function TabsetDirective() {
+    return {
+        scope: true,
+        restrict: 'E',
+        controller: TabsetController,
+        controllerAs: '$tabsetCtrl',
+        compile: (tElement: IAugmentedJQuery) => {
+            // Nest element contents inside a tabset
+            let tabset = element(`<div class="mf-tabset" role="tablist"></div>`);
+            forEach(tElement.contents(), (content: Element) => {
+                tabset.append(content);
+            });
+            tElement.append(tabset);
+
+            // Switch out 'mf-tab' elements for tabs and panes
+            let tabs = element(tabset).find('mf-tab');
+            let tab;
+            forEach(tabs, (tabElement: HTMLElement, index: number) => {
+                // Add tab
+                let label = tabElement.getAttribute('label');
+                tab = element(`<button ng-class="{'mf-tab': true, 'mf-selected': $tabsetCtrl.activeTab === ${index}}"
+                                       ng-click="$tabsetCtrl.activateTab(${index})"
+                                       role="tab">${label}</button>`);
+                element(tabElement).replaceWith(tab);
+
+                // Add pane
+                let pane = element(`<div class="mf-tab-pane-container"
+                                         ng-if="$tabsetCtrl.activeTab === ${index}"></div>`);
+                pane.append(tabElement.innerHTML);
+                tElement.append(pane);
+            });
+
+            // Add tab base and fill after the last tab, before any right-aligned elements such as links
+            if (tab) {
+                tab.after(`<div class="mf-tab-base"></div><div class="mf-fill"></div>`);
+            }
+
+            return (scope: IScope, iElement: IAugmentedJQuery, iAttrs: IAttributes, tabsetCtrl: ITabsetController) => {
+                tabsetCtrl.activeTab = Number(iAttrs.mfActiveTab) || 0;
+            };
+        }
+    };
+}

+ 6 - 0
client/src/ux/ux.module.ts

@@ -26,6 +26,7 @@ import AppBarComponent from './app-bar.component';
 import AutoCompleteComponent from './auto-complete.component';
 import ButtonComponent from './button.component';
 import DialogComponent from './dialog.component';
+import IasDialogComponent from './ias-dialog.component';
 // import { DialogService } from './dialog.service';
 import IconButtonComponent from './icon-button.component';
 import IconComponent from './icon.component';
@@ -33,10 +34,13 @@ import SearchBarComponent from './search-bar.component';
 import TableDirectiveFactory from './table.directive';
 import TableColumnDirectiveFactory from './table-column.directive';
 import ElementSizeService from './element-size.service';
+import TabsetDirective from './tabset.directive';
+import DialogService from './ias-dialog.service';
 
 var moduleName = 'peoplesearch.ux';
 
 module(moduleName, [ ])
+    .component('iasDialog', IasDialogComponent)
     .component('mfAppBar', AppBarComponent)
     .component('mfAutoComplete', AutoCompleteComponent)
     .component('mfButton', ButtonComponent)
@@ -46,6 +50,8 @@ module(moduleName, [ ])
     .component('mfSearchBar', SearchBarComponent)
     .directive('mfTable', TableDirectiveFactory)
     .directive('mfTableColumn', TableColumnDirectiveFactory)
+    .directive('mfTabset', TabsetDirective)
+    .service('IasDialogService', DialogService)
     .service('MfElementSizeService', ElementSizeService);
     // .service('MfDialogService', DialogService);
 

+ 1 - 0
client/tsconfig.json

@@ -2,6 +2,7 @@
   "compilerOptions": {
     "baseUrl": "./",
     "experimentalDecorators": true,
+    "lib": ["es2015", "es2015.iterable", "dom"],
     "module": "commonjs",
     "removeComments": true,
     "sourceMap": true,

+ 2 - 1
client/webpack.build.js

@@ -30,7 +30,8 @@ module.exports = webpackMerge(commonConfig, {
     entry: {
         'peoplesearch.ng': './src/main',
         'changepassword.ng': './src/pages/changepassword/changepassword.module',
-        'configeditor.ng': './src/pages/configeditor/configeditor.module'
+        'configeditor.ng': './src/pages/configeditor/configeditor.module',
+        'helpdesk.ng': './src/helpdesk/main'
     },
     module: {
         loaders: [

+ 10 - 0
client/webpack.common.js

@@ -87,7 +87,17 @@ module.exports = {
     },
     plugins: [
         new HtmlWebpackPlugin({
+            chunks: ['peoplesearch.ng'],
+            filename: 'peoplesearch.html',
             template: 'index.html',
+            // title: 'PeopleSearch Development',
+            inject: 'body'
+        }),
+        new HtmlWebpackPlugin({
+            chunks: ['helpdesk.ng'],
+            filename: 'helpdesk.html',
+            template: 'index.html',
+            // title: 'PeopleSearch Development',
             inject: 'body'
         })
     ],

+ 2 - 1
client/webpack.dev.js

@@ -30,7 +30,8 @@ module.exports = webpackMerge(commonConfig, {
     entry: {
         'peoplesearch.ng': './src/main.dev',
         'changepassword.ng': './src/pages/changepassword/changepassword.module',
-        'configeditor.ng': './src/pages/configeditor/configeditor.module'
+        'configeditor.ng': './src/pages/configeditor/configeditor.module',
+        'helpdesk.ng': './src/helpdesk/main.dev'
     },
     module: {
         loaders: [

+ 22 - 0
npm-debug.log

@@ -0,0 +1,22 @@
+0 info it worked if it ends with ok
+1 verbose cli [ 'C:\\Program Files\\nodejs\\node.exe',
+1 verbose cli   'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
+1 verbose cli   'start' ]
+2 info using npm@3.10.10
+3 info using node@v6.10.1
+4 verbose stack Error: ENOENT: no such file or directory, open 'D:\deps-dx\pwm\pwm\package.json'
+4 verbose stack     at Error (native)
+5 verbose cwd D:\deps-dx\pwm\pwm
+6 error Windows_NT 10.0.14393
+7 error argv "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "start"
+8 error node v6.10.1
+9 error npm  v3.10.10
+10 error path D:\deps-dx\pwm\pwm\package.json
+11 error code ENOENT
+12 error errno -4058
+13 error syscall open
+14 error enoent ENOENT: no such file or directory, open 'D:\deps-dx\pwm\pwm\package.json'
+15 error enoent ENOENT: no such file or directory, open 'D:\deps-dx\pwm\pwm\package.json'
+15 error enoent This is most likely not a problem with npm itself
+15 error enoent and is related to npm not being able to find a file.
+16 verbose exit [ -4058, true ]

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -61,7 +61,7 @@ Button_Unlock=Unlock
 Button_UnlockPassword=Unlock Password
 Button_Update=Update
 Button_Verify=Verify
-Button_Verificiations=Verifications
+Button_Verifications=Verifications
 Button_OK=OK
 Display_ActivateUser=To confirm your identity, please enter the following information. Your information will be used to locate and activate your user account.<p/>Be sure to complete the process, or your account will not be activated properly.
 Display_AutoGeneratedPassword=Auto-generate a new password

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_ca.properties

@@ -61,7 +61,7 @@ Button_Unlock=Desbloqueja
 Button_UnlockPassword=Desbloqueja la contrasenya
 Button_Update=Actualitza
 Button_Verify=Verifica
-Button_Verificiations=Verificacions
+Button_Verifications=Verificacions
 Button_OK=D'acord
 Display_ActivateUser=Per tal de confirmar la seva identitat, introdueixi la informaci\u00f3 seg\u00fcent. La informaci\u00f3 s'utilitzar\u00e0 per localitzar i activar el seu compte d'usuari.<p/>Asseguri's de finalitzar el proc\u00e9s o el compte no s'activar\u00e0 correctament.
 Display_AutoGeneratedPassword=Genera una contrasenya nova autom\u00e0ticament

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_da.properties

@@ -61,7 +61,7 @@ Button_Unlock=L\u00e5s op
 Button_UnlockPassword=Adgangskode til opl\u00e5sning
 Button_Update=Opdater
 Button_Verify=Bekr\u00e6ft
-Button_Verificiations=Bekr\u00e6ftelser
+Button_Verifications=Bekr\u00e6ftelser
 Button_OK=OK
 Display_ActivateUser=Angiv f\u00f8lgende oplysninger for at bekr\u00e6fte din identitet. Dine oplysninger bruges til at lokalisere og aktivere din brugerkonto.<p/>S\u00f8rg for at gennemf\u00f8re processen, ellers aktiveres din konto ikke korrekt.
 Display_AutoGeneratedPassword=Gener\u00e9r automatisk en ny adgangskode

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_de.properties

@@ -61,7 +61,7 @@ Button_Unlock=Entsperren
 Button_UnlockPassword=Passwort entsperren
 Button_Update=Aktualisieren
 Button_Verify=\u00dcberpr\u00fcfen
-Button_Verificiations=\u00dcberpr\u00fcfungen
+Button_Verifications=\u00dcberpr\u00fcfungen
 Button_OK=OK
 Display_ActivateUser=Geben Sie die folgenden Informationen ein, um Ihre Identit\u00e4t zu best\u00e4tigen. Anhand dieser Informationen wird Ihr Benutzerkonto ermittelt und aktiviert.<p/>Schlie\u00dfen Sie den Vorgang vollst\u00e4ndig ab, damit Ihr Konto ordnungsgem\u00e4\u00df aktiviert werden kann.
 Display_AutoGeneratedPassword=Automatisch ein neues Passwort generieren

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_en_CA.properties

@@ -61,7 +61,7 @@ Button_Unlock=Unlock
 Button_UnlockPassword=Unlock Password
 Button_Update=Update
 Button_Verify=Verify
-Button_Verificiations=Verifications
+Button_Verifications=Verifications
 Button_OK=OK
 Display_ActivateUser=To confirm your identity, please enter the following information.Your information will be used to locate and activate your user account.<p/>Be sure to complete the process or your account will not be activated properly.
 Display_AutoGeneratedPassword=Auto-generate a new password

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_es.properties

@@ -61,7 +61,7 @@ Button_Unlock=Desbloquear
 Button_UnlockPassword=Desbloquear contrase\u00f1a
 Button_Update=Actualizar
 Button_Verify=Verificar
-Button_Verificiations=Verificaciones
+Button_Verifications=Verificaciones
 Button_OK=Aceptar
 Display_ActivateUser=Para confirmar su identidad, introduzca la siguiente informaci\u00f3n. Su informaci\u00f3n se utilizar\u00e1 para localizar y activar su cuenta de usuario.<p/>Aseg\u00farese de completar el proceso o no se activar\u00e1 correctamente su cuenta.
 Display_AutoGeneratedPassword=Generar autom\u00e1ticamente nueva contrase\u00f1a

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_fr.properties

@@ -61,7 +61,7 @@ Button_Unlock=D\u00e9verrouiller
 Button_UnlockPassword=Mot de passe de d\u00e9verrouillage
 Button_Update=Mettre \u00e0 jour
 Button_Verify=V\u00e9rifier
-Button_Verificiations=V\u00e9rifications
+Button_Verifications=V\u00e9rifications
 Button_OK=OK
 Display_ActivateUser=Pour confirmer votre identit\u00e9, entrez les informations suivantes. Elles serviront \u00e0 localiser votre compte utilisateur et \u00e0 l'activer.<p/>Veillez \u00e0 finaliser le processus pour que votre compte soit activ\u00e9 correctement.
 Display_AutoGeneratedPassword=G\u00e9n\u00e9rer automatiquement un nouveau mot de passe

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_fr_CA.properties

@@ -61,7 +61,7 @@ Button_Unlock=D\u00e9verrouiller
 Button_UnlockPassword=Mot de passe de d\u00e9verrouillage
 Button_Update=Mettre \u00e0 jour
 Button_Verify=V\u00e9rifier
-Button_Verificiations=V\u00e9rifications
+Button_Verifications=V\u00e9rifications
 Button_OK=OK
 Display_ActivateUser=Pour confirmer votre identit\u00e9, entrez les informations suivantes. Elles serviront \u00e0 localiser votre compte utilisateur et \u00e0 l'activer.<p/>Veillez \u00e0 finaliser le processus pour que votre compte soit activ\u00e9 correctement.
 Display_AutoGeneratedPassword=G\u00e9n\u00e9rer automatiquement un nouveau mot de passe

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_it.properties

@@ -61,7 +61,7 @@ Button_Unlock=Sblocca
 Button_UnlockPassword=Sblocca password
 Button_Update=Aggiorna
 Button_Verify=Verifica
-Button_Verificiations=Verifiche
+Button_Verifications=Verifiche
 Button_OK=OK
 Display_ActivateUser=Per confermare l'identit\u00e0, immettere le informazioni seguenti. Le informazioni dell'utente saranno utilizzate per individuare e attivare l'account utente.<p/>Affinch\u00e9 l'account venga attivato correttamente, \u00e8 necessario completare la procedura.
 Display_AutoGeneratedPassword=Generare automaticamente una nuova password

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_iw.properties

@@ -61,7 +61,7 @@ Button_Unlock=\u05d1\u05d8\u05dc \u05e0\u05e2\u05d9\u05dc\u05d4
 Button_UnlockPassword=\u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05d1\u05d9\u05d8\u05d5\u05dc \u05d4\u05e0\u05e2\u05d9\u05dc\u05d4
 Button_Update=\u05e2\u05d3\u05db\u05df
 Button_Verify=\u05d0\u05de\u05ea
-Button_Verificiations=\u05d0\u05d9\u05de\u05d5\u05ea\u05d9\u05dd
+Button_Verifications=\u05d0\u05d9\u05de\u05d5\u05ea\u05d9\u05dd
 Button_OK=\u05d0\u05d9\u05e9\u05d5\u05e8
 Display_ActivateUser=\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05dc\u05d4\u05dc\u05df \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d6\u05d4\u05d5\u05ea\u05da. \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05d9\u05e9\u05de\u05e9\u05d5 \u05dc\u05d0\u05d9\u05ea\u05d5\u05e8 \u05d5\u05dc\u05d4\u05e4\u05e2\u05dc\u05d4 \u05e9\u05dc \u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc\u05da.<p/>\u05d4\u05e7\u05e4\u05d3 \u05dc\u05d4\u05e9\u05dc\u05d9\u05dd \u05d0\u05ea \u05d4\u05ea\u05d4\u05dc\u05d9\u05da, \u05d0\u05d5 \u05e9\u05d7\u05e9\u05d1\u05d5\u05e0\u05da \u05dc\u05d0 \u05d9\u05d5\u05e4\u05e2\u05dc \u05db\u05d4\u05dc\u05db\u05d4.
 Display_AutoGeneratedPassword=\u05d9\u05e6\u05d9\u05e8\u05ea \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3\u05e9\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_ja.properties

@@ -61,7 +61,7 @@ Button_Unlock=\u30ed\u30c3\u30af\u89e3\u9664
 Button_UnlockPassword=\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u30ed\u30c3\u30af\u89e3\u9664
 Button_Update=\u66f4\u65b0
 Button_Verify=\u691c\u8a3c
-Button_Verificiations=\u691c\u8a3c
+Button_Verifications=\u691c\u8a3c
 Button_OK=OK
 Display_ActivateUser=\u672c\u4eba\u78ba\u8a8d\u3092\u3059\u308b\u305f\u3081\u306b\u3001\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u306e\u60c5\u5831\u306f\u30e6\u30fc\u30b6\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u898b\u3064\u3051\u3066\u6709\u52b9\u306b\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002<p/>\u30d7\u30ed\u30bb\u30b9\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u6709\u52b9\u306b\u306a\u308a\u307e\u305b\u3093\u3002
 Display_AutoGeneratedPassword=\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u81ea\u52d5\u751f\u6210

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_nl.properties

@@ -61,7 +61,7 @@ Button_Unlock=Ontgrendelen
 Button_UnlockPassword=Wachtwoord ontgrendelen
 Button_Update=Bijwerken
 Button_Verify=Verifi\u00ebren
-Button_Verificiations=Verificaties
+Button_Verifications=Verificaties
 Button_OK=OK
 Display_ActivateUser=Ter bevestiging van uw identiteit dient u de volgende gegevens in te voeren. Deze gegevens worden gebruikt om uw gebruikersaccount te vinden en te activeren.<p/> Zorg ervoor dat u het proces volledig doorloopt. Anders wordt uw account niet juist geactiveerd.
 Display_AutoGeneratedPassword=Een nieuw wachtwoord automatisch genereren

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_no.properties

@@ -61,7 +61,7 @@ Button_Unlock=L\u00e5s opp
 Button_UnlockPassword=L\u00e5s opp passord
 Button_Update=Oppdater
 Button_Verify=Verifiser
-Button_Verificiations=Verifiseringer
+Button_Verifications=Verifiseringer
 Button_OK=OK
 Display_ActivateUser=For \u00e5 bekrefte identiteten din, fyll ut f\u00f8lgende informasjon. Din informasjon vil bli brukt til \u00e5 finne og aktivere din brukerkonto.<p/>S\u00f8rg for \u00e5 fullf\u00f8re prosessen. Hvis du avbryter, vil ikke brukerkontoen din bli korrekt aktivert.
 Display_AutoGeneratedPassword=Auto-generere et nytt passord

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_pl.properties

@@ -61,7 +61,7 @@ Button_Unlock=Odblokuj
 Button_UnlockPassword=Odblokuj has\u0142o
 Button_Update=Aktualizuj
 Button_Verify=Weryfikuj
-Button_Verificiations=Weryfikacje
+Button_Verifications=Weryfikacje
 Button_OK=OK
 Display_ActivateUser=Aby potwierdzi\u0107 swoj\u0105 to\u017csamo\u015b\u0107, wprowad\u017a poni\u017csze informacje. Te informacje pos\u0142u\u017c\u0105 do odnalezienia i aktywowania konta u\u017cytkownika.<p/>Wykonaj ten proces w ca\u0142o\u015bci, w przeciwnym razie konto nie zostanie poprawnie aktywowane.
 Display_AutoGeneratedPassword=Automatyczne generowanie nowego has\u0142a

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_pt_BR.properties

@@ -61,7 +61,7 @@ Button_Unlock=Desbloquear
 Button_UnlockPassword=Desbloquear Senha
 Button_Update=Atualizar
 Button_Verify=Verificar
-Button_Verificiations=Verifica\u00e7\u00f5es
+Button_Verifications=Verifica\u00e7\u00f5es
 Button_OK=OK
 Display_ActivateUser=Para confirmar sua identidade, digite as informa\u00e7\u00f5es a seguir. Suas informa\u00e7\u00f5es ser\u00e3o usadas para localizar e ativar a conta do usu\u00e1rio.<p/>Conclua o processo ou sua conta n\u00e3o ser\u00e1 ativada adequadamente.
 Display_AutoGeneratedPassword=Gerar uma nova senha automaticamente

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_ru.properties

@@ -61,7 +61,7 @@ Button_Unlock=\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432
 Button_UnlockPassword=\u041f\u0430\u0440\u043e\u043b\u044c \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438
 Button_Update=\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c
 Button_Verify=\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c
-Button_Verificiations=\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0438
+Button_Verifications=\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0438
 Button_OK=\u041e\u041a
 Display_ActivateUser=\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u0438\u0435, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u043d\u0438\u0436\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e. \u0423\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0430 \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0438 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0438 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.<p/>\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441; \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c.
 Display_AutoGeneratedPassword=\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044f

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_sv.properties

@@ -61,7 +61,7 @@ Button_Unlock=L\u00e5s upp
 Button_UnlockPassword=L\u00e5s upp l\u00f6senordet
 Button_Update=Uppdatera
 Button_Verify=Verifiera
-Button_Verificiations=Verifieringar
+Button_Verifications=Verifieringar
 Button_OK=OK
 Display_ActivateUser=Du bekr\u00e4ftar din identitet genom att ange f\u00f6ljande information. Din information anv\u00e4nds f\u00f6r att hitta och aktivera ditt anv\u00e4ndarkonto.<p/>Du m\u00e5ste slutf\u00f6ra processen, annars aktiveras inte ditt konto korrekt.
 Display_AutoGeneratedPassword=Skapa ett nytt l\u00f6senord automatiskt

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_zh_CN.properties

@@ -61,7 +61,7 @@ Button_Unlock=\u89e3\u9664\u9501\u5b9a
 Button_UnlockPassword=\u89e3\u9664\u9501\u5b9a\u53e3\u4ee4
 Button_Update=\u66f4\u65b0
 Button_Verify=\u6821\u9a8c
-Button_Verificiations=\u6821\u9a8c
+Button_Verifications=\u6821\u9a8c
 Button_OK=\u786e\u5b9a
 Display_ActivateUser=\u8981\u786e\u8ba4\u60a8\u7684\u8eab\u4efd\uff0c\u8bf7\u8f93\u5165\u4ee5\u4e0b\u4fe1\u606f\u3002\u60a8\u7684\u4fe1\u606f\u5c06\u4f1a\u7528\u4e8e\u67e5\u627e\u4e0e\u6fc0\u6d3b\u60a8\u7684\u7528\u6237\u5e10\u6237\u3002<p/>\u8bf7\u786e\u4fdd\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u5426\u5219\u65e0\u6cd5\u6b63\u5e38\u6fc0\u6d3b\u60a8\u7684\u5e10\u6237\u3002
 Display_AutoGeneratedPassword=\u81ea\u52a8\u751f\u6210\u4e00\u4e2a\u65b0\u7684\u53e3\u4ee4

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Display_zh_TW.properties

@@ -61,7 +61,7 @@ Button_Unlock=\u89e3\u9664\u9396\u5b9a
 Button_UnlockPassword=\u89e3\u9664\u9396\u5b9a\u5bc6\u78bc
 Button_Update=\u66f4\u65b0
 Button_Verify=\u9a57\u8b49
-Button_Verificiations=\u9a57\u8b49
+Button_Verifications=\u9a57\u8b49
 Button_OK=\u78ba\u5b9a
 Display_ActivateUser=\u82e5\u8981\u78ba\u8a8d\u60a8\u7684\u8eab\u5206\uff0c\u8acb\u8f38\u5165\u4e0b\u5217\u8cc7\u8a0a\u3002\u7cfb\u7d71\u5c07\u4f7f\u7528\u60a8\u7684\u8cc7\u8a0a\u627e\u51fa\u4e26\u555f\u52d5\u60a8\u7684\u4f7f\u7528\u8005\u5e33\u6236\u3002<p/>\u8acb\u52d9\u5fc5\u5b8c\u6210\u7a0b\u5e8f\uff0c\u5426\u5247\u5c07\u7121\u6cd5\u6b63\u78ba\u555f\u52d5\u60a8\u7684\u5e33\u6236\u3002
 Display_AutoGeneratedPassword=\u81ea\u52d5\u7522\u751f\u65b0\u5bc6\u78bc

+ 0 - 296
server/src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp

@@ -1,296 +0,0 @@
-<%--
-  ~ Password Management Servlets (PWM)
-  ~ http://www.pwm-project.org
-  ~
-  ~ Copyright (c) 2006-2009 Novell, Inc.
-  ~ Copyright (c) 2009-2017 The PWM Project
-  ~
-  ~ This program is free software; you can redistribute it and/or modify
-  ~ it under the terms of the GNU General Public License as published by
-  ~ the Free Software Foundation; either version 2 of the License, or
-  ~ (at your option) any later version.
-  ~
-  ~ This program is distributed in the hope that it will be useful,
-  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
-  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  ~ GNU General Public License for more details.
-  ~
-  ~ You should have received a copy of the GNU General Public License
-  ~ along with this program; if not, write to the Free Software
-  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-  --%>
-
-<%@ page import="password.pwm.http.servlet.helpdesk.HelpdeskDetailInfoBean" %>
-<%@ page import="password.pwm.util.java.JavaHelper" %>
-<%@ page import="password.pwm.util.java.StringUtil" %>
-<%@ page import="java.time.Instant" %>
-<%@ page import="password.pwm.http.bean.DisplayElement" %>
-<%@ page import="password.pwm.http.servlet.accountinfo.AccountInformationBean" %>
-<!DOCTYPE html>
-<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html; charset=UTF-8" %>
-<%@ taglib uri="pwm" prefix="pwm" %>
-<% final PwmRequest pwmRequest = JspUtility.getPwmRequest(pageContext); %>
-<% final HelpdeskDetailInfoBean helpdeskDetailInfoBean = (HelpdeskDetailInfoBean)pwmRequest.getAttribute(PwmRequestAttribute.HelpdeskDetail); %>
-
-<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
-<%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
-<body class="nihilo">
-<div id="wrapper">
-    <jsp:include page="/WEB-INF/jsp/fragment/header-body.jsp">
-        <jsp:param name="pwm.PageName" value="Title_Helpdesk"/>
-    </jsp:include>
-    <div id="centerbody" style="min-width: 800px">
-        <div id="page-content-title"><pwm:display key="Title_Helpdesk" displayIfMissing="true"/></div>
-        <% if (!StringUtil.isEmpty(helpdeskDetailInfoBean.getUserDisplayName())) { %>
-        <h2 style="text-align: center"><%=StringUtil.escapeHtml(helpdeskDetailInfoBean.getUserDisplayName())%></h2>
-        <% } %>
-        <pwm:script>
-            <script type="text/javascript">
-                PWM_GLOBAL['startupFunctions'].push(function(){
-                    PWM_VAR["helpdesk_obfuscatedDN"] = '<%=JspUtility.getAttribute(pageContext, PwmRequestAttribute.HelpdeskObfuscatedDN)%>';
-                    PWM_VAR["helpdesk_username"] = '<%=helpdeskDetailInfoBean.getUserDisplayName()%>';
-                });
-            </script>
-        </pwm:script>
-        <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
-        <table class="noborder">
-            <tr>
-                <td class="noborder" style="width: 600px; max-width:600px; vertical-align: top">
-                    <div id="panel-helpdesk-detail" data-dojo-type="dijit.layout.TabContainer" style="max-width: 600px; height: 100%;" data-dojo-props="doLayout: false, persist: true" >
-                        <div id="Field_Profile" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Field_Profile"/>" class="tabContent">
-                            <div style="max-height: 400px; overflow: auto;">
-                                <table class="nomargin">
-                                    <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getProfileData()) { %>
-                                    <% request.setAttribute("displayElement", displayElement); %>
-                                    <jsp:include page="fragment/displayelement-row.jsp"/>
-                                    <% } %>
-                                </table>
-                            </div>
-                        </div>
-                        <div id="Title_Status" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_Status"/>" class="tabContent">
-                            <table class="nomargin">
-                                <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getStatusData()) { %>
-                                <% request.setAttribute("displayElement", displayElement); %>
-                                <jsp:include page="fragment/displayelement-row.jsp"/>
-                                <% } %>
-                            </table>
-                        </div>
-                        <% if (helpdeskDetailInfoBean.getUserHistory() != null && !helpdeskDetailInfoBean.getUserHistory().isEmpty()) { %>
-                        <div id="Title_UserEventHistory" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_UserEventHistory"/>" class="tabContent">
-                            <div style="max-height: 400px; overflow: auto;">
-                                <table class="nomargin">
-                                    <% for (final AccountInformationBean.ActivityRecord record : helpdeskDetailInfoBean.getUserHistory()) { %>
-                                    <tr>
-                                        <td class="key timestamp" style="width:50%">
-                                            <%= JavaHelper.toIsoDate(record.getTimestamp()) %>
-                                        </td>
-                                        <td>
-                                            <%= record.getLabel() %>
-                                        </td>
-                                    </tr>
-                                    <% } %>
-                                </table>
-                            </div>
-                        </div>
-                        <% } %>
-                        <div id="Title_PasswordPolicy" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_PasswordPolicy"/>" class="tabContent">
-                            <div style="max-height: 400px; overflow: auto;">
-                                <table class="nomargin">
-                                    <tr>
-                                        <td class="key">
-                                            <pwm:display key="Field_Policy"/>
-                                        </td>
-                                        <td>
-                                            <%= StringUtil.escapeHtml(helpdeskDetailInfoBean.getPasswordPolicyDN()) %>
-                                        </td>
-                                    </tr>
-                                    <tr>
-                                        <td class="key">
-                                            <pwm:display key="Field_Profile"/>
-                                        </td>
-                                        <td>
-                                            <%= StringUtil.escapeHtml(helpdeskDetailInfoBean.getPasswordPolicyID()) %>
-                                        </td>
-                                    </tr>
-                                    <tr>
-                                        <td class="key">
-                                            <pwm:display key="Field_Display"/>
-                                        </td>
-                                        <td>
-                                            <ul>
-                                                <% for (final String requirementLine : helpdeskDetailInfoBean.getPasswordRequirements()) { %>
-                                                <li><%=requirementLine%>
-                                                </li>
-                                                <% } %>
-                                            </ul>
-                                        </td>
-                                    </tr>
-                                </table>
-                                <table class="nomargin">
-                                    <% for (final String key : helpdeskDetailInfoBean.getPasswordPolicyRules().keySet()) { %>
-                                    <tr>
-                                        <td class="key">
-                                            <%= StringUtil.escapeHtml(key) %>
-                                        </td>
-                                        <td>
-                                            <%= StringUtil.escapeHtml(helpdeskDetailInfoBean.getPasswordPolicyRules().get(key)) %>
-                                        </td>
-                                    </tr>
-                                    <% } %>
-                                </table>
-                            </div>
-                        </div>
-                        <% if (!JavaHelper.isEmpty(helpdeskDetailInfoBean.getHelpdeskResponses())) { %>
-                        <div id="Title_SecurityResponses" data-dojo-type="dijit.layout.ContentPane" title="<pwm:display key="Title_SecurityResponses"/>" class="tabContent">
-                            <table class="nomargin">
-                                <% for (final DisplayElement displayElement : helpdeskDetailInfoBean.getHelpdeskResponses()) { %>
-                                <% request.setAttribute("displayElement", displayElement); %>
-                                <jsp:include page="fragment/displayelement-row.jsp"/>
-                                <% } %>
-                            </table>
-                        </div>
-                        <% } %>
-                    </div>
-                    <br/>
-                    <div class="footnote"><span class="timestamp"><%=JavaHelper.toIsoDate(Instant.now())%></span></div>
-                </td>
-                <td class="noborder" style="width: 200px; max-width:200px; text-align: left; vertical-align: top">
-                    <div class="noborder" style="margin-top: 25px; margin-left: 5px">
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.back)) { %>
-                        <button name="they" class="helpdesk-detail-btn btn" id="button_continue" autofocus>
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
-                            <pwm:display key="Button_GoBack"/>
-                        </button>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.refresh)) { %>
-                        <button name="button_refresh" class="helpdesk-detail-btn btn" id="button_refresh">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-refresh"></span></pwm:if>
-                            <pwm:display key="Display_CaptchaRefresh"/>
-                        </button>
-                        <% } %>
-
-                        <br/><br/>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.changePassword)) { %>
-                        <button class="helpdesk-detail-btn btn" id="helpdesk_ChangePasswordButton">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-key"></span></pwm:if>
-                            <pwm:display key="Button_ChangePassword"/>
-                        </button>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.unlock)) { %>
-                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.unlock)) { %>
-                        <button id="helpdesk_unlockBtn" class="helpdesk-detail-btn btn">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-unlock"></span></pwm:if>
-                            <pwm:display key="Button_Unlock"/>
-                        </button>
-                        <% } else { %>
-                        <button id="helpdesk_unlockBtn" class="helpdesk-detail-btn btn" disabled="disabled">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-unlock"></span></pwm:if>
-                            <pwm:display key="Button_Unlock"/>
-                        </button>
-                        <% } %>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearResponses)) { %>
-                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearResponses)) { %>
-                        <button id="helpdesk_clearResponsesBtn" class="helpdesk-detail-btn btn">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
-                            <pwm:display key="Button_ClearResponses"/>
-                        </button>
-                        <% } else { %>
-                        <button id="helpdesk_clearResponsesBtn" class="helpdesk-detail-btn btn" disabled="disabled">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
-                            <pwm:display key="Button_ClearResponses"/>
-                        </button>
-                        <pwm:script>
-                            <script type="text/javascript">
-                                PWM_GLOBAL['startupFunctions'].push(function(){
-                                    PWM_MAIN.showTooltip({
-                                        id: "helpdesk_clearResponsesBtn",
-                                        text: 'User does not have responses'
-                                    });
-                                });</script>
-                        </pwm:script>
-                        <% } %>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearOtpSecret)) { %>
-                        <% if (helpdeskDetailInfoBean.getEnabledButtons().contains(HelpdeskDetailInfoBean.StandardButton.clearOtpSecret)) { %>
-                        <button id="helpdesk_clearOtpSecretBtn" class="helpdesk-detail-btn btn">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
-                            <pwm:display key="Button_HelpdeskClearOtpSecret"/>
-                        </button>
-                        <% } else { %>
-                        <button id="helpdesk_clearOtpSecretBtn" class="helpdesk-detail-btn btn" disabled="disabled">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-eraser"></span></pwm:if>
-                            <pwm:display key="Button_HelpdeskClearOtpSecret"/>
-                        </button>
-                        <% } %>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.verification)) { %>
-                        <button id="sendTokenButton" class="helpdesk-detail-btn btn">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-mobile-phone"></span></pwm:if>
-                            <pwm:display key="Button_Verify"/>
-                        </button>
-                        <% } %>
-
-                        <% if (helpdeskDetailInfoBean.getVisibleButtons().contains(HelpdeskDetailInfoBean.StandardButton.deleteUser)) { %>
-                        <button class="helpdesk-detail-btn btn" id="helpdesk_deleteUserButton">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-user-times"></span></pwm:if>
-                            <pwm:display key="Button_Delete"/>
-                        </button>
-                        <% } %>
-
-                        <button id="loadDetail" style="display:none">Load Detail</button>
-                        <pwm:script>
-                            <script type="text/javascript">
-                                PWM_GLOBAL['startupFunctions'].push(function(){
-                                    PWM_MAIN.addEventHandler('loadDetail','click',function(){
-                                        var url = 'helpdesk';
-                                        url = PWM_MAIN.addParamToUrl(url, 'processAction', 'detail');
-                                        url = PWM_MAIN.addParamToUrl(url, 'userKey', PWM_VAR['helpdesk_obfuscatedDN']);
-                                        //url = PWM_MAIN.addParamToUrl(url, 'verificationState', PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE));
-                                        PWM_MAIN.ajaxRequest(url,function () {
-                                        });
-                                    });
-                                });
-                            </script>
-                        </pwm:script>
-
-                        <% if (!JavaHelper.isEmpty(helpdeskDetailInfoBean.getCustomButtons())) { %>
-                        <% for (final HelpdeskDetailInfoBean.ButtonInfo customButton : helpdeskDetailInfoBean.getCustomButtons()) { %>
-                        <button class="helpdesk-detail-btn btn" name="action-<%=customButton.getName()%>" id="action-<%=customButton.getName()%>">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-location-arrow"></span></pwm:if>
-                            <%=StringUtil.escapeHtml(customButton.getLabel())%>
-                        </button>
-                        <pwm:script>
-                            <script type="text/javascript">
-                                PWM_GLOBAL['startupFunctions'].push(function(){
-                                    PWM_MAIN.addEventHandler('action-<%=customButton.getName()%>','click',function(){
-                                        PWM_HELPDESK.executeAction('<%=StringUtil.escapeJS(customButton.getName())%>');
-                                    });
-                                    PWM_MAIN.showTooltip({
-                                        id: "action-<%=customButton.getName()%>",
-                                        position: 'above',
-                                        text: '<%=StringUtil.escapeJS(customButton.getDescription())%>'
-                                    });
-                                });
-                            </script>
-                        </pwm:script>
-                        <% } %>
-                        <% } %>
-                    </div>
-                </td>
-            </tr>
-        </table>
-    </div>
-    <div class="push"></div>
-</div>
-<jsp:include page="/WEB-INF/jsp/fragment/footer.jsp"/>
-<pwm:script-ref url="/public/resources/js/helpdesk.js"/>
-<pwm:script-ref url="/public/resources/js/changepassword.js"/>
-</body>
-</html>

+ 14 - 29
server/src/main/webapp/WEB-INF/jsp/helpdesk.jsp

@@ -33,39 +33,24 @@
         <jsp:param name="pwm.PageName" value="Title_Helpdesk"/>
     </jsp:include>
     <div id="centerbody" class="wide tall">
-        <div id="page-content-title"><pwm:display key="Title_Helpdesk" displayIfMissing="true"/></div>
-        <div id="panel-searchbar" class="searchbar">
-            <input id="username" name="username" placeholder="<pwm:display key="Placeholder_Search"/>" class="helpdesk-input-username" <pwm:autofocus/> autocomplete="off"/>
-            <div class="searchbar-extras">
-                <div id="searchIndicator" style="display: none;">
-                    <span style="" class="pwm-icon pwm-icon-lg pwm-icon-spin pwm-icon-spinner"></span>
-                </div>
+        <ui-view id="helpdesk-view"><div class="WaitDialogBlank"></div></ui-view>
 
-                <div id="maxResultsIndicator" style="display: none;">
-                    <span style="color: #ffcd59;" class="pwm-icon pwm-icon-lg pwm-icon-exclamation-circle"></span>
-                </div>
-
-                <% if ((Boolean)JspUtility.getPwmRequest(pageContext).getAttribute(PwmRequestAttribute.HelpdeskVerificationEnabled)) { %>
-                <div id="verifications-btn">
-                    <button class="btn" id="button-show-current-verifications">
-                        <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-check"></span></pwm:if>
-                        <pwm:display key="Button_Verificiations"/>
-                    </button>
-                </div>
-                <% } %>
-            </div>
-
-            <noscript>
-                <span><pwm:display key="Display_JavascriptRequired"/></span>
-                <a href="<pwm:context/>"><pwm:display key="Title_MainPage"/></a>
-            </noscript>
-        </div>
-        <div id="helpdesk-searchResultsGrid" class="searchResultsGrid grid tall">
-        </div>
+        <noscript>
+            <span><pwm:display key="Display_JavascriptRequired"/></span>
+            <a href="<pwm:context/>"><pwm:display key="Title_MainPage"/></a>
+        </noscript>
     </div>
     <div class="push"></div>
 </div>
+
+<pwm:script-ref url="/public/resources/webjars/angular/angular.min.js" />
+<pwm:script-ref url="/public/resources/webjars/angular-ui-router/release/angular-ui-router.min.js" />
+<pwm:script-ref url="/public/resources/webjars/angular-translate/dist/angular-translate.min.js" />
+
 <jsp:include page="/WEB-INF/jsp/fragment/footer.jsp"/>
-<pwm:script-ref url="/public/resources/js/helpdesk.js"/>
+<link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/fonts.css' addContext="true"/>"/>
+<pwm:script-ref url="/public/resources/webjars/pwm-client/helpdesk.ng.js" />
+<pwm:script-ref url="/public/resources/js/changepassword.js"/>
+
 </body>
 </html>

+ 0 - 1
server/src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

@@ -43,7 +43,6 @@
 <pwm:script-ref url="/public/resources/webjars/angular-translate/dist/angular-translate.min.js" />
 
 <%@ include file="fragment/footer.jsp" %>
-<pwm:script-ref url="/public/resourcess/js/peoplesearch.js" />
 <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/fonts.css' addContext="true"/>"/>
 <pwm:script-ref url="/public/resources/webjars/pwm-client/peoplesearch.ng.js" />