Remove Bootstrap and jQuery from authentication pages and rewrite them in ReactJS. #6295

This commit is contained in:
Aditya Toshniwal 2023-06-30 16:08:33 +05:30 committed by GitHub
parent 732bcc2b4d
commit d6cddd8c29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1354 additions and 663 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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):

View File

@ -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(

View File

@ -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 (
"<div class='form-group'>{auth_description}</div>"
"<div class='form-group'>"
" <input class='form-control' placeholder='{otp_placeholder}'"
" name='code' type='password' autofocus='' pattern='\\d*'"
" autocomplete='one-time-code' require/>"
"</div>"
).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([
"<h5 class='form-group text-center'>{auth_title}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
@ -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

View File

@ -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([
"<h5 class='form-group text-center'>{label}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
"<input type='hidden' name='validate' value='send_code'/>",
"<div class='form-group pt-3'>{description}</div>",
"<div class='form-group'>",
" <input class='form-control' name='send_to' type='email'",
" placeholder='{email_address_placeholder}'",
" autofocus='' value='{email_address}' required",
" pattern='[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{{2,}}$'/>",
"</div></div>",
"<div class='alert alert-warning alert-dismissible fade show'",
" role='alert'>",
" <strong>{note_label}:</strong><span>{note}</span>",
"</div>",
]).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([
"<h5 class='form-group text-center'>{label}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
"<input type='hidden' name='validate' value='verify_code'/>",
"<div class='form-group pt-3'>{message}</div>",
"<div class='form-group'>",
" <input class='form-control' placeholder='{otp_placeholder}'",
" pattern='\\d{{6}}' type='password' autofocus=''",
" autocomplete='one-time-code' name='code' require>",
"</div>",
]).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()

View File

@ -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);
});
}

View File

@ -1,19 +0,0 @@
<div class="form-group">
<div class="form-group">{{ message }}</div>
<div id="showme" class="hidden">
<div class="form-group pb-3">
<div class="card ">
<div class="card-body">
<div class="alert alert-warning d-flex flex-row mb-0">
<i class="fas fa-lg fa-exclamation-triangle mr-3 align-self-center"></i>
<div class="h-100">
<span class="text-primary">{{ _("Haven't received an email?") }} <a class="enable_me_in_20 alert-link" href="#" onClick="javascript:sendCodeToEmail()" disabled>{{ _("Send again") }}</a></span></div>
</div>
</div>
</div>
</div>
</div>
<div class='form-group'>
<input class='form-control' placeholder='{{ _("Enter code") }}' name='code' type='password' autofocus='' pattern='\d*' autocomplete="one-time-code">
</div>
</div>

View File

@ -1,7 +0,0 @@
<div class="form-group">
<div class="form-group text-center h6">{{ _("Verify with Email Authentication") }}</div>
<div class="form-group" id="mfa_email_auth">
<button class="btn btn-primary btn-block btn-validate" id="btn_send_code"
type="button" onclick="sendCodeToEmail()">{{ _("Send Code") }}</button>
</div>
</div>

View File

@ -1,78 +1,10 @@
{% set auth_page = true %}
{% extends "security/panel.html" %}
{% block panel_image %}
<div class="pr-4">
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Registration') }}">
</div>
{% endblock %}
{% block panel_title %}{{ _('Authentication registration') }}{% endblock %}
{% block panel_body %}
<style>
div.form-group > label > .icon {
min-width: 30px;
min-height: 35px;
}
{% if error_message is none %}
{% for mfa in mfa_list %}{% if mfa.icon|length > 0 %}
div#mfa-{{mfa.id | e}} .icon {
background: url({{ mfa.icon }}) 0% 0% no-repeat transparent;
min-height: 30px;
min-width: 30px;
}{% endif %}{% endfor %}
</style>
<form id='mfa_view' method='post'
action='{{ url_for("mfa.register") }}'>
<div class='form-group'>
{% if mfa_view is not defined or mfa_view is none %}
<div class='form-group'>
{% for mfa in mfa_list %}
<div id="mfa-{{ mfa.id }}">
<label class="my-1 d-flex align-items-center">
<i class="fas mr-2 icon"></i>
<span>{{ mfa.label | safe }}</span>
<span class="ml-auto">
<button class='btn btn-primary btn-block btn-validate' name='{{ mfa.id }}'
type='submit'
value='{% if mfa.registered %}DELETE{% else %}SETUP{% endif %}'>{% if mfa.registered %}{{ _("Delete") }}{% else %}{{ _("Setup") }}{% endif %}</button>
</span>
</label>
</div>
{% endfor %}
</div>
{% if next_url != 'internal' %}
<div class="row align-items-center p-2">
<button class='btn btn-primary btn-block btn-validate col' type='submit'
value='{{ _("Continue") }}'>{{ _("Continue") }}</button>
</div>
{% endif %}
{% else %}
<div class='form-group'>
{{ mfa_view | safe }}
</div>
<div class="row align-items-center p-2">
<button class="btn btn-primary col mr-1" type="submit" name="continue" value="Continue">{{ _("Continue") }}</button>
<button class="btn btn-secondary col" type="submit" name="cancel" value="Cancel">{{ _("Cancel") }}</button>
</div>
{% endif %}
<input type="hidden" name="next" value="{{ next_url | safe }}"/>
</div>
</form>
{% else %}
<div class="form-group pb-3">
<div class="card ">
<div class="card-body">
<div class="alert alert-warning d-flex flex-row mb-0">
<i class="fas fa-lg fa-exclamation-triangle mr-3 align-self-center"></i>
<div class="h-100">
<span class="text-primary text-danger">{{ error_message }}</a></span></div>
</div>
</div>
</div>
</div>
{% 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 %}

View File

@ -1,121 +1,11 @@
{% extends "security/panel.html" %}
{% block panel_image %}
<div class="pr-4">
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Authentication') }}">
</div>
{% endblock %}
{% block panel_title %}{{ _('Authentication') }}{% endblock %}
{% block panel_body %}
<script>
function onMFAChange(val) {
const mfa_methods = {
{% for key in views %}"{{views[key].id | e}}": { "label": "{{ views[key].label | e }}", "view": {{ views[key].view | tojson }}, "script": {{ views[key].script | tojson }} },
{% endfor %}
};
var method = mfa_methods[val];
if (method == undefined)
return false;
window.init_mfa_method = null;
// Reset form classes - to show the 'Validate' button by default.
document.getElementById(
"mfa_form"
).classList = ["show_validate_btn"];
document.getElementById("mfa_method_prepend").setAttribute(
'data-auth-method', val
);
document.getElementById("mfa_view").innerHTML = method.view;
var elem = document.getElementById("mfa_method_script");
if (elem) {
elem.remove();
}
clear_error();
if (method.script) {
var elem = document.createElement('script');
elem.src = method.script;
elem.id = "mfa_method_script";
document.body.appendChild(elem);
}
}
function render_error(err) {
let divElem = document.createElement('div');
divElem.setAttribute(
'style',
'position: fixed; top: 20px; right: 20px; width: 400px; z-index: 9999'
);
divElem.setAttribute('id', 'alert-container');
divElem.innerHTML = [
"<div class='alert alert-danger alert-dismissible fade show'",
" role='alert'>",
" <span id='alert_msg'></span>",
" <button onclick='hide()' type='button' class='close'",
" data-dismiss='alert' aria-label='Close'>",
" <span aria-hidden='true'>×</span>",
" </button>",
"</div>",
].join('')
var alertContainer = document.getElementById("alert-container");
if (alertContainer) {
alertContainer.remove();
}
document.body.appendChild(divElem);
var alertMsg = document.getElementById("alert_msg");
alertMsg.innerHTML = err;
};
function clear_error() {
var alertContainer = document.getElementById("alert-container");
if (alertContainer) {
alertContainer.remove();
}
}
window.onload = () => {
{% for key in views %}{% if views[key].selected is true %}onMFAChange("{{ views[key].id | e }}");{% endif %}{% endfor %}
};
</script>
<style>
form #validate-btn-group {
display: none;
}
form.show_validate_btn #validate-btn-group {
display: block;
}
{% for key in views %}{% if views[key].icon|length > 0 %}
form #mfa_method_prepend[data-auth-method={{views[key].id | e}}] {
background: url({{ views[key].icon }}) 0% 0% no-repeat #eee;
}{% endif %}{% endfor %}
</style>
<form action="{{ url_for('mfa.validate') }}" method="POST"
name="mfa_form" id="mfa_form" class="show_validate_btn">
<div class="form-group">
<div class="from-group">
<div class="input-group pb-2">
<div class="input-group-prepend">
<label class="input-group-text" for="mfa_method" id="mfa_method_prepend">&nbsp;&nbsp;&nbsp;</label>
</div>
<select name="mfa_method" id="mfa_method" class="auth-select custom-select" onchange="onMFAChange(this.value);">
{% for key in views %}<option value="{{views[key].id | e}}" {% if views[key].selected is true %}selected{% endif %}>{{ views[key].label | e }}</option>{% endfor %}
</select>
</div>
<div class="from-group pt-2 pb-2" id="mfa_view"></div>
</div>
<div class="row align-items-center p-2" id='validate-btn-group'>
<button class="col btn btn-primary btn-block btn-validate" type="submit" value="{{ _('Validate') }}">{{ _('Validate') }}</button>
</div>
<div class="form-group text-right p-2"><a class="text-right" role="link" href="{{ logout_url }}">{{ _('Logout') }}</a></div>
</form>
{% 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 %}

View File

@ -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(

View File

@ -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",

View File

@ -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)

View File

@ -28,6 +28,7 @@ samp,
min-height: 100%;
background: $loading-bg;
z-index: 1056;
top: 0;
.pg-sp-content {
position: absolute;

View File

@ -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:

View File

@ -125,7 +125,7 @@ export default function AppMenuBar() {
<div className={classes.gravatar}>
{userMenuInfo.gravatar &&
<img src={userMenuInfo.gravatar} width = "18" height = "18"
alt = "Gravatar image for {{ username }}" />}
alt ={`Gravatar image for ${ userMenuInfo.username }`} />}
{!userMenuInfo.gravatar && <AccountCircleRoundedIcon />}
</div>
{ userMenuInfo.username } ({userMenuInfo.auth_source})

View File

@ -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<SchemaView
formType={'dialog'}
getInitData={() => { /*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
};

View File

@ -292,6 +292,52 @@ export function showChangeServerPassword() {
});
}
export function showChangeUserPassword(url) {
mountDialog(gettext('Change pgAdmin User Password'), (onClose)=> {
const api = getApiInstance();
return <Theme>
<ChangePasswordContent
getInitData={()=>{
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}
/>
</Theme>;
}, undefined, undefined, pgAdmin.Browser.stdH.sm);
}
export function showNamedRestorePoint() {
let title = arguments[0],
nodeData = arguments[1],

View File

@ -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 <Button type="submit" className={classes.button} {...props} />;
}
export default function BasePage({pageImage, title, children, messages}) {
const classes = useStyles();
const snackbar = useSnackbar();
useEffect(()=>{
messages?.forEach((m)=>{
snackbar.enqueueSnackbar(null, {
autoHideDuration: null,
content: (key)=>{
if(Array.isArray(m[0])) m[0] = m[0][0];
const type = Object.values(MESSAGE_TYPE).includes(m[0]) ? m[0] : MESSAGE_TYPE.INFO;
return <FinalNotifyContent>
<NotifierMessage type={type} message={m[1]} closable={true} onClose={()=>{snackbar.closeSnackbar(key);}} style={{maxWidth: '400px'}} />
</FinalNotifyContent>;
}
});
});
}, [messages]);
return (
<Box className={classes.root}>
<Box display="flex" minWidth="80%" gridGap='40px' alignItems="center" padding="20px 80px">
<Box flexGrow={1} height="80%" textAlign="center">
{pageImage}
</Box>
<Box className={classes.pageContent}>
<Box className={classes.item}><div className={classes.logo} /></Box>
<Box className={classes.item}>{title}</Box>
<Box display="flex" flexDirection="column" minHeight={0}>{children}</Box>
</Box>
</Box>
</Box>
);
}
BasePage.propTypes = {
pageImage: CustomPropTypes.children,
title: PropTypes.string,
children: CustomPropTypes.children,
messages: PropTypes.arrayOf(PropTypes.array)
};

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
import ForgotPasswordImage from '../../img/forgot_password.svg?svgr';
import { InputText } from '../components/FormComponents';
import BasePage, { SecurityButton } from './BasePage';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
export default function ForgotPasswordPage({csrfToken, actionUrl, ...props}) {
const [form, setForm] = useState(({email: ''}));
const onTextChange = (n, val)=>{
setForm((prev)=>({...prev, [n]: val}));
};
return (
<BasePage title={gettext('Forget Password')} pageImage={<ForgotPasswordImage style={{height: '100%', width: '100%'}} />} {...props} >
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={actionUrl} method="POST">
<input name="csrf_token" defaultValue={csrfToken} hidden/>
<div>{gettext('Enter the email address for the user account you wish to recover the password for:')}</div>
<InputText name="email" value={form.email} onChange={(v)=>onTextChange('email', v)} placeholder={gettext('Email Address')} autoFocus />
<SecurityButton name="internal_button" value="Recover Password">{gettext('Recover Password')}</SecurityButton>
</form>
</BasePage>
);
}
ForgotPasswordPage.propTypes = {
csrfToken: PropTypes.string,
actionUrl: PropTypes.string,
};

View File

@ -0,0 +1,67 @@
import { Box, Icon } from '@material-ui/core';
import React, { useState } from 'react';
import LoginImage from '../../img/login.svg?svgr';
import { InputSelectNonSearch, InputText, MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents';
import BasePage, { SecurityButton } from './BasePage';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
export default function LoginPage({userLanguage, langOptions, forgotPassUrl, csrfToken, loginUrl, authSources, authSourcesEnum, oauth2Config, loginBanner, ...props}) {
const [form, setForm] = useState(({email: '', password: '', language: userLanguage}));
const showLoginForm = authSources?.includes('internal');
const onTextChange = (n, val)=>{
setForm((prev)=>({...prev, [n]: val}));
};
return (
<>
{loginBanner && <NotifierMessage showIcon={false} closable={false} type={MESSAGE_TYPE.ERROR} message={loginBanner} style={{
position: 'absolute',
width: '80%',
top: '30px',
left: 0,
right: 0,
marginRight: 'auto',
marginLeft: 'auto'
}} textCenter />}
<BasePage title={gettext('Login')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={loginUrl} method="POST">
{showLoginForm &&
<>
<input name="csrf_token" defaultValue={csrfToken} hidden/>
<InputText name="email" value={form.email} onChange={(v)=>onTextChange('email', v)} placeholder={gettext('Email Address / Username')} autoFocus />
<InputText name="password" value={form.password} onChange={(v)=>onTextChange('password', v)} type="password" placeholder={gettext('Password')} />
<Box textAlign="right" marginTop="-10px">
<a style={{color: 'inherit'}} href={forgotPassUrl}>{gettext('Forgotten your password?')}</a>
</Box>
<InputSelectNonSearch name="language" options={langOptions} value={form.language} onChange={(v)=>onTextChange('language', v.target.value)} />
<SecurityButton name="internal_button" value="Login">{gettext('Login')}</SecurityButton>
</>
}
{authSources?.includes?.(authSourcesEnum.OAUTH2) &&
oauth2Config.map((oauth)=>{
return (
<SecurityButton key={oauth.OAUTH2_NAME} name="oauth2_button" value={oauth.OAUTH2_NAME} style={{backgroundColor: oauth.OAUTH2_BUTTON_COLOR}}>
<Icon className={'fab '+oauth.OAUTH2_ICON} style={{ fontSize: '1.5em', marginRight: '8px' }} />{gettext('Login with %s', oauth.OAUTH2_DISPLAY_NAME)}
</SecurityButton>
);
})
}
</form>
</BasePage>
</>
);
}
LoginPage.propTypes = {
userLanguage: PropTypes.string,
langOptions: PropTypes.arrayOf(PropTypes.object),
forgotPassUrl: PropTypes.string,
csrfToken: PropTypes.string,
loginUrl: PropTypes.string,
authSources: PropTypes.arrayOf(PropTypes.string),
authSourcesEnum: PropTypes.object,
oauth2Config: PropTypes.arrayOf(PropTypes.object),
loginBanner: PropTypes.string
};

View File

@ -0,0 +1,112 @@
import { Box } from '@material-ui/core';
import React, { useState } from 'react';
import LoginImage from '../../img/login.svg?svgr';
import { FormNote, InputText } from '../components/FormComponents';
import BasePage, { SecurityButton } from './BasePage';
import { DefaultButton } from '../components/Buttons';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
function EmailRegisterView({mfaView}) {
const [inputEmail, setInputEmail] = useState(mfaView.email_address);
const [inputCode, setInputCode] = useState('');
if(mfaView.email_address_placeholder) {
return <>
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.label}</div>
<div>
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
<input type='hidden' name='validate' value='send_code'/>
</div>
<div>{mfaView.description}</div>
<InputText value={inputEmail} type="email" name="send_to" placeholder={mfaView.email_address_placeholder}
onChange={setInputEmail} required
/>
<FormNote text={mfaView.note} />
</>;
} else if(mfaView.otp_placeholder) {
return <>
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.label}</div>
<div>
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
<input type='hidden' name='validate' value='verify_code'/>
</div>
<div>{mfaView.message}</div>
<InputText value={inputCode} pattern="\d{6}" type="password" name="code" placeholder={mfaView.otp_placeholder}
onChange={setInputCode} required autoComplete="one-time-code"
/>
</>;
}
}
EmailRegisterView.propTypes = {
mfaView: PropTypes.object,
};
function AuthenticatorRegisterView({mfaView}) {
const [inputValue, setInputValue] = useState(mfaView.email_address);
return <>
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.auth_title}</div>
<div>
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
<input type='hidden' name='VALIDATE' value='validate'/>
</div>
<div style={{minHeight: 0, display: 'flex'}}>
<img src={`data:image/jpeg;base64,${mfaView.image}`} style={{maxWidth: '100%', objectFit: 'contain'}} alt={mfaView.qrcode_alt_text} />
</div>
<div>{mfaView.auth_description}</div>
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
onChange={setInputValue}
/>
</>;
}
AuthenticatorRegisterView.propTypes = {
mfaView: PropTypes.object,
};
export default function MfaRegisterPage({actionUrl, mfaList, nextUrl, mfaView, ...props}) {
return (
<>
<BasePage title={gettext('Authentication Registration')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
<form style={{display:'flex', gap:'15px', flexDirection:'column', minHeight: 0}} action={actionUrl} method="POST">
{mfaView ? <>
{mfaView.auth_method == 'email' && <EmailRegisterView mfaView={mfaView} />}
{mfaView.auth_method == 'authenticator' && <AuthenticatorRegisterView mfaView={mfaView} />}
<Box display="flex" gridGap="15px">
<SecurityButton name="continue" value="Continue">{gettext('Continue')}</SecurityButton>
<DefaultButton type="submit" name="cancel" value="Cancel" style={{width: '100%'}}>{gettext('Cancel')}</DefaultButton>
</Box>
</>:<>
{mfaList?.map((m)=>{
return (
<Box display="flex" width="100%" key={m.label}>
<div style={{
width: '10%', mask: `url(${m.icon})`, maskRepeat: 'no-repeat',
WebkitMask: `url(${m.icon})`, WebkitMaskRepeat: 'no-repeat',
backgroundColor: '#fff'
}}>
</div>
<div style={{width: '70%'}}>{m.label}</div>
<div style={{width: '20%'}}>
<SecurityButton name={m.id} value={m.registered ? 'DELETE' : 'SETUP'}>{m.registered ? gettext('Delete') : gettext('Setup')}</SecurityButton>
</div>
</Box>
);
})}
{nextUrl != 'internal' && <SecurityButton value="Continue">{gettext('Continue')}</SecurityButton>}
</>}
<div><input type="hidden" name="next" value={nextUrl}/></div>
</form>
</BasePage>
</>
);
}
MfaRegisterPage.propTypes = {
actionUrl: PropTypes.string,
mfaList: PropTypes.arrayOf(PropTypes.object),
nextUrl: PropTypes.string,
mfaView: PropTypes.object
};

View File

@ -0,0 +1,125 @@
import React, { useState } from 'react';
import LoginImage from '../../img/login.svg?svgr';
import { InputSelect, InputText, MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents';
import BasePage, { SecurityButton } from './BasePage';
import { useDelayedCaller } from '../custom_hooks';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
function EmailValidateView({mfaView, sendEmailUrl, csrfHeader, csrfToken}) {
const [inputValue, setInputValue] = useState('');
const [error, setError] = useState('');
const [sentMessage, setSentMessage] = useState('');
const [sending, setSending] = useState(false);
const [showResend, setShowResend] = useState(false);
const showResendAfter = useDelayedCaller(()=>{
setShowResend(true);
});
const sendCodeToEmail = ()=>{
setSending(true);
let accept = 'text/html; charset=utf-8;';
fetch(sendEmailUrl, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: {
'Accept': accept,
'Content-Type': 'application/json; charset=utf-8;',
[csrfHeader]: csrfToken,
},
redirect: 'follow'
}).then((resp) => {
if (!resp.ok) {
resp.text().then(msg => setError(msg));
return;
}
return resp.json();
}).then((resp) => {
if (!resp)
return;
setSentMessage(resp.message);
showResendAfter(20000);
}).finally(()=>{
setSending(false);
});
};
return <>
<div style={{textAlign: 'center'}}>{mfaView.description}</div>
{sentMessage && <>
<div>{sentMessage}</div>
{showResend && <div>
<span>{gettext('Haven\'t received an email?')} <a style={{color:'inherit', fontWeight: 'bold'}} href="#" onClick={sendCodeToEmail}>{gettext('Send again')}</a></span>
</div>}
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
onChange={setInputValue} autoFocus
/>
<SecurityButton value='Validate'>{gettext('Validate')}</SecurityButton>
</>}
{error && <NotifierMessage message={error} type={MESSAGE_TYPE.ERROR} closable={false} />}
{!sentMessage &&
<SecurityButton type="button" name="send_code" onClick={sendCodeToEmail} disabled={sending}>
{sending ? mfaView.button_label_sending : mfaView.button_label}
</SecurityButton>}
</>;
}
EmailValidateView.propTypes = {
mfaView: PropTypes.object,
sendEmailUrl: PropTypes.string,
csrfHeader: PropTypes.string,
csrfToken: PropTypes.string
};
function AuthenticatorValidateView({mfaView}) {
const [inputValue, setInputValue] = useState('');
return <>
<div>{mfaView.auth_description}</div>
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
onChange={setInputValue} autoFocus
/>
<SecurityButton value='Validate'>{gettext('Validate')}</SecurityButton>
</>;
}
AuthenticatorValidateView.propTypes = {
mfaView: PropTypes.object,
};
export default function MfaValidatePage({actionUrl, views, logoutUrl, sendEmailUrl, csrfHeader, csrfToken, ...props}) {
const [method, setMethod] = useState(Object.values(views).find((v)=>v.selected)?.id);
return (
<>
<BasePage title={gettext('Authentication')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
<form style={{display:'flex', gap:'15px', flexDirection:'column', minHeight: 0}} action={actionUrl} method="POST">
<InputSelect value={method} options={Object.keys(views).map((k)=>({
label: views[k].label,
value: views[k].id,
imageUrl: views[k].icon
}))} onChange={setMethod} controlProps={{
allowClear: false,
}} />
<div><input type='hidden' name='mfa_method' defaultValue={method} /></div>
{method == 'email' && <EmailValidateView mfaView={views[method].view} sendEmailUrl={sendEmailUrl} csrfHeader={csrfHeader} csrfToken={csrfToken} />}
{method == 'authenticator' && <AuthenticatorValidateView mfaView={views[method].view} />}
<div style={{textAlign: 'right'}}>
<a style={{color:'inherit'}} href={logoutUrl}>{gettext('Logout')}</a>
</div>
</form>
</BasePage>
</>
);
}
MfaValidatePage.propTypes = {
actionUrl: PropTypes.string,
views: PropTypes.object,
logoutUrl: PropTypes.string,
sendEmailUrl: PropTypes.string,
csrfHeader: PropTypes.string,
csrfToken: PropTypes.string
};

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
import ForgotPasswordImage from '../../img/forgot_password.svg?svgr';
import { InputText } from '../components/FormComponents';
import BasePage, { SecurityButton } from './BasePage';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
export default function PasswordResetPage({csrfToken, actionUrl, ...props}) {
const [form, setForm] = useState(({password: '', password_confirm: ''}));
const onTextChange = (n, val)=>{
setForm((prev)=>({...prev, [n]: val}));
};
return (
<BasePage title={gettext('Reset Password')} pageImage={<ForgotPasswordImage style={{height: '100%', width: '100%'}} />} {...props} >
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={actionUrl} method="POST">
<input name="csrf_token" defaultValue={csrfToken} hidden/>
<InputText name="password" value={form.password} onChange={(v)=>onTextChange('password', v)} type="password" placeholder={gettext('Password')} autoFocus/>
<InputText name="password_confirm" value={form.password_confirm} onChange={(v)=>onTextChange('password_confirm', v)} type="password" placeholder={gettext('Retype Password')} />
<SecurityButton value="Reset Password">{gettext('Reset Password')}</SecurityButton>
</form>
</BasePage>
);
}
PasswordResetPage.propTypes = {
csrfToken: PropTypes.string,
actionUrl: PropTypes.string
};

View File

@ -0,0 +1,37 @@
import ReactDOM from 'react-dom';
import React from 'react';
import { SnackbarProvider } from 'notistack';
import Theme from '../Theme/index';
import LoginPage from './LoginPage';
import ForgotPasswordPage from './ForgotPasswordPage';
import PasswordResetPage from './PasswordResetPage';
import MfaRegisterPage from './MfaRegisterPage';
import MfaValidatePage from './MfaValidatePage';
window.renderSecurityPage = function(pageName, pageProps, otherProps) {
let ComponentPageMap = {
'login_user': LoginPage,
'forgot_password': ForgotPasswordPage,
'reset_password': PasswordResetPage,
'mfa_register': MfaRegisterPage,
'mfa_validate': MfaValidatePage,
};
const Page = ComponentPageMap[pageName];
if(Page) {
ReactDOM.render(
<Theme>
<SnackbarProvider
maxSnack={5}
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}>
<Page {...pageProps} {...otherProps} />
</SnackbarProvider>
</Theme>
, document.querySelector('#root'));
} else {
ReactDOM.render(
<h1>Invalid Page</h1>
, document.querySelector('#root'));
}
};

View File

@ -760,17 +760,19 @@ const customReactSelectStyles = (theme, readonly) => ({
}),
});
function OptionView({ image, label }) {
function OptionView({ image, imageUrl, label }) {
const classes = useStyles();
return (
<>
{image && <span className={clsx(classes.optionIcon, image)}></span>}
{imageUrl && <img style={{height: '20px', marginRight: '4px'}} src={imageUrl} />}
<span>{label}</span>
</>
);
}
OptionView.propTypes = {
image: PropTypes.string,
imageUrl: PropTypes.string,
label: PropTypes.string,
};
@ -787,7 +789,7 @@ CustomSelectInput.propTypes = {
function CustomSelectOption(props) {
return (
<RSComponents.Option {...props}>
<OptionView image={props.data.image} label={props.data.label} />
<OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />
</RSComponents.Option>
);
}
@ -798,7 +800,7 @@ CustomSelectOption.propTypes = {
function CustomSelectSingleValue(props) {
return (
<RSComponents.SingleValue {...props}>
<OptionView image={props.data.image} label={props.data.label} />
<OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />
</RSComponents.SingleValue>
);
}
@ -1111,9 +1113,11 @@ const useStylesFormFooter = makeStyles((theme) => ({
color: theme.palette.warning.main,
},
message: {
color: theme.palette.text.primary,
marginLeft: theme.spacing(0.5),
},
messageCenter: {
color: theme.palette.text.primary,
margin: 'auto',
},
closeButton: {

View File

@ -76,7 +76,7 @@ export function initializeModalProvider(modalContainer) {
);
}
const FinalNotifyContent = React.forwardRef(({children}, ref) => {
export const FinalNotifyContent = React.forwardRef(({children}, ref) => {
return <SnackbarContent style= {{justifyContent:'end', maxWidth: '700px'}} ref={ref}>{children}</SnackbarContent>;
});
FinalNotifyContent.displayName = 'FinalNotifyContent';

View File

@ -1,12 +1,4 @@
<!DOCTYPE html>
<!--[if lt IE 7]>
<html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]>
<html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]>
<html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@ -17,6 +9,32 @@
<!-- To set pgAdmin4 shortcut icon in browser -->
<link rel="shortcut icon" href="{{ url_for('redirects.favicon') }}"/>
<style>
.pg-sp-container {
position: absolute;
min-width: 100%;
min-height: 100%;
background: rgba(#000,0.6);
z-index: 9999;
top: 0;
}
.pg-sp-container .pg-sp-content {
position: absolute;
width: 100%;
top: 40%;
}
.pg-sp-icon {
background: url("data:image/svg+xml;charset=UTF-8,%3c?xml version='1.0' encoding='utf-8'?%3e%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 38 38' style='enable-background:new 0 0 38 38;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:none;stroke:%23ebeef3;stroke-width:2;%7d .st1%7bfill:none;stroke:%23326690;stroke-width:2;%7d %3c/style%3e%3cg%3e%3cg transform='translate(1 1)'%3e%3ccircle class='st0' cx='18' cy='18' r='18'/%3e%3cpath class='st1' d='M36,18c0-9.9-8.1-18-18-18 '%3e%3canimateTransform accumulate='none' additive='replace' attributeName='transform' calcMode='linear' dur='0.7s' fill='remove' from='0 18 18' repeatCount='indefinite' restart='always' to='360 18 18' type='rotate'%3e%3c/animateTransform%3e%3c/path%3e%3c/g%3e%3c/g%3e%3c/svg%3e") center center no-repeat;
height: 75px;
width: 100%;
text-align: center;
}
.pg-sp-text {
font-size: 20px;
text-align: center;
color: #fff;
}
</style>
<!-- Base template stylesheets -->
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/generated/style.css')}}"/>
@ -31,10 +49,9 @@
/* This is used to change publicPath of webpack at runtime */
window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/";
</script>
<!-- Base template scripts -->
{% if requirejs is defined and requirejs is true %}
<script type="application/javascript"
src="{{ url_for('static', filename='vendor/require/require.js' if config.DEBUG else 'vendor/require/require.min.js') }}"></script>
src="{{ url_for('static', filename='vendor/require/require.js' if config.DEBUG else 'vendor/require/require.min.js') }}"></script>
<!-- Base template scripts -->
<script type="application/javascript">
require.config({
baseUrl: '',
@ -53,18 +70,17 @@
'pgadmin.browser.constants': "{{ url_for('browser.index') }}" + "js/constants",
'pgadmin.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers",
'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user",
'translations': "{{ url_for('tools.index') }}" + "translations"
'translations': "{{ url_for('tools.index') }}" + "translations",
'security.pages': "{{ url_for('static', filename='js/generated/security.pages') }}"
}
});
</script>
{% endif %}
{% if basejs is defined and basejs is true %}
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
<!-- View specified scripts -->
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.others.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script>
{% endif %}
</head>
<body>

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% from "security/fields.html" import render_field_with_errors %}
{% block body %}
<div class="container-fluid change_pass auth_page">
{% include "security/messages.html" %}
<div class="row align-items-center h-100">
<div class="col-md-4 mx-md-auto mx-sm-5 mx-xs-5">
<div class="panel-header h4"><i class="app-icon pg-icon-blue" aria-hidden="true"></i> {{ _('%(appname)s', appname=config.APP_NAME) }}</div>
<div class="panel-body">
<div class="d-block text-color pb-3 h5">{{ _('Password Change') }}</div>
{% if config.SERVER_MODE %}
<form action="{{ url_for('browser.change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
<fieldset>
<legend class="skip-navigation">{{ _('Change Password Form') }}</legend>
{{ render_field_with_errors(change_password_form.password, "password") }}
{{ render_field_with_errors(change_password_form.new_password, "password") }}
{{ render_field_with_errors(change_password_form.new_password_confirm, "password") }}
<input class="btn btn-primary btn-block btn-change-pass" type="submit" value="{{ _('Change Password') }}"
title="{{ _('Change Password') }}" aria-label="{{ _('Change Password') }}>
</fieldset>
</form>
{% endif %}
</div>
</div>
<div class="col-md-4"></div>
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% macro render_field_with_errors(field, type) %}
<div class="form-group mb-3 {% if field.errors %} has-error{% endif %}">
<input class="form-control" placeholder="{{ field.label.text }}" name="{{ field.name }}"
type="{% if type %}{{ type }}{% else %}{{ field.type }}{% endif %}" autofocus>
{% if field.errors %}
{% for error in field.errors %}
<span class="form-text">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
{% endmacro %}
{% macro render_username_with_errors(field, type) %}
<div class="form-group mb-3 {% if field.errors %} has-error{% endif %}">
<input class="form-control" placeholder="{{ field.label.text }} / Username" name="{{ field.name }}"
type="{% if type %}{{ type }}{% else %}{{ field.type }}{% endif %}" autofocus>
{% if field.errors %}
{% for error in field.errors %}
<span class="form-text">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
{% endmacro %}

View File

@ -1,21 +1,8 @@
{% extends "security/panel.html" %}
{% block panel_image %}
<div class="p-5">
<img src="{{ url_for('static', filename='img/forgot_password.svg') }}" alt="{{ _('Forgot Password') }}">
</div>
{% endblock %}
{% block panel_title %}{{ _('Recover Password', appname=config.APP_NAME) }}{% endblock %}
{% block panel_body %}
{% if config.SERVER_MODE %}
<p>{{ _('Enter the email address for the user account you wish to recover the password for:') }}</p>
<form action="{{ url_for('browser.forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
<fieldset>
<legend class="skip-navigation">{{ _('Forget Password Form') }}</legend>
{{ render_field_with_errors(forgot_password_form.email, "text") }}
<input class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Recover Password') }}"
title="{{ _('Recover Password') }}">
</fieldset>
</form>
{% endif %}
{% endblock %}
{% set page_name = 'forgot_password' %}
{% set page_props = {
'actionUrl': url_for('browser.forgot_password'),
'csrfToken': csrf_token(),
} %}
{% extends "security/render_page.html" %}
{% block title %}{{ _('Recover Password') }}{% endblock %}

View File

@ -1,43 +1,22 @@
{% extends "security/panel.html" %}
{% block panel_image %}
<div class="pr-4">
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Login') }}">
</div>
{% endblock %}
{% block panel_title %}{{ _('Login') }}{% endblock %}
{% block panel_body %}
{% if config.SERVER_MODE %}
<form action="{{ url_for('authenticate.login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }}
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
{% set show_login_form = not ((config.OAUTH2 in config.AUTHENTICATION_SOURCES or config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 1
or (config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 2) %}
{% if show_login_form %}
{{ render_username_with_errors(login_user_form.email, "text") }}
{{ render_field_with_errors(login_user_form.password, "password") }}
<button name="internal_button" class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
{% endif %}
<div class="form-group row mb-3 c user-language">
{% if show_login_form %}
<div class="col-7">
<span class="help-block">{{ _('<a href="%(url)s" class="text-white">Forgotten your password</a>?', url=url_for('browser.forgot_password')) }}</span>
</div>
{% endif %}
<div class="{{'col-5' if show_login_form else 'col-12'}}">
<select class="form-control" name="language" value="{{user_language}}">
{% for key, lang in config.LANGUAGES.items() %}
<option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
{% endfor %}
</select>
</div>
</div>
{% if config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES %}
{% for oauth_config in config.OAUTH2_CONFIG %}
<button name="oauth2_button" class="btn btn-primary btn-block btn-oauth" style="background-color: {{oauth_config['OAUTH2_BUTTON_COLOR']}}" value="{{ oauth_config['OAUTH2_NAME'] }}" type="submit">
<i class="fab {{ oauth_config['OAUTH2_ICON'] }} fa-lg mr-2" aria-hidden="true" role="image"></i>
{{ _('Login with %(oauth_name)s', oauth_name=oauth_config['OAUTH2_DISPLAY_NAME']) }}</button>
{% endfor %}
{% endif %}
</form>
{% 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 %}

View File

@ -1,21 +0,0 @@
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
<div style="position: fixed; top: 20px; right: 20px; width: 400px; z-index: 9999">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button onclick="hide()" type="button" class="close" data-dismiss="alert" aria-label="{{ _('Close') }}"><span
aria-hidden="true">&times;</span></button>
</div>
{% endfor %}
</div>
<script>
function hide(){
var target = event.target || event.srcElement;
if (target.type === undefined)
target=target.parentNode;
target.parentNode.classList.remove("show");
}
</script>
{% endif %}
{%- endwith %}

View File

@ -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 %}
<div class="container-fluid h-100 login_page{% if auth_page is defined and auth_page is true %} auth_page{% endif %}">
{% if config.LOGIN_BANNER is defined and config.LOGIN_BANNER != "" %}
<div class="login_banner alert alert-danger" role="alert">
{{ config.LOGIN_BANNER|safe }}
</div>
{% endif %}
{% include "security/messages.html" %}
<div class="row h-100 align-items-center justify-content-center">
<div class="d-none d-md-block col-md-6">{% block panel_image %}{% endblock %}</div>
<div class="col-md-3 panel-container mh-100">
<div class="panel-header h4 pt-2 pb-1 m-0 rounded-top">
<span class="d-flex justify-content-center pgadmin_header_logo"
onclick="return false;"
href="#"
title="{{ _('%(appname)s', appname=config.APP_NAME) }}"
aria-label="{{ _('%(appname)s', appname=config.APP_NAME) }} logo">
<i class="app-icon pg-icon"
aria-hidden="true"
style="width: auto; background-size: auto; min-width: 100px;"></i>
</span>
</div>
<div class="panel-body p-3 overflow-auto" style="max-height: calc(100vh - 3em);">
<div class="d-block text-color pb-2 h4 text-center">{% block panel_title %}{% endblock %}</div>
{% block panel_body %}{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% set other_props = {
'messages': get_flashed_messages(with_categories=true)
} %}
{% block body %}
<style>
#root:not(:empty) + .pg-sp-container {
display: none;
}
</style>
<div id="root" style="height: 100%"></div>
<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="pg-sp-icon"></div>
</div>
</div>
{% 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 %}

View File

@ -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 %}
<form action="{{ url_for('browser.reset_password', token=reset_password_token) }}" method="POST"
name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
<fieldset>
<legend class="skip-navigation">{{ _('Reset Password Form') }}</legend>
{{ render_field_with_errors(reset_password_form.password, "password") }}
{{ render_field_with_errors(reset_password_form.password_confirm, "password") }}
<input class="btn btn-lg btn-success btn-block" type="submit" value="{{ _('Reset Password') }}"
title="{{ _('Reset Password') }}">
</fieldset>
</form>
{% 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 %}

View File

@ -1,7 +0,0 @@
{% block watermark %}
<div style="position: fixed; bottom: 0; right: 0;">
<img src="{{ url_for('static', filename='img/logo-right-256.png') }}"
alt="{{ config.APP_NAME }} {{ _('logo') }}"
>
</div>
{% endblock %}

View File

@ -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
)

View File

@ -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),
)

View File

@ -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
)

View File

@ -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,
)

View File

@ -333,8 +333,6 @@ def panel(trans_id):
"sqleditor/index.html",
title=underscore_unescape(params['title']),
params=json.dumps(params),
requirejs=True,
basejs=True,
)

View File

@ -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.

View File

@ -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'

View File

@ -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(<Theme>
<ForgotPasswordPage {...props}/>
</Theme>);
};
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);
});
});

View File

@ -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(<Theme>
<LoginPage {...props}/>
</Theme>);
};
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);
});
});

View File

@ -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(<Theme>
<MfaRegisterPage {...props}/>
</Theme>);
};
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);
});
});

View File

@ -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(<Theme>
<MfaValidatePage {...props}/>
</Theme>);
};
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);
});
});

View File

@ -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(<Theme>
<PasswordResetPage {...props}/>
</Theme>);
};
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);
});
});

View File

@ -1,4 +1,4 @@
//////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//

View File

@ -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',

View File

@ -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'),