diff --git a/docs/en_US/images/change_user_password.png b/docs/en_US/images/change_user_password.png index 15a9a3b2c..7dc51ff66 100644 Binary files a/docs/en_US/images/change_user_password.png and b/docs/en_US/images/change_user_password.png differ diff --git a/docs/en_US/images/login.png b/docs/en_US/images/login.png index 982322fc9..41ef542d5 100644 Binary files a/docs/en_US/images/login.png and b/docs/en_US/images/login.png differ diff --git a/docs/en_US/images/login_recover.png b/docs/en_US/images/login_recover.png index f0a303f69..ce2200d3a 100644 Binary files a/docs/en_US/images/login_recover.png and b/docs/en_US/images/login_recover.png differ diff --git a/docs/en_US/images/mfa_registration.png b/docs/en_US/images/mfa_registration.png index 680dce3ca..abb5062da 100644 Binary files a/docs/en_US/images/mfa_registration.png and b/docs/en_US/images/mfa_registration.png differ diff --git a/docs/en_US/images/oauth2_login.png b/docs/en_US/images/oauth2_login.png index 512814dcc..3fac4eda7 100644 Binary files a/docs/en_US/images/oauth2_login.png and b/docs/en_US/images/oauth2_login.png differ diff --git a/docs/en_US/kerberos.rst b/docs/en_US/kerberos.rst index 7f333ed1a..7f1befdff 100644 --- a/docs/en_US/kerberos.rst +++ b/docs/en_US/kerberos.rst @@ -32,7 +32,7 @@ from *config.py* file and modify the values for the following parameters. * [‘kerberos’, ‘internal’]: pgAdmin will first try to authenticate the user through kerberos. If that authentication fails, then it will return back - to the login dialog where you need to provide internal pgAdmin user + to the login page where you need to provide internal pgAdmin user credentials for authentication." "KERBEROS_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically create a pgAdmin user corresponding to a successfully diff --git a/docs/en_US/login.rst b/docs/en_US/login.rst index 356796ea4..798b3de4e 100644 --- a/docs/en_US/login.rst +++ b/docs/en_US/login.rst @@ -1,16 +1,16 @@ .. _login: ********************* -`Login Dialog`:index: +`Login Page`:index: ********************* -Use the *Login* dialog to log in to pgAdmin: +Use the *Login* page to log in to pgAdmin: .. image:: images/login.png - :alt: pgAdmin login dialog + :alt: pgAdmin login page :align: center -Use the fields in the *Login* dialog to authenticate your connection. There are +Use the fields in the *Login* page to authenticate your connection. There are two ways to authenticate your connection: - From pgAdmin version 4.21 onwards, support for LDAP authentication @@ -52,7 +52,7 @@ to launch a password recovery utility. If you have forgotten the email associated with your account, please contact your administrator. -Please note that your LDAP password cannot be recovered using this dialog. If +Please note that your LDAP password cannot be recovered using this page. If you enter your LDAP username in the *Email Address/Username* field, and then enter your email to recover your password, an error message will be displayed asking you to contact the LDAP administrator to recover your LDAP password. diff --git a/docs/en_US/webserver.rst b/docs/en_US/webserver.rst index 2de1d6630..f753c710c 100644 --- a/docs/en_US/webserver.rst +++ b/docs/en_US/webserver.rst @@ -27,7 +27,7 @@ and modify the values for the following parameters: * [‘webserver’, ‘internal’]: pgAdmin will first try to authenticate the user through webserver. If that authentication fails, then it will return back - to the login dialog where you need to provide internal pgAdmin user + to the login page where you need to provide internal pgAdmin user credentials for authentication." "WEBSERVER_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically create a pgAdmin user corresponding to a successfully authenticated Webserver user. diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py index 5e891a99c..cc7c6e8d2 100644 --- a/web/pgadmin/authenticate/__init__.py +++ b/web/pgadmin/authenticate/__init__.py @@ -27,7 +27,8 @@ from flask_socketio import disconnect, ConnectionRefusedError from pgadmin.model import db, User from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect -from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP +from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP,\ + MessageType from pgadmin.authenticate.registry import AuthSourceRegistry MODULE_NAME = 'authenticate' @@ -132,7 +133,7 @@ def _login(): if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0: flash(gettext('Your account is locked. Please contact the ' 'Administrator.'), - 'warning') + MessageType.WARNING) logout_user() return redirect(get_post_logout_redirect()) @@ -158,7 +159,7 @@ def _login(): if flash_login_attempt_error: error = error + flash_login_attempt_error flash_login_attempt_error = None - flash(error, 'warning') + flash(error, MessageType.WARNING) return redirect(get_post_logout_redirect()) @@ -175,7 +176,7 @@ def _login(): return redirect('{0}?next={1}'.format(url_for( 'authenticate.kerberos_login'), url_for('browser.index'))) - flash(msg, 'danger') + flash(msg, MessageType.ERROR) return redirect(get_post_logout_redirect()) session['auth_source_manager'] = current_auth_obj @@ -194,7 +195,7 @@ def _login(): return msg if 'auth_obj' in session: session.pop('auth_obj') - flash(msg, 'danger') + flash(msg, MessageType.ERROR) form_class = _security.forms.get('login_form').cls form = form_class() @@ -268,7 +269,7 @@ class AuthSourceManager: if status: return True if err_msg: - flash(err_msg, 'warning') + flash(err_msg, MessageType.WARNING) return False def authenticate(self): diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py index 479acf021..ab052b471 100644 --- a/web/pgadmin/authenticate/kerberos.py +++ b/web/pgadmin/authenticate/kerberos.py @@ -23,7 +23,7 @@ from flask_security import login_required import config from pgadmin.model import User from pgadmin.tools.user_management import create_user -from pgadmin.utils.constants import KERBEROS +from pgadmin.utils.constants import KERBEROS, MessageType from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response, internal_server_error @@ -199,7 +199,7 @@ class KerberosAuthentication(BaseAuthentication): retval = self.__auto_create_user( str(negotiate.initiator_name)) elif isinstance(negotiate, Exception): - flash(gettext(negotiate), 'danger') + flash(gettext(negotiate), MessageType.ERROR) retval = [status, Response(render_template( "security/login_user.html", @@ -209,8 +209,8 @@ class KerberosAuthentication(BaseAuthentication): str(base64.b64encode(negotiate), 'utf-8')) return False, Response("Success", 200, headers) else: - flash(gettext("Kerberos authentication failed." - " Couldn't find kerberos ticket."), 'danger') + flash(gettext("Kerberos authentication failed. Couldn't find " + "kerberos ticket."), MessageType.ERROR) headers.add('WWW-Authenticate', 'Negotiate') retval = [False, Response(render_template( diff --git a/web/pgadmin/authenticate/mfa/authenticator.py b/web/pgadmin/authenticate/mfa/authenticator.py index 9f377c26d..dcc2c40d0 100644 --- a/web/pgadmin/authenticate/mfa/authenticator.py +++ b/web/pgadmin/authenticate/mfa/authenticator.py @@ -24,6 +24,7 @@ from pgadmin.model import UserMFA from .registry import BaseMFAuth from .utils import ValidationException, fetch_auth_option, mfa_add +from pgadmin.utils.constants import MessageType _TOTP_AUTH_METHOD = "authenticator" @@ -119,14 +120,7 @@ class TOTPAuthenticator(BaseMFAuth): Returns: str: Authentication view as a string """ - return ( - "
{auth_description}
" - "
" - " " - "
" - ).format( + return dict( auth_description=_( "Enter the code shown in your authenticator application for " "TOTP (Time-based One-Time Password)" @@ -162,6 +156,17 @@ class TOTPAuthenticator(BaseMFAuth): img.save(buffered, format="JPEG") img_base64 = base64.b64encode(buffered.getvalue()) + return dict( + auth_title=_(_TOTP_AUTHENTICATOR), + auth_method=_TOTP_AUTH_METHOD, + image=img_base64.decode("utf-8"), + qrcode_alt_text=_("TOTP Authenticator QRCode"), + auth_description=_( + "Scan the QR code and the enter the code from the " + "TOTP Authenticator application" + ), otp_placeholder=_("Enter code") + ) + return "".join([ "
{auth_title}
", "", @@ -210,13 +215,13 @@ class TOTPAuthenticator(BaseMFAuth): authenticator_opt = session.get('mfa_authenticator_opt', None) if authenticator_opt is None or \ pyotp.TOTP(authenticator_opt).verify(code) is False: - flash(_("Failed to validate the code"), "danger") + flash(_("Failed to validate the code"), MessageType.ERROR) return self._registration_view() mfa_add(_TOTP_AUTH_METHOD, authenticator_opt) flash(_( "TOTP Authenticator registered successfully for authentication." - ), "success") + ), MessageType.SUCCESS) session.pop('mfa_authenticator_opt', None) return None diff --git a/web/pgadmin/authenticate/mfa/email.py b/web/pgadmin/authenticate/mfa/email.py index 52b0f58b8..e106c7f48 100644 --- a/web/pgadmin/authenticate/mfa/email.py +++ b/web/pgadmin/authenticate/mfa/email.py @@ -18,6 +18,7 @@ import config from pgadmin.utils.csrf import pgCSRFProtect from .registry import BaseMFAuth from .utils import ValidationException, mfa_add, fetch_auth_option +from pgadmin.utils.constants import MessageType def __generate_otp() -> str: @@ -154,10 +155,7 @@ def send_email_code() -> Response: if success is False: return Response(message, http_code, mimetype='text/html') - return Response(render_template( - "mfa/email_code_sent.html", _=_, - message=message, - ), http_code, mimetype='text/html') + return dict(message=message) @pgCSRFProtect.exempt @@ -204,28 +202,15 @@ class EmailAuthentication(BaseMFAuth): def validation_view(self): session.pop("mfa_email_code", None) - return render_template( - "mfa/email_view.html", _=_ + return dict( + description=_("Verify with Email Authentication"), + button_label=_("Send Code"), + button_label_sending=_("Sending Code...") ) def _registration_view(self): email = getattr(current_user, 'email', '') - return "\n".join([ - "
{label}
", - "", - "", - "
{description}
", - "
", - " ", - "
", - "", - ]).format( + return dict( label=email_authentication_label(), auth_method=EMAIL_AUTH_METHOD, description=_("Enter the email address to send a code"), @@ -247,20 +232,10 @@ class EmailAuthentication(BaseMFAuth): ) if success is False: - flash(message, 'danger') + flash(message, MessageType.ERROR) return None - return "\n".join([ - "
{label}
", - "", - "", - "
{message}
", - "
", - " ", - "
", - ]).format( + return dict( label=email_authentication_label(), auth_method=EMAIL_AUTH_METHOD, message=message, @@ -282,13 +257,13 @@ class EmailAuthentication(BaseMFAuth): flash(_( "Email Authentication registered successfully." - ), "success") + ), MessageType.SUCCESS) session.pop('mfa_email_code', None) return None - flash(_('Invalid code'), 'danger') + flash(_('Invalid code'), MessageType.ERROR) return self._registration_view() diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email.js b/web/pgadmin/authenticate/mfa/templates/mfa/email.js deleted file mode 100644 index 3a2c255ca..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email.js +++ /dev/null @@ -1,66 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2023, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -let mfa_form_elem = document.getElementById('mfa_form'); - -if (mfa_form_elem) - mfa_form_elem.setAttribute('class', ''); - -function sendCodeToEmail(data, _json, _callback) { - const URL = '{{ url_for('mfa.send_email_code') }}'; - let accept = 'text/html; charset=utf-8;'; - - let btn_send_code_elem = document.getElementById('btn_send_code'); - if (btn_send_code_elem) btn_send_code_elem.disabled = true; - - if (!data) { - data = {'code': ''}; - } - - if (_json) { - accept = 'application/json; charset=utf-8;'; - } - - clear_error(); - - fetch(URL, { - method: 'POST', - mode: 'cors', - cache: 'no-cache', - headers: { - 'Accept': accept, - 'Content-Type': 'application/json; charset=utf-8;', - '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}': '{{ csrf_token() }}' - }, - redirect: 'follow', - body: JSON.stringify(data) - }).then((resp) => { - if (_callback) { - setTimeout(() => (_callback(resp)), 1); - return null; - } - if (!resp.ok) { - let btn_send_code_elem = document.getElementById('btn_send_code'); - if (btn_send_code_elem) btn_send_code_elem.disabled = true; - resp.text().then(msg => render_error(msg)); - - return; - } - if (_json) return resp.json(); - return resp.text(); - }).then((string) => { - if (!string) - return; - document.getElementById("mfa_email_auth").innerHTML = string; - document.getElementById("mfa_form").classList = ["show_validate_btn"]; - setTimeout(() => { - document.getElementById("showme").classList = []; - }, 20000); - }); -} diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html b/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html deleted file mode 100644 index 0325ac9e5..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email_code_sent.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
{{ message }}
- -
- -
-
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html b/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html deleted file mode 100644 index 3ecf635fb..000000000 --- a/web/pgadmin/authenticate/mfa/templates/mfa/email_view.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
{{ _("Verify with Email Authentication") }}
-
- -
-
diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/register.html b/web/pgadmin/authenticate/mfa/templates/mfa/register.html index 7b5173e97..7be69472e 100644 --- a/web/pgadmin/authenticate/mfa/templates/mfa/register.html +++ b/web/pgadmin/authenticate/mfa/templates/mfa/register.html @@ -1,78 +1,10 @@ -{% set auth_page = true %} -{% extends "security/panel.html" %} -{% block panel_image %} -
- {{ _('Registration') }} -
-{% endblock %} -{% block panel_title %}{{ _('Authentication registration') }}{% endblock %} -{% block panel_body %} - - -
-
-{% if mfa_view is not defined or mfa_view is none %} -
- {% for mfa in mfa_list %} -
- -
- {% endfor %} -
- {% if next_url != 'internal' %} -
- -
- {% endif %} -{% else %} -
- {{ mfa_view | safe }} -
-
- - -
-{% endif %} - -
-
-{% else %} -
-
-
-
- -
- {{ error_message }}
-
-
-
-
-{% endif %} -{% endblock %} +{% set page_name = 'mfa_register' %} +{% set page_props = { + 'actionUrl': url_for('mfa.register'), + 'mfaList': mfa_list, + 'nextUrl': next_url, + 'mfaView': mfa_view, + 'errorMessage': error_message, +} %} +{% extends "security/render_page.html" %} +{% block title %}{{ _('Authentication Registration') }}{% endblock %} diff --git a/web/pgadmin/authenticate/mfa/templates/mfa/validate.html b/web/pgadmin/authenticate/mfa/templates/mfa/validate.html index acb07b004..49361104f 100644 --- a/web/pgadmin/authenticate/mfa/templates/mfa/validate.html +++ b/web/pgadmin/authenticate/mfa/templates/mfa/validate.html @@ -1,121 +1,11 @@ -{% extends "security/panel.html" %} -{% block panel_image %} -
- {{ _('Authentication') }} -
-{% endblock %} -{% block panel_title %}{{ _('Authentication') }}{% endblock %} -{% block panel_body %} - - -
-
-
-
-
- -
- -
-
-
-
- -
- - -{% endblock %} +{% set page_name = 'mfa_validate' %} +{% set page_props = { + 'actionUrl': url_for('mfa.validate'), + 'views': views, + 'logoutUrl': logout_url, + 'sendEmailUrl': url_for("mfa.send_email_code"), + 'csrfHeader': current_app.config.get("WTF_CSRF_HEADERS")[0], + 'csrfToken': csrf_token() +} %} +{% extends "security/render_page.html" %} +{% block title %}{{ _('Authentication') }}{% endblock %} diff --git a/web/pgadmin/authenticate/mfa/views.py b/web/pgadmin/authenticate/mfa/views.py index c64c31013..db2a174e3 100644 --- a/web/pgadmin/authenticate/mfa/views.py +++ b/web/pgadmin/authenticate/mfa/views.py @@ -22,6 +22,7 @@ from pgadmin.utils.ajax import bad_request from .utils import user_supported_mfa_methods, mfa_user_registered, \ mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \ is_mfa_session_authenticated +from pgadmin.utils.constants import MessageType _INDEX_URL = "browser.index" @@ -118,11 +119,11 @@ def validate_view() -> Response: "MFA validation failed for the user '{}' with an error: " "{}" ).format(current_user.username, str(ve))) - flash(str(ve), "danger") + flash(str(ve), MessageType.ERROR) return_code = 401 except Exception as ex: current_app.logger.exception(ex) - flash(str(ex), "danger") + flash(str(ex), MessageType.ERROR) return_code = 500 mfa_views = { @@ -166,7 +167,8 @@ def _mfa_registration_view( if form_data[mfa.name] == 'SETUP': if supported_mfa['registered'] is True: - flash(_("'{}' is already registerd'").format(mfa.label), "success") + flash(_("'{}' is already registerd'").format(mfa.label), + MessageType.SUCCESS) return None return mfa.registration_view(form_data) @@ -174,13 +176,13 @@ def _mfa_registration_view( if mfa_delete(mfa.name) is True: flash(_( "'{}' unregistered from the authentication list." - ).format(mfa.label), "success") + ).format(mfa.label), MessageType.SUCCESS) return None flash(_( "'{}' is not found in the authentication list." - ).format(mfa.label), "warning") + ).format(mfa.label), MessageType.WARNING) return None @@ -255,7 +257,7 @@ def __handle_registration_view_for_post_method( if view is False: if next_url != 'internal': return None, redirect(next_url), None - flash(_("Please close the dialog."), "info") + flash(_("Please close the dialog."), MessageType.INFO) if view is not None: return None, Response( @@ -336,7 +338,8 @@ def registration_view() -> Response: ) elif is_mfa_session_authenticated() is False and \ found_one_mfa is True: - flash(_("Complete the authentication process first"), "danger") + flash(_("Complete the authentication process first"), + MessageType.ERROR) return redirect(login_url("mfa.validate", next_url=next_url)) return Response(render_template( diff --git a/web/pgadmin/authenticate/oauth2.py b/web/pgadmin/authenticate/oauth2.py index cf083828a..e3a8faac3 100644 --- a/web/pgadmin/authenticate/oauth2.py +++ b/web/pgadmin/authenticate/oauth2.py @@ -21,7 +21,7 @@ from flask_security.utils import get_post_logout_redirect, logout_user from pgadmin.authenticate.internal import BaseAuthentication from pgadmin.model import User from pgadmin.tools.user_management import create_user -from pgadmin.utils.constants import OAUTH2 +from pgadmin.utils.constants import OAUTH2, MessageType from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.model import db @@ -61,7 +61,7 @@ def init_app(app): if 'auth_obj' in session: session.pop('auth_obj') logout_user() - flash(msg, 'danger') + flash(msg, MessageType.ERROR) return redirect(get_safe_post_login_redirect()) @blueprint.route('/logout', endpoint="logout", diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index a558514c2..6df1e1c93 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -452,7 +452,7 @@ def check_browser_upgrade(): download_url=data[config.UPGRADE_CHECK_KEY]['download_url'] ) - flash(msg, 'warning') + flash(msg, MessageType.WARNING) @blueprint.route("/") @@ -487,7 +487,7 @@ def index(): known=browser_known ) - flash(msg, 'warning') + flash(msg, MessageType.WARNING) # Get the current version info from the website, and flash a message if # the user is out of date, and the check is enabled. @@ -507,8 +507,6 @@ def index(): response = Response(render_template( MODULE_NAME + "/index.html", username=current_user.username, - requirejs=True, - basejs=True, _=gettext )) @@ -1061,66 +1059,55 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE: def change_password(): """View function which handles a change password request.""" - has_error = False form_class = _security.forms.get('change_password_form').cls req_json = request.get_json(silent=True) - if req_json: - form = form_class(MultiDict(req_json)) - else: + if not req_json: form = form_class() + return { + 'csrf_token': form.csrf_token._value() + } + elif req_json: + form = form_class(MultiDict(req_json)) + if form.validate_on_submit(): + errormsg = None + try: + change_user_password(current_user._get_current_object(), + form.new_password.data, + autologin=False) + except SOCKETErrorException as e: + # Handle socket errors which are not covered by + # SMTPExceptions. + logging.exception(str(e), exc_info=True) + errormsg = gettext(SMTP_SOCKET_ERROR).format(e) + except (SMTPConnectError, SMTPResponseException, + SMTPServerDisconnected, SMTPDataError, SMTPHeloError, + SMTPException, SMTPAuthenticationError, + SMTPSenderRefused, SMTPRecipientsRefused) as ex: + # Handle smtp specific exceptions. + logging.exception(str(ex), exc_info=True) + errormsg = gettext(SMTP_ERROR).format(ex) + except Exception as e: + # Handle other exceptions. + logging.exception(str(e), exc_info=True) + errormsg = gettext(PASS_ERROR).format(e) - if form.validate_on_submit(): - try: - change_user_password(current_user._get_current_object(), - form.new_password.data) - except SOCKETErrorException as e: - # Handle socket errors which are not covered by SMTPExceptions. - logging.exception(str(e), exc_info=True) - flash(gettext(SMTP_SOCKET_ERROR).format(e), - 'danger') - has_error = True - except (SMTPConnectError, SMTPResponseException, - SMTPServerDisconnected, SMTPDataError, SMTPHeloError, - SMTPException, SMTPAuthenticationError, SMTPSenderRefused, - SMTPRecipientsRefused) as e: - # Handle smtp specific exceptions. - logging.exception(str(e), exc_info=True) - flash(gettext(SMTP_ERROR).format(e), - 'danger') - has_error = True - except Exception as e: - # Handle other exceptions. - logging.exception(str(e), exc_info=True) - flash( - gettext(PASS_ERROR).format(e), - 'danger' - ) - has_error = True + if request.get_json(silent=True) is None and errormsg is None: + old_key = get_crypt_key()[1] + set_crypt_key(form.new_password.data, False) - if request.get_json(silent=True) is None and not has_error: - after_this_request(view_commit) - do_flash(*get_message('PASSWORD_CHANGE')) + from pgadmin.browser.server_groups.servers.utils \ + import reencrpyt_server_passwords + reencrpyt_server_passwords( + current_user.id, old_key, form.new_password.data) - old_key = get_crypt_key()[1] - set_crypt_key(form.new_password.data, False) + return redirect(get_url(_security.post_change_view) or + get_url(_security.post_login_view)) + else: + return internal_server_error(errormsg) + else: + return bad_request(list(form.errors.values())[0][0]) - from pgadmin.browser.server_groups.servers.utils \ - import reencrpyt_server_passwords - reencrpyt_server_passwords( - current_user.id, old_key, form.new_password.data) - - return redirect(get_url(_security.post_change_view) or - get_url(_security.post_login_view)) - - if request.get_json(silent=True) and not has_error: - form.user = current_user - return default_render_json(form) - - return _security.render_template( - config_value('CHANGE_PASSWORD_TEMPLATE'), - change_password_form=form, - **_ctx('change_password')) # Only register route if SECURITY_RECOVERABLE is set to True if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: @@ -1171,7 +1158,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: 'Please contact the administrators of this ' 'service if you need to reset your password.' ).format(form.user.auth_source), - 'danger') + MessageType.ERROR) has_error = True if not has_error: try: @@ -1181,7 +1168,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: # covered by SMTPExceptions. logging.exception(str(e), exc_info=True) flash(gettext(SMTP_SOCKET_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True except (SMTPConnectError, SMTPResponseException, SMTPServerDisconnected, SMTPDataError, SMTPHeloError, @@ -1191,13 +1178,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: # Handle smtp specific exceptions. logging.exception(str(e), exc_info=True) flash(gettext(SMTP_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True except Exception as e: # Handle other exceptions. logging.exception(str(e), exc_info=True) flash(gettext(PASS_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True if request.get_json(silent=True) is None and not has_error: @@ -1247,7 +1234,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: # Handle socket errors which are not covered by SMTPExceptions. logging.exception(str(e), exc_info=True) flash(gettext(SMTP_SOCKET_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True except (SMTPConnectError, SMTPResponseException, SMTPServerDisconnected, SMTPDataError, SMTPHeloError, @@ -1257,13 +1244,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: # Handle smtp specific exceptions. logging.exception(str(e), exc_info=True) flash(gettext(SMTP_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True except Exception as e: # Handle other exceptions. logging.exception(str(e), exc_info=True) flash(gettext(PASS_ERROR).format(e), - 'danger') + MessageType.ERROR) has_error = True if not has_error: @@ -1275,7 +1262,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: flash(gettext('You successfully reset your password but' ' your account is locked. Please contact ' 'the Administrator.'), - 'warning') + MessageType.WARNING) return redirect(get_post_logout_redirect()) do_flash(*get_message('PASSWORD_RESET')) login_user(user) diff --git a/web/pgadmin/browser/static/scss/_browser.scss b/web/pgadmin/browser/static/scss/_browser.scss index 096bb0fac..34ac6e981 100644 --- a/web/pgadmin/browser/static/scss/_browser.scss +++ b/web/pgadmin/browser/static/scss/_browser.scss @@ -28,6 +28,7 @@ samp, min-height: 100%; background: $loading-bg; z-index: 1056; + top: 0; .pg-sp-content { position: absolute; diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py index 97ba7ff50..fe4f8d79c 100644 --- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py +++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py @@ -67,6 +67,10 @@ class KerberosLoginMockTestCase(BaseTestGenerator): def runTest(self): """This function checks spnego/kerberos login functionality.""" if self.flag == 1: + if app_config.SERVER_MODE is False: + self.skipTest( + "Can not run Kerberos Authentication in the Desktop mode." + ) self.test_unauthorized() elif self.flag == 2: if app_config.SERVER_MODE is False: diff --git a/web/pgadmin/static/js/AppMenuBar.jsx b/web/pgadmin/static/js/AppMenuBar.jsx index 5e5c3cadb..ab4df6e3b 100644 --- a/web/pgadmin/static/js/AppMenuBar.jsx +++ b/web/pgadmin/static/js/AppMenuBar.jsx @@ -125,7 +125,7 @@ export default function AppMenuBar() {
{userMenuInfo.gravatar && Gravatar image for {{ username }}} + alt ={`Gravatar image for ${ userMenuInfo.username }`} />} {!userMenuInfo.gravatar && }
{ userMenuInfo.username } ({userMenuInfo.auth_source}) diff --git a/web/pgadmin/static/js/Dialogs/ChangePasswordContent.jsx b/web/pgadmin/static/js/Dialogs/ChangePasswordContent.jsx index d5bc4395c..fc646d447 100644 --- a/web/pgadmin/static/js/Dialogs/ChangePasswordContent.jsx +++ b/web/pgadmin/static/js/Dialogs/ChangePasswordContent.jsx @@ -15,7 +15,7 @@ import BaseUISchema from '../SchemaView/base_schema.ui'; import SchemaView from '../SchemaView'; class ChangePasswordSchema extends BaseUISchema { - constructor(user, isPgpassFileUsed) { + constructor(user, isPgpassFileUsed, hasCsrfToken=false, showUser=true) { super({ user: user, password: '', @@ -23,13 +23,15 @@ class ChangePasswordSchema extends BaseUISchema { confirmPassword: '' }); this.isPgpassFileUsed = isPgpassFileUsed; + this.hasCsrfToken = hasCsrfToken; + this.showUser = showUser; } get baseFields() { let self = this; return [ { - id: 'user', label: gettext('User'), type: 'text', disabled: true + id: 'user', label: gettext('User'), type: 'text', disabled: true, visible: this.showUser }, { id: 'password', label: gettext('Current Password'), type: 'password', disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true, @@ -42,14 +44,18 @@ class ChangePasswordSchema extends BaseUISchema { controlProps: { maxLength: null } - }, { + }, { id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password', noEmpty: true, controlProps: { maxLength: null } } - ]; + ].concat(this.hasCsrfToken ? [ + { + id: 'csrf_token', visible: false, type: 'text' + } + ]: []); } validate(state, setError) { @@ -72,13 +78,14 @@ const useStyles = makeStyles((theme)=>({ }, })); -export default function ChangePasswordContent({onSave, onClose, userName, isPgpassFileUsed}) { +export default function ChangePasswordContent({getInitData=() => { /*This is intentional (SonarQube)*/ }, + onSave, onClose, hasCsrfToken=false, showUser=true}) { const classes = useStyles(); return { /*This is intentional (SonarQube)*/ }} - schema={new ChangePasswordSchema(userName, isPgpassFileUsed)} + getInitData={getInitData} + schema={new ChangePasswordSchema('', false, hasCsrfToken, showUser)} viewHelperProps={{ mode: 'create', }} @@ -96,5 +103,8 @@ ChangePasswordContent.propTypes = { onSave: PropTypes.func, onClose: PropTypes.func, userName: PropTypes.string, - isPgpassFileUsed: PropTypes.bool + isPgpassFileUsed: PropTypes.bool, + getInitData: PropTypes.func, + hasCsrfToken: PropTypes.bool, + showUser: PropTypes.bool }; diff --git a/web/pgadmin/static/js/Dialogs/index.jsx b/web/pgadmin/static/js/Dialogs/index.jsx index 82cc34c4d..b74c4101c 100644 --- a/web/pgadmin/static/js/Dialogs/index.jsx +++ b/web/pgadmin/static/js/Dialogs/index.jsx @@ -292,6 +292,52 @@ export function showChangeServerPassword() { }); } +export function showChangeUserPassword(url) { + mountDialog(gettext('Change pgAdmin User Password'), (onClose)=> { + const api = getApiInstance(); + return + { + return new Promise((resolve, reject)=>{ + api.get(url) + .then((res)=>{ + resolve(res.data); + }) + .catch((err)=>{ + reject(err); + }); + }); + }} + onClose={()=>{ + onClose(); + }} + onSave={(_isNew, data)=>{ + return new Promise((resolve, reject)=>{ + const formData = { + 'password': data.password, + 'new_password': data.newPassword, + 'new_password_confirm': data.confirmPassword, + 'csrf_token': data.csrf_token + }; + + api({ + method: 'POST', + url: url, + data: formData, + }).then((res)=>{ + resolve(res); + }).catch((err)=>{ + reject(err); + }); + }); + }} + hasCsrfToken={true} + showUser={false} + /> + ; + }, undefined, undefined, pgAdmin.Browser.stdH.sm); +} + export function showNamedRestorePoint() { let title = arguments[0], nodeData = arguments[1], diff --git a/web/pgadmin/static/js/SecurityPages/BasePage.jsx b/web/pgadmin/static/js/SecurityPages/BasePage.jsx new file mode 100644 index 000000000..3081bbd45 --- /dev/null +++ b/web/pgadmin/static/js/SecurityPages/BasePage.jsx @@ -0,0 +1,101 @@ +import { Box, Button, darken, makeStyles } from '@material-ui/core'; +import { useSnackbar } from 'notistack'; +import React, { useEffect } from 'react'; +import { MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents'; +import { FinalNotifyContent } from '../helpers/Notifier'; +import PropTypes from 'prop-types'; +import CustomPropTypes from '../custom_prop_types'; + +const contentBg = '#2b709b'; +const loginBtnBg = '#038bba'; + +const useStyles = makeStyles((theme)=>({ + root: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + display: 'flex', + justifyContent: 'center', + height: '100%', + }, + pageContent: { + display: 'flex', + flexDirection: 'column', + padding: '16px', + backgroundColor: contentBg, + borderRadius: theme.shape.borderRadius, + maxHeight: '100%', + minWidth: '450px', + maxWidth: '450px' + }, + logo: { + width: '96px', + height: '40px', + background: 'url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDUgNTAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZmZmO30uY2xzLTJ7ZmlsbDojMzI2ODkzO308L3N0eWxlPjwvZGVmcz48dGl0bGU+cGdBZG1pbjwvdGl0bGU+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTguOTQsNDEuNGEyLjQ4LDIuNDgsMCwwLDEtMi4yNy0zLjQ5TDY0LDIxLjI5VjZhNiw2LDAsMCwwLTYtNkg2QTYsNiwwLDAsMCwwLDZWNDRhNiw2LDAsMCwwLDYsNkg1OGE2LDYsMCwwLDAsNi02VjQxLjRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjkuMjUsMzAuMTdhMTMuMTMsMTMuMTMsMCwwLDEtMS44Mi02LjkzLDEzLDEzLDAsMCwxLDEuODItNi44OCwxMi41LDEyLjUsMCwwLDEsMS40OC0xLjk1LDEwLjQ0LDEwLjQ0LDAsMCwwLTMuMjUtMi44OSwxMS4xNiwxMS4xNiwwLDAsMC01LjY1LTEuNDVxLTQuNDgsMC02LjcyLDIuNjRWMTAuNDRINy41MVY0MC4zNmExLDEsMCwwLDAsMSwxaDZhMSwxLDAsMCwwLDEtMVYzMS4xOWE4LjQ3LDguNDcsMCwwLDAsNi4zNCwyLjQsMTEuMjYsMTEuMjYsMCwwLDAsNS42NS0xLjQ1LDEwLjUzLDEwLjUzLDAsMCwwLDIuMDYtMS41NkMyOS40NCwzMC40NCwyOS4zNCwzMC4zMSwyOS4yNSwzMC4xN1pNMjMuNiwyNS44YTQuNTIsNC41MiwwLDAsMS0zLjQ1LDEuNDQsNC40OCw0LjQ4LDAsMCwxLTMuNDQtMS40NCw1LjYsNS42LDAsMCwxLTEuMzUtNCw1LjU5LDUuNTksMCwwLDEsMS4zNS00LDQuNDYsNC40NiwwLDAsMSwzLjQ0LTEuNDUsNC40OSw0LjQ5LDAsMCwxLDMuNDUsMS40NSw1LjYzLDUuNjMsMCwwLDEsMS4zNCw0QTUuNjQsNS42NCwwLDAsMSwyMy42LDI1LjhaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTYuNDksMTIuNjNWMzEuMjRxMCw2LjM1LTMuNDQsOS41MXQtOS45MiwzLjE3YTI1LjQyLDI1LjQyLDAsMCwxLTYuMy0uNzUsMTUsMTUsMCwwLDEtNS0yLjIzbDIuODktNS41OWExMC4xNywxMC4xNywwLDAsMCwzLjUxLDEuNzksMTQuMzcsMTQuMzcsMCwwLDAsNC4xOC42NUE2LjUzLDYuNTMsMCwwLDAsNDcsMzYuNGE1LjM3LDUuMzcsMCwwLDAsMS40Ny00LjExdi0uNzZjLTEuNTQsMS44LTMuNzksMi42OS02Ljc2LDIuNjlhMTEuNywxMS43LDAsMCwxLTUuNTktMS4zNkExMC4zNywxMC4zNywwLDAsMSwzMi4wOSwyOWExMC44OSwxMC44OSwwLDAsMS0xLjUxLTUuNzcsMTAuODYsMTAuODYsMCwwLDEsMS41MS01Ljc0LDEwLjQyLDEwLjQyLDAsMCwxLDQuMDctMy44NiwxMS43MSwxMS43MSwwLDAsMSw1LjU5LTEuMzdjMy4yNSwwLDUuNjMsMS4wNiw3LjE0LDMuMTVWMTIuNjNabS05LjMsMTMuOTVhNC40LDQuNCwwLDAsMCwxLjQtMy4zNiw0LjM0LDQuMzQsMCwwLDAtMS4zOC0zLjM0LDUuNjUsNS42NSwwLDAsMC03LjE2LDAsNC4zLDQuMywwLDAsMC0xLjQxLDMuMzQsNC4zNSw0LjM1LDAsMCwwLDEuNDMsMy4zNiw1LjA4LDUuMDgsMCwwLDAsMy41NywxLjNBNSw1LDAsMCwwLDQ3LjE5LDI2LjU4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTgzLjQzLDMyLjg5SDcxbC0yLDUuMDlhMSwxLDAsMCwxLS45My42Mkg2MS43M2ExLDEsMCwwLDEtLjkxLTEuNEw3Mi45MSw5LjhhMSwxLDAsMCwxLC45Mi0uNmg2Ljg5YTEsMSwwLDAsMSwuOTEuNkw5My43NywzNy4yYTEsMSwwLDAsMS0uOTIsMS40SDg2LjQxYTEsMSwwLDAsMS0uOTMtLjYyWk04MSwyNi43NmwtMy43OC05LjQxLTMuNzgsOS40MVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xMjAuNDQsOC40NFYzNy42YTEsMSwwLDAsMS0xLDFoLTUuNmExLDEsMCwwLDEtMS0xVjM2LjMzUTExMC42MiwzOSwxMDYuMTYsMzlhMTEuMjksMTEuMjksMCwwLDEtNS42Ny0xLjQ1LDEwLjU0LDEwLjU0LDAsMCwxLTQtNC4xNEExMi42MiwxMi42MiwwLDAsMSw5NSwyNy4xOCwxMi41MywxMi41MywwLDAsMSw5Ni40NCwyMWExMC4zNSwxMC4zNSwwLDAsMSw0LTQuMDksMTEuNDgsMTEuNDgsMCwwLDEsNS42Ny0xLjQzLDguMjQsOC4yNCwwLDAsMSw2LjMsMi4zNVY4LjQ0YTEsMSwwLDAsMSwxLTFoNkExLDEsMCwwLDEsMTIwLjQ0LDguNDRabS05LjE5LDIyLjc1YTUuNzEsNS43MSwwLDAsMCwxLjM0LTQsNS42LDUuNiwwLDAsMC0xLjMyLTMuOTUsNC40Nyw0LjQ3LDAsMCwwLTMuNDMtMS40Myw0LjUzLDQuNTMsMCwwLDAtMy40NCwxLjQzLDUuNTEsNS41MSwwLDAsMC0xLjM0LDMuOTUsNS42Nyw1LjY3LDAsMCwwLDEuMzQsNCw0Ljc3LDQuNzcsMCwwLDAsNi44NSwwWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTE2MSwxOGMxLjY2LDEuNjgsMi41LDQuMjEsMi41LDcuNnYxMmExLDEsMCwwLDEtMSwxaC02YTEsMSwwLDAsMS0xLTFWMjYuODhhNS42Nyw1LjY3LDAsMCwwLS45LTMuNTMsMy4wOSwzLjA5LDAsMCwwLTIuNTUtMS4xMywzLjYyLDMuNjIsMCwwLDAtMi44OSwxLjI2LDUuNzEsNS43MSwwLDAsMC0xLjEsMy44MlYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYyNi44OGMwLTMuMTEtMS4xNC00LjY2LTMuNDQtNC42NmEzLjcsMy43LDAsMCwwLTIuOTQsMS4yNiw1LjcxLDUuNzEsMCwwLDAtMS4wOSwzLjgyVjM3LjZhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjE2Ljg0YTEsMSwwLDAsMSwxLTFoNS42YTEsMSwwLDAsMSwxLDF2MS4zOWE4LDgsMCwwLDEsMy0yLjA4LDEwLjIzLDEwLjIzLDAsMCwxLDMuOC0uNjksMTAsMTAsMCwwLDEsNC4yOS44OEE3LjI4LDcuMjgsMCwwLDEsMTQ2LjQyLDE5YTguODUsOC44NSwwLDAsMSwzLjQxLTIuNjUsMTAuOTMsMTAuOTMsMCwwLDEsNC40OS0uOTJBOSw5LDAsMCwxLDE2MSwxOFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xNjguMTIsMTIuMWEzLjkxLDMuOTEsMCwwLDEtMS4zNC0yLjc5QTQuMTYsNC4xNiwwLDAsMSwxNjgsNi4xOWE1LDUsMCwwLDEsMy42Ny0xLjM2QTUuMjUsNS4yNSwwLDAsMSwxNzUuMTgsNmEzLjc1LDMuNzUsMCwwLDEsMS4zNCwzLDQuMSw0LjEsMCwwLDEtMS4zNCwzLjEzLDUuNjgsNS42OCwwLDAsMS03LjA2LDBabS41NCwzLjc0aDZhMSwxLDAsMCwxLDEsMVYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NEExLDEsMCwwLDEsMTY4LjY2LDE1Ljg0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTIwMS41NSwxOHEyLjU5LDIuNTIsMi41OSw3LjZ2MTJhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjI2Ljg4cTAtNC42Ni0zLjc0LTQuNjZhNC4zLDQuMywwLDAsMC0zLjMsMS4zNCw1LjgzLDUuODMsMCwwLDAtMS4yNCw0djEwYTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NGExLDEsMCwwLDEsMS0xaDUuNjFhMSwxLDAsMCwxLDEsMXYxLjQ3YTkuMDUsOS4wNSwwLDAsMSwzLjE5LTIuMTIsMTAuNzgsMTAuNzgsMCwwLDEsNC0uNzNBOS4zNCw5LjM0LDAsMCwxLDIwMS41NSwxOFoiLz48L3N2Zz4=) 0 0 no-repeat', + backgroundPositionY: 'center', + }, + item: { + display: 'flex', + justifyContent: 'center', + marginBottom: '15px', + fontSize: '1.2rem' + }, + button: { + backgroundColor: loginBtnBg, + color: '#fff', + padding: '4px 8px', + width: '100%', + '&:hover': { + backgroundColor: darken(loginBtnBg, 0.1), + }, + '&.Mui-disabled': { + opacity: 0.60, + color: '#fff' + }, + } +})); + +export function SecurityButton({...props}) { + const classes = useStyles(); + return - {% endif %} -
- {% if show_login_form %} -
- {{ _('Forgotten your password?', url=url_for('browser.forgot_password')) }} -
- {% endif %} - -
-{% if config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES %} - {% for oauth_config in config.OAUTH2_CONFIG %} - - {% endfor %} -{% endif %} - -{% endif %} -{% endblock %} +{% set page_name = 'login_user' %} +{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %} +{% set ns = namespace(langOptions=[]) %} +{% for key, lang in config.LANGUAGES.items() %} +{% set _ = ns.langOptions.append({'value': key, 'label': lang}) %} +{% endfor %} +{% set page_props = { + 'userLanguage': user_language, + 'langOptions': ns.langOptions, + 'forgotPassUrl': url_for('browser.forgot_password'), + 'loginUrl': url_for('authenticate.login'), + 'csrfToken': csrf_token(), + 'authSources': config.AUTHENTICATION_SOURCES, + 'authSourcesEnum': { + 'OAUTH2': config.OAUTH2, + 'KERBEROS': config.KERBEROS, + }, + 'oauth2Config': config.OAUTH2_CONFIG, + 'loginBanner': config.LOGIN_BANNER|safe +} %} +{% extends "security/render_page.html" %} +{% block title %}{{ _('Login') }}{% endblock %} diff --git a/web/pgadmin/templates/security/messages.html b/web/pgadmin/templates/security/messages.html deleted file mode 100644 index 4f938a820..000000000 --- a/web/pgadmin/templates/security/messages.html +++ /dev/null @@ -1,21 +0,0 @@ -{%- with messages = get_flashed_messages(with_categories=true) -%} -{% if messages %} -
- {% for category, message in messages %} - - {% endfor %} -
- -{% endif %} -{%- endwith %} diff --git a/web/pgadmin/templates/security/panel.html b/web/pgadmin/templates/security/panel.html deleted file mode 100644 index d0fdef76e..000000000 --- a/web/pgadmin/templates/security/panel.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "base.html" %} -{% from "security/fields.html" import render_field_with_errors, render_username_with_errors %} -{% block title %}{{ config.APP_NAME }}{% endblock %} -{% block body %} - -{% endblock %} diff --git a/web/pgadmin/templates/security/render_page.html b/web/pgadmin/templates/security/render_page.html new file mode 100644 index 000000000..0689ef63d --- /dev/null +++ b/web/pgadmin/templates/security/render_page.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% set other_props = { + 'messages': get_flashed_messages(with_categories=true) +} %} +{% block body %} + +
+
+
+
+
+
+{% endblock %} +{% block init_script %} +try { + require( + ['security.pages'], + function() { + window.renderSecurityPage('{{page_name}}', {{page_props|tojson|safe}}, + {{other_props|tojson|safe}}); + }, function() { + console.log(arguments); + } + ); +} catch (err) { + console.log(err); +} +{% endblock %} diff --git a/web/pgadmin/templates/security/reset_password.html b/web/pgadmin/templates/security/reset_password.html index 2023eb351..ba135225d 100644 --- a/web/pgadmin/templates/security/reset_password.html +++ b/web/pgadmin/templates/security/reset_password.html @@ -1,17 +1,8 @@ -{% extends "security/panel.html" %} -{% block panel_title %}{{ _('%(appname)s Password Reset', appname=config.APP_NAME) }}{% endblock %} -{% block panel_body %} -{% if config.SERVER_MODE %} -
- {{ reset_password_form.hidden_tag() }} -
- {{ _('Reset Password Form') }} - {{ render_field_with_errors(reset_password_form.password, "password") }} - {{ render_field_with_errors(reset_password_form.password_confirm, "password") }} - -
-
-{% endif %} -{% endblock %} +{% set page_name = 'reset_password' %} +{% set page_props = { + 'actionUrl': url_for('browser.reset_password', token=reset_password_token), + 'csrfToken': csrf_token(), +} %} +{% extends "security/render_page.html" %} +{% block title %}{{ _('Reset Password') }}{% endblock %} + diff --git a/web/pgadmin/templates/security/watermark.html b/web/pgadmin/templates/security/watermark.html deleted file mode 100644 index 69a1ffd93..000000000 --- a/web/pgadmin/templates/security/watermark.html +++ /dev/null @@ -1,7 +0,0 @@ -{% block watermark %} -
- {{ config.APP_NAME }} {{ _('logo') }} -
-{% endblock %} diff --git a/web/pgadmin/tools/debugger/__init__.py b/web/pgadmin/tools/debugger/__init__.py index 9afbd4018..e3f566e17 100644 --- a/web/pgadmin/tools/debugger/__init__.py +++ b/web/pgadmin/tools/debugger/__init__.py @@ -638,8 +638,6 @@ def direct_new(trans_id): is_linux=is_linux_platform, client_platform=user_agent.platform, function_name_with_arguments=function_name_with_arguments, - requirejs=True, - basejs=True, layout=layout ) diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index a95896dc3..767d9f435 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -491,8 +491,6 @@ def panel(trans_id): return render_template( "erd/index.html", title=underscore_unescape(params['title']), - requirejs=True, - basejs=True, params=json.dumps(params), ) diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py index 6f5566a8b..59ac0aae5 100644 --- a/web/pgadmin/tools/psql/__init__.py +++ b/web/pgadmin/tools/psql/__init__.py @@ -113,8 +113,6 @@ def panel(trans_id): title=underscore_unescape(params['title']), theme=params['theme'], o_db_name=o_db_name, - requirejs=True, - basejs=True, platform=_platform ) diff --git a/web/pgadmin/tools/schema_diff/__init__.py b/web/pgadmin/tools/schema_diff/__init__.py index 40d9b651c..af59a95e1 100644 --- a/web/pgadmin/tools/schema_diff/__init__.py +++ b/web/pgadmin/tools/schema_diff/__init__.py @@ -127,8 +127,6 @@ def panel(trans_id, editor_title): "schema_diff/index.html", _=gettext, trans_id=trans_id, - requirejs=True, - basejs=True, editor_title=editor_title, ) diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index e03c14185..9a72b6efd 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -333,8 +333,6 @@ def panel(trans_id): "sqleditor/index.html", title=underscore_unescape(params['title']), params=json.dumps(params), - requirejs=True, - basejs=True, ) diff --git a/web/pgadmin/tools/user_management/static/js/user_management.js b/web/pgadmin/tools/user_management/static/js/user_management.js index aa3fc56ef..ee5f0b055 100644 --- a/web/pgadmin/tools/user_management/static/js/user_management.js +++ b/web/pgadmin/tools/user_management/static/js/user_management.js @@ -9,10 +9,9 @@ import pgAdmin from 'sources/pgadmin'; import gettext from 'sources/gettext'; -import { showUrlDialog } from '../../../../static/js/Dialogs/index'; +import { showChangeUserPassword, showUrlDialog } from '../../../../static/js/Dialogs/index'; import { showUserManagement } from './UserManagementDialog'; - class UserManagement { static instance; @@ -31,7 +30,7 @@ class UserManagement { // This is a callback function to show change user dialog. change_password(url) { - showUrlDialog(gettext('Change Password'), url, 'change_user_password.html', undefined, pgAdmin.Browser.stdH.lg); + showChangeUserPassword(url); } // This is a callback function to show 2FA dialog. diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 4d65bfef4..acef546a1 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -132,3 +132,11 @@ KEY_RING_SERVICE_NAME = 'pgAdmin4' KEY_RING_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}' KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}' KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}' + + +class MessageType: + SUCCESS = 'Success', + ERROR = 'Error', + INFO = 'Info', + CLOSE = 'Close', + WARNING = 'Warning' diff --git a/web/regression/javascript/SecurityPages/ForgotPasswordPage.spec.js b/web/regression/javascript/SecurityPages/ForgotPasswordPage.spec.js new file mode 100644 index 000000000..35f90e101 --- /dev/null +++ b/web/regression/javascript/SecurityPages/ForgotPasswordPage.spec.js @@ -0,0 +1,53 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import ForgotPasswordPage from '../../../pgadmin/static/js/SecurityPages/ForgotPasswordPage'; + +describe('ForgotPasswordPage', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + // spyOn(Notify, 'alert'); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('basic', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/forgot/url', + csrfToken: 'some-token', + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/forgot/url'); + expect(ctrl.find('input[name="email"]')).toExist(); + ctrl.unmount(); + done(); + }, 100); + }); +}); diff --git a/web/regression/javascript/SecurityPages/LoginPage.spec.js b/web/regression/javascript/SecurityPages/LoginPage.spec.js new file mode 100644 index 000000000..ba22ca111 --- /dev/null +++ b/web/regression/javascript/SecurityPages/LoginPage.spec.js @@ -0,0 +1,100 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import LoginPage from '../../../pgadmin/static/js/SecurityPages/LoginPage'; + +describe('LoginPage', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + // spyOn(Notify, 'alert'); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('internal', (done)=>{ + const ctrl = ctrlMount({ + userLanguage: 'en', + langOptions: [{ + label: 'English', + value: 'en', + }], + forgotPassUrl: '/forgot/url', + csrfToken: 'some-token', + loginUrl: '/login/url', + authSources: ['internal'], + authSourcesEnum: { + OAUTH2: 'oauth2', + KERBEROS: 'kerberos' + }, + oauth2Config: [], + loginBanner: 'login banner' + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/login/url'); + expect(ctrl.find('input[name="email"]')).toExist(); + expect(ctrl.find('input[name="password"]')).toExist(); + ctrl.unmount(); + done(); + }, 100); + }); + + it('oauth2', (done)=>{ + const ctrl = ctrlMount({ + userLanguage: 'en', + langOptions: [{ + label: 'English', + value: 'en', + }], + forgotPassUrl: '/forgot/url', + csrfToken: 'some-token', + loginUrl: '/login/url', + authSources: ['internal', 'oauth2'], + authSourcesEnum: { + OAUTH2: 'oauth2', + KERBEROS: 'kerberos' + }, + oauth2Config: [{ + OAUTH2_NAME: 'github', + OAUTH2_BUTTON_COLOR: '#fff', + OAUTH2_ICON: 'fa-github', + OAUTH2_DISPLAY_NAME: 'Github' + }], + loginBanner: '' + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/login/url'); + expect(ctrl.find('input[name="email"]')).toExist(); + expect(ctrl.find('input[name="password"]')).toExist(); + expect(ctrl.find('button[name="oauth2_button"]')).toHaveProp('value', 'github'); + ctrl.unmount(); + done(); + }, 100); + }); +}); diff --git a/web/regression/javascript/SecurityPages/MfaRegisterPage.spec.js b/web/regression/javascript/SecurityPages/MfaRegisterPage.spec.js new file mode 100644 index 000000000..12462333b --- /dev/null +++ b/web/regression/javascript/SecurityPages/MfaRegisterPage.spec.js @@ -0,0 +1,196 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import MfaRegisterPage from '../../../pgadmin/static/js/SecurityPages/MfaRegisterPage'; + +describe('MfaRegisterPage', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + // spyOn(Notify, 'alert'); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('email registered', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/register', + mfaList: [{ + label: 'Email', + icon: '', + registered: true, + },{ + label: 'Authenticator', + icon: '', + registered: false, + }], + nextUrl: '', + mfaView: null, + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/mfa/register'); + expect(ctrl.find('EmailRegisterView')).not.toExist(); + expect(ctrl.find('AuthenticatorRegisterView')).not.toExist(); + expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(1); + expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(1); + ctrl.unmount(); + done(); + }, 100); + }); + + it('both registered', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/register', + mfaList: [{ + label: 'Email', + icon: '', + registered: true, + },{ + label: 'Authenticator', + icon: '', + registered: true, + }], + nextUrl: '', + mfaView: null, + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/mfa/register'); + expect(ctrl.find('EmailRegisterView')).not.toExist(); + expect(ctrl.find('AuthenticatorRegisterView')).not.toExist(); + expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(2); + expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0); + ctrl.unmount(); + done(); + }, 100); + }); + + it('email view register', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/register', + mfaList: [{ + label: 'Email', + icon: '', + registered: false, + },{ + label: 'Authenticator', + icon: '', + registered: false, + }], + nextUrl: '', + mfaView: { + label: 'email_authentication_label', + auth_method: 'email', + description:'Enter the email address to send a code', + email_address_placeholder:'Email address', + email_address:'email@test.com', + note_label:'Note', + note:'This email address will only be used for two factor' + }, + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/mfa/register'); + expect(ctrl.find('EmailRegisterView')).toExist(); + expect(ctrl.find('input[name="send_to"]')).toExist(); + expect(ctrl.find('AuthenticatorRegisterView')).not.toExist(); + expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0); + expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0); + ctrl.unmount(); + done(); + }, 100); + }); + + it('email view otp code', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/register', + mfaList: [{ + label: 'Email', + icon: '', + registered: false, + },{ + label: 'Authenticator', + icon: '', + registered: false, + }], + nextUrl: '', + mfaView: { + label: 'email_authentication_label', + auth_method: 'email', + description:'Enter the email address to send a code', + otp_placeholder:'Enter OTP', + email_address:'email@test.com', + note_label:'Note', + note:'This email address will only be used for two factor' + }, + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/mfa/register'); + expect(ctrl.find('EmailRegisterView')).toExist(); + expect(ctrl.find('input[name="code"]')).toExist(); + expect(ctrl.find('AuthenticatorRegisterView')).not.toExist(); + expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0); + expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0); + ctrl.unmount(); + done(); + }, 100); + }); + + it('authenticator view register', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/register', + mfaList: [{ + label: 'Email', + icon: '', + registered: false, + },{ + label: 'Authenticator', + icon: '', + registered: false, + }], + nextUrl: '', + mfaView: { + auth_title:'_TOTP_AUTHENTICATOR', + auth_method: 'authenticator', + image: 'image', + qrcode_alt_text: 'TOTP Authenticator QRCode', + auth_description: 'Scan the QR code and the enter the code', + otp_placeholder: 'Enter code' + }, + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/mfa/register'); + expect(ctrl.find('EmailRegisterView')).not.toExist(); + expect(ctrl.find('AuthenticatorRegisterView')).toExist(); + expect(ctrl.find('input[name="code"]')).toExist(); + expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0); + expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0); + ctrl.unmount(); + done(); + }, 100); + }); +}); diff --git a/web/regression/javascript/SecurityPages/MfaValidatePage.spec.js b/web/regression/javascript/SecurityPages/MfaValidatePage.spec.js new file mode 100644 index 000000000..4fe00b1ba --- /dev/null +++ b/web/regression/javascript/SecurityPages/MfaValidatePage.spec.js @@ -0,0 +1,128 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import MfaValidatePage from '../../../pgadmin/static/js/SecurityPages/MfaValidatePage'; + +describe('MfaValidatePage', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + // spyOn(Notify, 'alert'); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('email selected', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/validate', + views: { + 'email': { + id: 'email', + label: 'Email', + icon: '', + selected: true, + view: { + description: 'description', + otp_placeholder: 'otp_placeholder', + button_label: 'button_label', + button_label_sending: 'button_label_sending' + } + }, + 'authenticator': { + id: 'authenticator', + label: 'Authenticator', + icon: '', + selected: false, + view: { + auth_description: 'auth_description', + otp_placeholder: 'otp_placeholder', + } + } + }, + logoutUrl: '/logout/url', + sendEmailUrl: '/send/email', + csrfHeader: 'csrfHeader', + csrfToken: 'csrfToken', + }); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('form')).toHaveProp('action', '/mfa/validate'); + expect(ctrl.find('EmailValidateView')).toExist(); + expect(ctrl.find('AuthenticatorValidateView')).not.toExist(); + expect(ctrl.find('button[name="send_code"]')).toExist(); + expect(ctrl.find('input[name="mfa_method"]').instance().value).toBe('email'); + ctrl.unmount(); + done(); + }, 100); + }); + + it('authenticator selected', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/mfa/validate', + views: { + 'email': { + id: 'email', + label: 'Email', + icon: '', + selected: false, + view: { + description: 'description', + otp_placeholder: 'otp_placeholder', + button_label: 'button_label', + button_label_sending: 'button_label_sending' + } + }, + 'authenticator': { + id: 'authenticator', + label: 'Authenticator', + icon: '', + selected: true, + view: { + auth_description: 'auth_description', + otp_placeholder: 'otp_placeholder', + } + } + }, + logoutUrl: '/logout/url', + sendEmailUrl: '/send/email', + csrfHeader: 'csrfHeader', + csrfToken: 'csrfToken', + }); + setTimeout(()=>{ + ctrl.update(); + expect(ctrl.find('form')).toHaveProp('action', '/mfa/validate'); + expect(ctrl.find('EmailValidateView')).not.toExist(); + expect(ctrl.find('AuthenticatorValidateView')).toExist(); + expect(ctrl.find('input[name="code"]')).toExist(); + expect(ctrl.find('input[name="mfa_method"]').instance().value).toBe('authenticator'); + ctrl.unmount(); + done(); + }, 100); + }); +}); diff --git a/web/regression/javascript/SecurityPages/PasswordResetPage.spec.js b/web/regression/javascript/SecurityPages/PasswordResetPage.spec.js new file mode 100644 index 000000000..14411e8cb --- /dev/null +++ b/web/regression/javascript/SecurityPages/PasswordResetPage.spec.js @@ -0,0 +1,54 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2023, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import PasswordResetPage from '../../../pgadmin/static/js/SecurityPages/PasswordResetPage'; + +describe('PasswordResetPage', ()=>{ + let mount; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + // spyOn(Notify, 'alert'); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + }); + + let ctrlMount = (props)=>{ + return mount( + + ); + }; + + it('basic', (done)=>{ + const ctrl = ctrlMount({ + actionUrl: '/reset/url', + csrfToken: 'some-token', + }); + setTimeout(()=>{ + expect(ctrl.find('form')).toHaveProp('action', '/reset/url'); + expect(ctrl.find('input[name="password"]')).toExist(); + expect(ctrl.find('input[name="password_confirm"]')).toExist(); + ctrl.unmount(); + done(); + }, 100); + }); +}); diff --git a/web/regression/javascript/jasmine_capture_warnings_beforeall.js b/web/regression/javascript/jasmine_capture_warnings_beforeall.js index f81f19f2a..5e838e9ca 100644 --- a/web/regression/javascript/jasmine_capture_warnings_beforeall.js +++ b/web/regression/javascript/jasmine_capture_warnings_beforeall.js @@ -1,4 +1,4 @@ -////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // diff --git a/web/webpack.config.js b/web/webpack.config.js index eec8ad69d..6e7b6059e 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -46,7 +46,7 @@ const providePlugin = new webpack.ProvidePlugin({ 'moment': 'moment', 'window.moment':'moment', process: 'process/browser', - Buffer: ['buffer', 'Buffer'], + Buffer: ['buffer', 'Buffer'] }); // Helps in debugging each single file, it extracts the module files @@ -361,6 +361,7 @@ module.exports = [{ entry: { 'app.bundle': sourceDir + '/bundle/app.js', codemirror: sourceDir + '/bundle/codemirror.js', + 'security.pages': 'security.pages', sqleditor: './pgadmin/tools/sqleditor/static/js/index.js', schema_diff: './pgadmin/tools/schema_diff/static/js/index.js', erd_tool: './pgadmin/tools/erd/static/js/index.js', diff --git a/web/webpack.shim.js b/web/webpack.shim.js index a1660cc71..37e69e4e7 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -37,6 +37,7 @@ let webpackShimConfig = { 'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'), 'tools': path.join(__dirname, './pgadmin/tools/'), 'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'), + 'security.pages': path.join(__dirname, './pgadmin/static/js/SecurityPages/index.jsx'), // Vendor JS 'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker.min'),