Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into support/use-searchbox-on-heroku

Yuki Takei 6 лет назад
Родитель
Сommit
de549835f6

+ 1 - 1
resource/locales/en-US/translation.json

@@ -705,7 +705,7 @@
     "send_new_password": "Please send the new password to the user.",
     "send_new_password": "Please send the new password to the user.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",
-    "related_username": "Related user's <code>%s</code>",
+    "related_username": "Related user's ",
     "accept": "Accept",
     "accept": "Accept",
     "deactivate_account":"Deactivate Account",
     "deactivate_account":"Deactivate Account",
     "your_own":"You cannot deactivate your own account",
     "your_own":"You cannot deactivate your own account",

+ 1 - 1
resource/locales/ja/translation.json

@@ -689,7 +689,7 @@
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "reset_password": "パスワードの再発行",
     "reset_password": "パスワードの再発行",
-    "related_username": "関連付けられているユーザーの <code>%s</code>",
+    "related_username": "関連付けられているユーザーの ",
     "accept": "承認する",
     "accept": "承認する",
     "deactivate_account": "アカウント停止",
     "deactivate_account": "アカウント停止",
     "your_own": "自分自身のアカウントを停止することはできません",
     "your_own": "自分自身のアカウントを停止することはできません",

+ 15 - 2
src/client/js/app.jsx

@@ -40,7 +40,7 @@ import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import UserManagement from './components/Admin/UserManagement';
 import UserManagement from './components/Admin/UserManagement';
-import ManageExternalAccount from './components/Admin/Users/ManageExternalAccount';
+import ManageExternalAccount from './components/Admin/ManageExternalAccount';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import Customize from './components/Admin/Customize/Customize';
 import Customize from './components/Admin/Customize/Customize';
 import ImportDataPage from './components/Admin/ImportDataPage';
 import ImportDataPage from './components/Admin/ImportDataPage';
@@ -56,6 +56,7 @@ import UserGroupDetailContainer from './services/UserGroupDetailContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import MarkDownSettingContainer from './services/MarkDownSettingContainer';
 import MarkDownSettingContainer from './services/MarkDownSettingContainer';
+import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
 
 
 const logger = loggerFactory('growi:app');
 const logger = loggerFactory('growi:app');
 
 
@@ -110,7 +111,6 @@ let componentMappings = {
 
 
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-customize': <Customize />,
   'admin-customize': <Customize />,
-  'admin-external-account-setting': <ManageExternalAccount />,
 
 
   'staff-credit': <StaffCredit />,
   'staff-credit': <StaffCredit />,
   'admin-importer': <ImportDataPage />,
   'admin-importer': <ImportDataPage />,
@@ -171,6 +171,19 @@ if (adminUsersElem != null) {
   );
   );
 }
 }
 
 
+const adminExternalAccountsElem = document.getElementById('admin-external-account-setting');
+if (adminExternalAccountsElem != null) {
+  const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
+  ReactDOM.render(
+    <Provider inject={[injectableContainers, adminExternalAccountsContainer]}>
+      <I18nextProvider i18n={i18n}>
+        <ManageExternalAccount />
+      </I18nextProvider>
+    </Provider>,
+    adminExternalAccountsElem,
+  );
+}
+
 const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
 const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
 if (adminUserGroupDetailElem != null) {
 if (adminUserGroupDetailElem != null) {
   const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);
   const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);

+ 80 - 0
src/client/js/components/Admin/ManageExternalAccount.jsx

@@ -0,0 +1,80 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import PaginationWrapper from '../PaginationWrapper';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import AdminExternalAccountsContainer from '../../services/AdminExternalAccountsContainer';
+import ExternalAccountTable from './Users/ExternalAccountTable';
+import { toastError } from '../../util/apiNotification';
+
+
+class ManageExternalAccount extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.xss = window.xss;
+    this.handleExternalAccountPage = this.handleExternalAccountPage.bind(this);
+  }
+
+  componentWillMount() {
+    this.handleExternalAccountPage(1);
+  }
+
+  async handleExternalAccountPage(selectedPage) {
+    try {
+      await this.props.adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(selectedPage);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminExternalAccountsContainer } = this.props;
+
+    const pager = (
+      <div className="pull-right">
+        <PaginationWrapper
+          activePage={adminExternalAccountsContainer.state.activePage}
+          changePage={this.handleExternalAccountPage}
+          totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
+          pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
+        />
+      </div>
+    );
+    return (
+      <Fragment>
+        <p>
+          <a className="btn btn-default" href="/admin/users">
+            <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+            { t('user_management.back_to_user_management') }
+          </a>
+        </p>
+
+        <h2>{ t('user_management.external_account_list') }</h2>
+
+        { pager }
+        <ExternalAccountTable />
+        { pager }
+
+      </Fragment>
+    );
+  }
+
+}
+
+ManageExternalAccount.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
+};
+
+const ManageExternalAccountWrapper = (props) => {
+  return createSubscribedElement(ManageExternalAccount, props, [AppContainer, AdminExternalAccountsContainer]);
+};
+
+
+export default withTranslation()(ManageExternalAccountWrapper);

+ 132 - 0
src/client/js/components/Admin/Users/ExternalAccountTable.jsx

@@ -0,0 +1,132 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import dateFnsFormat from 'date-fns/format';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import AdminExternalAccountsContainer from '../../../services/AdminExternalAccountsContainer';
+
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class ExternalAccountTable extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+    this.removeExtenalAccount = this.removeExtenalAccount.bind(this);
+  }
+
+  // remove external-account
+  async removeExtenalAccount(externalAccountId) {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminExternalAccountsContainer.removeExternal(externalAccountId);
+      toastSuccess(t('user_management.remove_user_success', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+
+  render() {
+    const { t, adminExternalAccountsContainer } = this.props;
+    return (
+      <Fragment>
+        <table className="table table-bordered table-user-list">
+          <thead>
+            <tr>
+              <th width="120px">{ t('user_management.authentication_provider') }</th>
+              <th><code>accountId</code></th>
+              <th>{ t('user_management.related_username') }<code>username</code></th>
+              <th>
+                { t('user_management.password_setting') }
+                <div
+                  className="text-muted"
+                  data-toggle="popover"
+                  data-placement="top"
+                  data-trigger="hover focus"
+                  tabIndex="0"
+                  role="button"
+                  data-animation="false"
+                  data-html="true"
+                  data-content={t('user_management.password_setting_help')}
+                >
+                  <small>
+                    <i className="icon-question" aria-hidden="true"></i>
+                  </small>
+                </div>
+              </th>
+              <th width="100px">{ t('Created') }</th>
+              <th width="70px"></th>
+            </tr>
+          </thead>
+          <tbody>
+            {adminExternalAccountsContainer.state.externalAccounts.map((ea) => {
+              return (
+                <tr key={ea._id}>
+                  <td>{ea.providerType}</td>
+                  <td>
+                    <strong>{ea.accountId}</strong>
+                  </td>
+                  <td>
+                    <strong>{ ea.user.username }</strong>
+                  </td>
+                  <td>
+                    { ea.user.password
+                      ? (
+                        <span className="label label-info">
+                          { t('user_management.set') }
+                        </span>
+                      )
+                      : (
+                        <span className="label label-warning">
+                          { t('user_management.unset') }
+                        </span>
+                      )
+                    }
+                  </td>
+                  <td>{dateFnsFormat(new Date(ea.createdAt), 'yyyy-MM-dd')}</td>
+                  <td>
+                    <div className="btn-group admin-user-menu">
+                      <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                        <i className="icon-settings"></i> <span className="caret"></span>
+                      </button>
+                      <ul className="dropdown-menu" role="menu">
+                        <li className="dropdown-header">{ t('user_management.edit_menu') }</li>
+                        <li>
+                          <a onClick={() => { return this.removeExtenalAccount(ea._id) }}>
+                            <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
+                          </a>
+                        </li>
+                      </ul>
+                    </div>
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </Fragment>
+    );
+  }
+
+}
+
+ExternalAccountTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
+};
+
+const ExternalAccountTableWrapper = (props) => {
+  return createSubscribedElement(ExternalAccountTable, props, [AppContainer, AdminExternalAccountsContainer]);
+};
+
+
+export default withTranslation()(ExternalAccountTableWrapper);

+ 0 - 78
src/client/js/components/Admin/Users/ManageExternalAccount.jsx

@@ -1,78 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-
-class ManageExternalAccount extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-    };
-  }
-
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <p>
-          <a className="btn btn-default" href="/admin/users">
-            <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-            { t('user_management.back_to_user_management') }
-          </a>
-        </p>
-
-        <h2>{ t('user_management.external_account_list') }</h2>
-
-        <table className="table table-bordered table-user-list">
-          <thead>
-            <tr>
-              <th width="120px">{ t('user_management.authentication_provider') }</th>
-              <th><code>accountId</code></th>
-              <th>{ t('user_management.related_username', 'username') }</th>
-              <th>
-                { t('user_management.password_setting') }
-                <div
-                  className="text-muted"
-                  data-toggle="popover"
-                  data-placement="top"
-                  data-trigger="hover focus"
-                  tabIndex="0"
-                  role="button"
-                  data-animation="false"
-                  data-html="true"
-                  data-content="<small>{{ t('user_management.password_setting_help') }}</small>"
-                >
-                  <small>
-                    <i className="icon-question" aria-hidden="true"></i>
-                  </small>
-                </div>
-              </th>
-              <th width="100px">{ t('Created') }</th>
-              <th width="70px"></th>
-            </tr>
-          </thead>
-          {/* TODO GW-328 */}
-        </table>
-      </Fragment>
-    );
-  }
-
-}
-
-const ManageExternalAccountWrapper = (props) => {
-  return createSubscribedElement(ManageExternalAccount, props, [AppContainer]);
-};
-
-ManageExternalAccount.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(ManageExternalAccountWrapper);

+ 72 - 0
src/client/js/services/AdminExternalAccountsContainer.js

@@ -0,0 +1,72 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
+
+/**
+ * Service container for admin external-accounts page (ManageExternalAccountsContainer.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminExternalAccountsContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      externalAccounts: [],
+      totalAccounts: 0,
+      activePage: 1,
+      pagingLimit: Infinity,
+    };
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminExternalAccountsContainer';
+  }
+
+
+  /**
+   * syncExternalAccounts of selectedPage
+   * @memberOf AdminExternalAccountsContainer
+   * @param {number} selectedPage
+   */
+  async retrieveExternalAccountsByPagingNum(selectedPage) {
+
+    const params = { page: selectedPage };
+    const { data } = await this.appContainer.apiv3.get('/users/external-accounts', params);
+
+    if (data.paginateResult == null) {
+      throw new Error('data must conclude \'paginateResult\' property.');
+    }
+    const { docs: externalAccounts, totalDocs: totalAccounts, limit: pagingLimit } = data.paginateResult;
+    this.setState({
+      externalAccounts,
+      totalAccounts,
+      pagingLimit,
+      activePage: selectedPage,
+    });
+
+  }
+
+  /**
+   * remove external account
+   *
+   * @memberOf AdminExternalAccountsContainer
+   * @param {string} externalAccountId id of the External Account to be removed
+   */
+  async removeExternal(externalAccountId) {
+    const res = await this.appContainer.apiv3.delete(`/users/external-accounts/${externalAccountId}/remove`);
+    const externalAccountData = res.data.externalAccount.user;
+    return externalAccountData;
+  }
+
+}

+ 1 - 4
src/server/models/external-account.js

@@ -169,10 +169,7 @@ class ExternalAccount {
       options.limit = ExternalAccount.DEFAULT_LIMIT;
       options.limit = ExternalAccount.DEFAULT_LIMIT;
     }
     }
 
 
-    return this.paginate(query, options)
-      .catch((err) => {
-        debug('Error on pagination:', err);
-      });
+    return this.paginate(query, options);
   }
   }
 
 
 }
 }

+ 1 - 11
src/server/routes/admin.js

@@ -486,17 +486,7 @@ module.exports = function(crowi, app) {
 
 
   actions.externalAccount = {};
   actions.externalAccount = {};
   actions.externalAccount.index = function(req, res) {
   actions.externalAccount.index = function(req, res) {
-    const page = parseInt(req.query.page) || 1;
-
-    ExternalAccount.findAllWithPagination({ page })
-      .then((result) => {
-        const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
-
-        return res.render('admin/external-accounts', {
-          accounts: result.docs,
-          pager,
-        });
-      });
+    return res.render('admin/external-accounts');
   };
   };
 
 
   actions.externalAccount.remove = async function(req, res) {
   actions.externalAccount.remove = async function(req, res) {

+ 73 - 0
src/server/routes/apiv3/users.js

@@ -334,5 +334,78 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users:
+   *      get:
+   *        tags: [Users]
+   *        description: Get external-account
+   *        responses:
+   *          200:
+   *            description: external-account are fetched
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    paginateResult:
+   *                      $ref: '#components/schemas/PaginateResult'
+   */
+  router.get('/external-accounts/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const page = parseInt(req.query.page) || 1;
+    try {
+      const paginateResult = await ExternalAccount.findAllWithPagination({ page });
+      return res.apiv3({ paginateResult });
+    }
+    catch (err) {
+      const msg = 'Error occurred in fetching external-account list  ';
+      logger.error(msg, err);
+      return res.apiv3Err(new ErrorV3(msg + err.message, 'external-account-list-fetch-failed'), 500);
+    }
+  });
+
+
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users/external-accounts/{id}/remove:
+   *      delete:
+   *        tags: [Users]
+   *        description: Delete ExternalAccount
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of ExternalAccount
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description:  External Account is removed
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    externalAccount:
+   *                      type: object
+   *                      description: A result of `ExtenralAccount.findByIdAndRemove`
+   */
+
+  router.delete('/external-accounts/:id/remove', loginRequiredStrictly, adminRequired, ApiV3FormValidator, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const externalAccount = await ExternalAccount.findByIdAndRemove(id);
+
+      return res.apiv3({ externalAccount });
+    }
+    catch (err) {
+      const msg = 'Error occurred in deleting a external account  ';
+      logger.error(msg, err);
+      return res.apiv3Err(new ErrorV3(msg + err.message, 'extenral-account-delete-failed'));
+    }
+  });
   return router;
   return router;
 };
 };

+ 1 - 86
src/server/views/admin/external-accounts.html

@@ -38,92 +38,7 @@
       {% include './widget/menu.html' with {current: 'external-account'} %}
       {% include './widget/menu.html' with {current: 'external-account'} %}
     </div>
     </div>
 
 
-    <!-- TODO reactify admin -->
-    <div class="col-md-9">
-      <p>
-        <a class="btn btn-default" href="/admin/users">
-          <i class="icon-fw ti-arrow-left" aria-hidden="true"></i>
-          {{ t('user_management.back_to_user_management') }}
-        </a>
-      </p>
-
-      <h2>{{ t('user_management.external_account_list') }}</h2>
-
-      <table class="table table-bordered table-user-list">
-        <thead>
-          <tr>
-            <th width="120px">{{ t('user_management.authentication_provider') }}</th>
-            <th><code>accountId</code></th>
-            <th>{{ t('user_management.related_username', 'username') }}</th>
-
-            <th>
-              {{ t('user_management.password_setting') }}
-              <a class="text-muted"
-                  data-toggle="popover" data-placement="top"
-                  data-trigger="hover focus" tabindex="0" role="button" {# dismiss settings #}
-                  data-animation="false" data-html="true"
-                  data-content="<small>{{ t('user_management.password_setting_help') }}</small>">
-                <small>
-                  <i class="icon-question" aria-hidden="true"></i>
-                </small>
-              </a>
-            </th>
-            <th width="100px">{{ t('Created') }}</th>
-            <th width="70px"></th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for account in accounts %}
-          <tr>
-            <td>{{ account.providerType }}</td>
-            <td>
-              <strong>{{ account.accountId }}</strong>
-            </td>
-            <td>
-              <strong>{{ account.user.username }}</strong>
-            </td>
-            <td>
-              {% if account.user.password != null %}
-              <span class="label label-info">
-                {{ t('user_management.set') }}
-              </span>
-              {% else %}
-              <span class="label label-warning">
-                {{ t('user_management.unset') }}
-              </span>
-              {% endif %}
-            </td>
-            <td>{{ account.createdAt|datetz('Y-m-d') }}</td>
-            <td>
-              <div class="btn-group admin-user-menu">
-
-                <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                  <i class="icon-settings"></i> <span class="caret"></span>
-                </button>
-                <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">{{ t('user_management.edit_menu') }}</li>
-                  <form id="form_remove_{{ loop.index }}" action="/admin/users/external-accounts/{{ account._id.toString() }}/remove" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_remove_{{ loop.index }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i>
-                      {{ t('Delete') }}
-                    </a>
-                  </li>
-                </ul>{# end of .dropdown-menu #}
-
-
-
-              </div>{# end of .btn-group #}
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-
-      {% include '../widget/pager.html' with {path: "/admin/users/external-accounts", pager: pager} %}
-
+    <div class="col-md-9" id="admin-external-account-setting">
     </div>
     </div>
   </div>
   </div>
 </div>
 </div>

+ 1 - 1
src/server/views/admin/widget/passport/ldap.html

@@ -81,7 +81,7 @@
       <div class="form-group">
       <div class="form-group">
         <label for="settingForm[security:passport-ldap:bindDNPassword]" class="col-xs-3 control-label">{{ t("security_setting.ldap.bind_DN_password") }}</label>
         <label for="settingForm[security:passport-ldap:bindDNPassword]" class="col-xs-3 control-label">{{ t("security_setting.ldap.bind_DN_password") }}</label>
         <div class="col-xs-6">
         <div class="col-xs-6">
-          <input class="form-control passport-ldap-managerbind" type="text" {% if isUserBind %}style="display: none;"{% endif %}
+          <input class="form-control passport-ldap-managerbind" type="password" {% if isUserBind %}style="display: none;"{% endif %}
               name="settingForm[security:passport-ldap:bindDNPassword]" value="{{ getConfig('crowi', 'security:passport-ldap:bindDNPassword') | default('') }}">
               name="settingForm[security:passport-ldap:bindDNPassword]" value="{{ getConfig('crowi', 'security:passport-ldap:bindDNPassword') | default('') }}">
           <p class="help-block passport-ldap-managerbind">
           <p class="help-block passport-ldap-managerbind">
             <small>
             <small>