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

pull/6526/head
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 * [kerberos, internal]: pgAdmin will first try to authenticate the user
through kerberos. If that authentication fails, then it will return back 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." credentials for authentication."
"KERBEROS_AUTO_CREATE_USER", "Set the value to *True* if you want to "KERBEROS_AUTO_CREATE_USER", "Set the value to *True* if you want to
automatically create a pgAdmin user corresponding to a successfully automatically create a pgAdmin user corresponding to a successfully

View File

@ -1,16 +1,16 @@
.. _login: .. _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 .. image:: images/login.png
:alt: pgAdmin login dialog :alt: pgAdmin login page
:align: center :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: two ways to authenticate your connection:
- From pgAdmin version 4.21 onwards, support for LDAP authentication - 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 If you have forgotten the email associated with your account, please contact
your administrator. 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 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 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. 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 * [webserver, internal]: pgAdmin will first try to authenticate the user
through webserver. If that authentication fails, then it will return back 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." credentials for authentication."
"WEBSERVER_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically "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. 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.model import db, User
from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect 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 from pgadmin.authenticate.registry import AuthSourceRegistry
MODULE_NAME = 'authenticate' MODULE_NAME = 'authenticate'
@ -132,7 +133,7 @@ def _login():
if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0: if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
flash(gettext('Your account is locked. Please contact the ' flash(gettext('Your account is locked. Please contact the '
'Administrator.'), 'Administrator.'),
'warning') MessageType.WARNING)
logout_user() logout_user()
return redirect(get_post_logout_redirect()) return redirect(get_post_logout_redirect())
@ -158,7 +159,7 @@ def _login():
if flash_login_attempt_error: if flash_login_attempt_error:
error = error + flash_login_attempt_error error = error + flash_login_attempt_error
flash_login_attempt_error = None flash_login_attempt_error = None
flash(error, 'warning') flash(error, MessageType.WARNING)
return redirect(get_post_logout_redirect()) return redirect(get_post_logout_redirect())
@ -175,7 +176,7 @@ def _login():
return redirect('{0}?next={1}'.format(url_for( return redirect('{0}?next={1}'.format(url_for(
'authenticate.kerberos_login'), url_for('browser.index'))) 'authenticate.kerberos_login'), url_for('browser.index')))
flash(msg, 'danger') flash(msg, MessageType.ERROR)
return redirect(get_post_logout_redirect()) return redirect(get_post_logout_redirect())
session['auth_source_manager'] = current_auth_obj session['auth_source_manager'] = current_auth_obj
@ -194,7 +195,7 @@ def _login():
return msg return msg
if 'auth_obj' in session: if 'auth_obj' in session:
session.pop('auth_obj') session.pop('auth_obj')
flash(msg, 'danger') flash(msg, MessageType.ERROR)
form_class = _security.forms.get('login_form').cls form_class = _security.forms.get('login_form').cls
form = form_class() form = form_class()
@ -268,7 +269,7 @@ class AuthSourceManager:
if status: if status:
return True return True
if err_msg: if err_msg:
flash(err_msg, 'warning') flash(err_msg, MessageType.WARNING)
return False return False
def authenticate(self): def authenticate(self):

View File

@ -23,7 +23,7 @@ from flask_security import login_required
import config import config
from pgadmin.model import User from pgadmin.model import User
from pgadmin.tools.user_management import create_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 import PgAdminModule
from pgadmin.utils.ajax import make_json_response, internal_server_error from pgadmin.utils.ajax import make_json_response, internal_server_error
@ -199,7 +199,7 @@ class KerberosAuthentication(BaseAuthentication):
retval = self.__auto_create_user( retval = self.__auto_create_user(
str(negotiate.initiator_name)) str(negotiate.initiator_name))
elif isinstance(negotiate, Exception): elif isinstance(negotiate, Exception):
flash(gettext(negotiate), 'danger') flash(gettext(negotiate), MessageType.ERROR)
retval = [status, retval = [status,
Response(render_template( Response(render_template(
"security/login_user.html", "security/login_user.html",
@ -209,8 +209,8 @@ class KerberosAuthentication(BaseAuthentication):
str(base64.b64encode(negotiate), 'utf-8')) str(base64.b64encode(negotiate), 'utf-8'))
return False, Response("Success", 200, headers) return False, Response("Success", 200, headers)
else: else:
flash(gettext("Kerberos authentication failed." flash(gettext("Kerberos authentication failed. Couldn't find "
" Couldn't find kerberos ticket."), 'danger') "kerberos ticket."), MessageType.ERROR)
headers.add('WWW-Authenticate', 'Negotiate') headers.add('WWW-Authenticate', 'Negotiate')
retval = [False, retval = [False,
Response(render_template( Response(render_template(

View File

@ -24,6 +24,7 @@ from pgadmin.model import UserMFA
from .registry import BaseMFAuth from .registry import BaseMFAuth
from .utils import ValidationException, fetch_auth_option, mfa_add from .utils import ValidationException, fetch_auth_option, mfa_add
from pgadmin.utils.constants import MessageType
_TOTP_AUTH_METHOD = "authenticator" _TOTP_AUTH_METHOD = "authenticator"
@ -119,14 +120,7 @@ class TOTPAuthenticator(BaseMFAuth):
Returns: Returns:
str: Authentication view as a string str: Authentication view as a string
""" """
return ( return dict(
"<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(
auth_description=_( auth_description=_(
"Enter the code shown in your authenticator application for " "Enter the code shown in your authenticator application for "
"TOTP (Time-based One-Time Password)" "TOTP (Time-based One-Time Password)"
@ -162,6 +156,17 @@ class TOTPAuthenticator(BaseMFAuth):
img.save(buffered, format="JPEG") img.save(buffered, format="JPEG")
img_base64 = base64.b64encode(buffered.getvalue()) 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([ return "".join([
"<h5 class='form-group text-center'>{auth_title}</h5>", "<h5 class='form-group text-center'>{auth_title}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>", "<input type='hidden' name='{auth_method}' value='SETUP'/>",
@ -210,13 +215,13 @@ class TOTPAuthenticator(BaseMFAuth):
authenticator_opt = session.get('mfa_authenticator_opt', None) authenticator_opt = session.get('mfa_authenticator_opt', None)
if authenticator_opt is None or \ if authenticator_opt is None or \
pyotp.TOTP(authenticator_opt).verify(code) is False: 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() return self._registration_view()
mfa_add(_TOTP_AUTH_METHOD, authenticator_opt) mfa_add(_TOTP_AUTH_METHOD, authenticator_opt)
flash(_( flash(_(
"TOTP Authenticator registered successfully for authentication." "TOTP Authenticator registered successfully for authentication."
), "success") ), MessageType.SUCCESS)
session.pop('mfa_authenticator_opt', None) session.pop('mfa_authenticator_opt', None)
return None return None

View File

@ -18,6 +18,7 @@ import config
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from .registry import BaseMFAuth from .registry import BaseMFAuth
from .utils import ValidationException, mfa_add, fetch_auth_option from .utils import ValidationException, mfa_add, fetch_auth_option
from pgadmin.utils.constants import MessageType
def __generate_otp() -> str: def __generate_otp() -> str:
@ -154,10 +155,7 @@ def send_email_code() -> Response:
if success is False: if success is False:
return Response(message, http_code, mimetype='text/html') return Response(message, http_code, mimetype='text/html')
return Response(render_template( return dict(message=message)
"mfa/email_code_sent.html", _=_,
message=message,
), http_code, mimetype='text/html')
@pgCSRFProtect.exempt @pgCSRFProtect.exempt
@ -204,28 +202,15 @@ class EmailAuthentication(BaseMFAuth):
def validation_view(self): def validation_view(self):
session.pop("mfa_email_code", None) session.pop("mfa_email_code", None)
return render_template( return dict(
"mfa/email_view.html", _=_ description=_("Verify with Email Authentication"),
button_label=_("Send Code"),
button_label_sending=_("Sending Code...")
) )
def _registration_view(self): def _registration_view(self):
email = getattr(current_user, 'email', '') email = getattr(current_user, 'email', '')
return "\n".join([ return dict(
"<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(
label=email_authentication_label(), label=email_authentication_label(),
auth_method=EMAIL_AUTH_METHOD, auth_method=EMAIL_AUTH_METHOD,
description=_("Enter the email address to send a code"), description=_("Enter the email address to send a code"),
@ -247,20 +232,10 @@ class EmailAuthentication(BaseMFAuth):
) )
if success is False: if success is False:
flash(message, 'danger') flash(message, MessageType.ERROR)
return None return None
return "\n".join([ return dict(
"<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(
label=email_authentication_label(), label=email_authentication_label(),
auth_method=EMAIL_AUTH_METHOD, auth_method=EMAIL_AUTH_METHOD,
message=message, message=message,
@ -282,13 +257,13 @@ class EmailAuthentication(BaseMFAuth):
flash(_( flash(_(
"Email Authentication registered successfully." "Email Authentication registered successfully."
), "success") ), MessageType.SUCCESS)
session.pop('mfa_email_code', None) session.pop('mfa_email_code', None)
return None return None
flash(_('Invalid code'), 'danger') flash(_('Invalid code'), MessageType.ERROR)
return self._registration_view() 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 %} {% set page_name = 'mfa_register' %}
{% extends "security/panel.html" %} {% set page_props = {
{% block panel_image %} 'actionUrl': url_for('mfa.register'),
<div class="pr-4"> 'mfaList': mfa_list,
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Registration') }}"> 'nextUrl': next_url,
</div> 'mfaView': mfa_view,
{% endblock %} 'errorMessage': error_message,
{% block panel_title %}{{ _('Authentication registration') }}{% endblock %} } %}
{% block panel_body %} {% extends "security/render_page.html" %}
<style> {% block title %}{{ _('Authentication Registration') }}{% endblock %}
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 %}

View File

@ -1,121 +1,11 @@
{% extends "security/panel.html" %} {% set page_name = 'mfa_validate' %}
{% block panel_image %} {% set page_props = {
<div class="pr-4"> 'actionUrl': url_for('mfa.validate'),
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Authentication') }}"> 'views': views,
</div> 'logoutUrl': logout_url,
{% endblock %} 'sendEmailUrl': url_for("mfa.send_email_code"),
{% block panel_title %}{{ _('Authentication') }}{% endblock %} 'csrfHeader': current_app.config.get("WTF_CSRF_HEADERS")[0],
{% block panel_body %} 'csrfToken': csrf_token()
<script> } %}
function onMFAChange(val) { {% extends "security/render_page.html" %}
const mfa_methods = { {% block title %}{{ _('Authentication') }}{% endblock %}
{% 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 %}

View File

@ -22,6 +22,7 @@ from pgadmin.utils.ajax import bad_request
from .utils import user_supported_mfa_methods, mfa_user_registered, \ from .utils import user_supported_mfa_methods, mfa_user_registered, \
mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \ mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \
is_mfa_session_authenticated is_mfa_session_authenticated
from pgadmin.utils.constants import MessageType
_INDEX_URL = "browser.index" _INDEX_URL = "browser.index"
@ -118,11 +119,11 @@ def validate_view() -> Response:
"MFA validation failed for the user '{}' with an error: " "MFA validation failed for the user '{}' with an error: "
"{}" "{}"
).format(current_user.username, str(ve))) ).format(current_user.username, str(ve)))
flash(str(ve), "danger") flash(str(ve), MessageType.ERROR)
return_code = 401 return_code = 401
except Exception as ex: except Exception as ex:
current_app.logger.exception(ex) current_app.logger.exception(ex)
flash(str(ex), "danger") flash(str(ex), MessageType.ERROR)
return_code = 500 return_code = 500
mfa_views = { mfa_views = {
@ -166,7 +167,8 @@ def _mfa_registration_view(
if form_data[mfa.name] == 'SETUP': if form_data[mfa.name] == 'SETUP':
if supported_mfa['registered'] is True: 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 None
return mfa.registration_view(form_data) return mfa.registration_view(form_data)
@ -174,13 +176,13 @@ def _mfa_registration_view(
if mfa_delete(mfa.name) is True: if mfa_delete(mfa.name) is True:
flash(_( flash(_(
"'{}' unregistered from the authentication list." "'{}' unregistered from the authentication list."
).format(mfa.label), "success") ).format(mfa.label), MessageType.SUCCESS)
return None return None
flash(_( flash(_(
"'{}' is not found in the authentication list." "'{}' is not found in the authentication list."
).format(mfa.label), "warning") ).format(mfa.label), MessageType.WARNING)
return None return None
@ -255,7 +257,7 @@ def __handle_registration_view_for_post_method(
if view is False: if view is False:
if next_url != 'internal': if next_url != 'internal':
return None, redirect(next_url), None return None, redirect(next_url), None
flash(_("Please close the dialog."), "info") flash(_("Please close the dialog."), MessageType.INFO)
if view is not None: if view is not None:
return None, Response( return None, Response(
@ -336,7 +338,8 @@ def registration_view() -> Response:
) )
elif is_mfa_session_authenticated() is False and \ elif is_mfa_session_authenticated() is False and \
found_one_mfa is True: 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 redirect(login_url("mfa.validate", next_url=next_url))
return Response(render_template( 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.authenticate.internal import BaseAuthentication
from pgadmin.model import User from pgadmin.model import User
from pgadmin.tools.user_management import create_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 import PgAdminModule, get_safe_post_login_redirect
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.model import db from pgadmin.model import db
@ -61,7 +61,7 @@ def init_app(app):
if 'auth_obj' in session: if 'auth_obj' in session:
session.pop('auth_obj') session.pop('auth_obj')
logout_user() logout_user()
flash(msg, 'danger') flash(msg, MessageType.ERROR)
return redirect(get_safe_post_login_redirect()) return redirect(get_safe_post_login_redirect())
@blueprint.route('/logout', endpoint="logout", @blueprint.route('/logout', endpoint="logout",

View File

@ -452,7 +452,7 @@ def check_browser_upgrade():
download_url=data[config.UPGRADE_CHECK_KEY]['download_url'] download_url=data[config.UPGRADE_CHECK_KEY]['download_url']
) )
flash(msg, 'warning') flash(msg, MessageType.WARNING)
@blueprint.route("/") @blueprint.route("/")
@ -487,7 +487,7 @@ def index():
known=browser_known known=browser_known
) )
flash(msg, 'warning') flash(msg, MessageType.WARNING)
# Get the current version info from the website, and flash a message if # Get the current version info from the website, and flash a message if
# the user is out of date, and the check is enabled. # the user is out of date, and the check is enabled.
@ -507,8 +507,6 @@ def index():
response = Response(render_template( response = Response(render_template(
MODULE_NAME + "/index.html", MODULE_NAME + "/index.html",
username=current_user.username, username=current_user.username,
requirejs=True,
basejs=True,
_=gettext _=gettext
)) ))
@ -1061,66 +1059,55 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
def change_password(): def change_password():
"""View function which handles a change password request.""" """View function which handles a change password request."""
has_error = False
form_class = _security.forms.get('change_password_form').cls form_class = _security.forms.get('change_password_form').cls
req_json = request.get_json(silent=True) req_json = request.get_json(silent=True)
if req_json: if not req_json:
form = form_class(MultiDict(req_json))
else:
form = form_class() 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(): if request.get_json(silent=True) is None and errormsg is None:
try: old_key = get_crypt_key()[1]
change_user_password(current_user._get_current_object(), set_crypt_key(form.new_password.data, False)
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 not has_error: from pgadmin.browser.server_groups.servers.utils \
after_this_request(view_commit) import reencrpyt_server_passwords
do_flash(*get_message('PASSWORD_CHANGE')) reencrpyt_server_passwords(
current_user.id, old_key, form.new_password.data)
old_key = get_crypt_key()[1] return redirect(get_url(_security.post_change_view) or
set_crypt_key(form.new_password.data, False) 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 # Only register route if SECURITY_RECOVERABLE is set to True
if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: 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 ' 'Please contact the administrators of this '
'service if you need to reset your password.' 'service if you need to reset your password.'
).format(form.user.auth_source), ).format(form.user.auth_source),
'danger') MessageType.ERROR)
has_error = True has_error = True
if not has_error: if not has_error:
try: try:
@ -1181,7 +1168,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
# covered by SMTPExceptions. # covered by SMTPExceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(SMTP_SOCKET_ERROR).format(e), flash(gettext(SMTP_SOCKET_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
except (SMTPConnectError, SMTPResponseException, except (SMTPConnectError, SMTPResponseException,
SMTPServerDisconnected, SMTPDataError, SMTPHeloError, SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
@ -1191,13 +1178,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
# Handle smtp specific exceptions. # Handle smtp specific exceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(SMTP_ERROR).format(e), flash(gettext(SMTP_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
except Exception as e: except Exception as e:
# Handle other exceptions. # Handle other exceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(PASS_ERROR).format(e), flash(gettext(PASS_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
if request.get_json(silent=True) is None and not has_error: 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. # Handle socket errors which are not covered by SMTPExceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(SMTP_SOCKET_ERROR).format(e), flash(gettext(SMTP_SOCKET_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
except (SMTPConnectError, SMTPResponseException, except (SMTPConnectError, SMTPResponseException,
SMTPServerDisconnected, SMTPDataError, SMTPHeloError, SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
@ -1257,13 +1244,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
# Handle smtp specific exceptions. # Handle smtp specific exceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(SMTP_ERROR).format(e), flash(gettext(SMTP_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
except Exception as e: except Exception as e:
# Handle other exceptions. # Handle other exceptions.
logging.exception(str(e), exc_info=True) logging.exception(str(e), exc_info=True)
flash(gettext(PASS_ERROR).format(e), flash(gettext(PASS_ERROR).format(e),
'danger') MessageType.ERROR)
has_error = True has_error = True
if not has_error: 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' flash(gettext('You successfully reset your password but'
' your account is locked. Please contact ' ' your account is locked. Please contact '
'the Administrator.'), 'the Administrator.'),
'warning') MessageType.WARNING)
return redirect(get_post_logout_redirect()) return redirect(get_post_logout_redirect())
do_flash(*get_message('PASSWORD_RESET')) do_flash(*get_message('PASSWORD_RESET'))
login_user(user) login_user(user)

View File

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

View File

@ -67,6 +67,10 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
def runTest(self): def runTest(self):
"""This function checks spnego/kerberos login functionality.""" """This function checks spnego/kerberos login functionality."""
if self.flag == 1: 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() self.test_unauthorized()
elif self.flag == 2: elif self.flag == 2:
if app_config.SERVER_MODE is False: if app_config.SERVER_MODE is False:

View File

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

View File

@ -15,7 +15,7 @@ import BaseUISchema from '../SchemaView/base_schema.ui';
import SchemaView from '../SchemaView'; import SchemaView from '../SchemaView';
class ChangePasswordSchema extends BaseUISchema { class ChangePasswordSchema extends BaseUISchema {
constructor(user, isPgpassFileUsed) { constructor(user, isPgpassFileUsed, hasCsrfToken=false, showUser=true) {
super({ super({
user: user, user: user,
password: '', password: '',
@ -23,13 +23,15 @@ class ChangePasswordSchema extends BaseUISchema {
confirmPassword: '' confirmPassword: ''
}); });
this.isPgpassFileUsed = isPgpassFileUsed; this.isPgpassFileUsed = isPgpassFileUsed;
this.hasCsrfToken = hasCsrfToken;
this.showUser = showUser;
} }
get baseFields() { get baseFields() {
let self = this; let self = this;
return [ 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', id: 'password', label: gettext('Current Password'), type: 'password',
disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true, disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true,
@ -42,14 +44,18 @@ class ChangePasswordSchema extends BaseUISchema {
controlProps: { controlProps: {
maxLength: null maxLength: null
} }
}, { }, {
id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password', id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password',
noEmpty: true, noEmpty: true,
controlProps: { controlProps: {
maxLength: null maxLength: null
} }
} }
]; ].concat(this.hasCsrfToken ? [
{
id: 'csrf_token', visible: false, type: 'text'
}
]: []);
} }
validate(state, setError) { 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(); const classes = useStyles();
return<SchemaView return<SchemaView
formType={'dialog'} formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }} getInitData={getInitData}
schema={new ChangePasswordSchema(userName, isPgpassFileUsed)} schema={new ChangePasswordSchema('', false, hasCsrfToken, showUser)}
viewHelperProps={{ viewHelperProps={{
mode: 'create', mode: 'create',
}} }}
@ -96,5 +103,8 @@ ChangePasswordContent.propTypes = {
onSave: PropTypes.func, onSave: PropTypes.func,
onClose: PropTypes.func, onClose: PropTypes.func,
userName: PropTypes.string, 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() { export function showNamedRestorePoint() {
let title = arguments[0], let title = arguments[0],
nodeData = arguments[1], 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(); const classes = useStyles();
return ( return (
<> <>
{image && <span className={clsx(classes.optionIcon, image)}></span>} {image && <span className={clsx(classes.optionIcon, image)}></span>}
{imageUrl && <img style={{height: '20px', marginRight: '4px'}} src={imageUrl} />}
<span>{label}</span> <span>{label}</span>
</> </>
); );
} }
OptionView.propTypes = { OptionView.propTypes = {
image: PropTypes.string, image: PropTypes.string,
imageUrl: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
}; };
@ -787,7 +789,7 @@ CustomSelectInput.propTypes = {
function CustomSelectOption(props) { function CustomSelectOption(props) {
return ( return (
<RSComponents.Option {...props}> <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> </RSComponents.Option>
); );
} }
@ -798,7 +800,7 @@ CustomSelectOption.propTypes = {
function CustomSelectSingleValue(props) { function CustomSelectSingleValue(props) {
return ( return (
<RSComponents.SingleValue {...props}> <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> </RSComponents.SingleValue>
); );
} }
@ -1111,9 +1113,11 @@ const useStylesFormFooter = makeStyles((theme) => ({
color: theme.palette.warning.main, color: theme.palette.warning.main,
}, },
message: { message: {
color: theme.palette.text.primary,
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),
}, },
messageCenter: { messageCenter: {
color: theme.palette.text.primary,
margin: 'auto', margin: 'auto',
}, },
closeButton: { 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>; return <SnackbarContent style= {{justifyContent:'end', maxWidth: '700px'}} ref={ref}>{children}</SnackbarContent>;
}); });
FinalNotifyContent.displayName = 'FinalNotifyContent'; FinalNotifyContent.displayName = 'FinalNotifyContent';

View File

@ -1,12 +1,4 @@
<!DOCTYPE html> <!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> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@ -17,6 +9,32 @@
<!-- To set pgAdmin4 shortcut icon in browser --> <!-- To set pgAdmin4 shortcut icon in browser -->
<link rel="shortcut icon" href="{{ url_for('redirects.favicon') }}"/> <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 --> <!-- Base template stylesheets -->
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/generated/style.css')}}"/> <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 */ /* This is used to change publicPath of webpack at runtime */
window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/"; window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/";
</script> </script>
<!-- Base template scripts -->
{% if requirejs is defined and requirejs is true %}
<script type="application/javascript" <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"> <script type="application/javascript">
require.config({ require.config({
baseUrl: '', baseUrl: '',
@ -53,18 +70,17 @@
'pgadmin.browser.constants': "{{ url_for('browser.index') }}" + "js/constants", 'pgadmin.browser.constants': "{{ url_for('browser.index') }}" + "js/constants",
'pgadmin.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers", 'pgadmin.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers",
'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user", '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> </script>
{% endif %} <script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
{% if basejs is defined and basejs is true %}
<!-- View specified scripts --> <!-- 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.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/vendor.others.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script> <script type="application/javascript" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script>
{% endif %}
</head> </head>
<body> <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" %} {% set page_name = 'forgot_password' %}
{% block panel_image %} {% set page_props = {
<div class="p-5"> 'actionUrl': url_for('browser.forgot_password'),
<img src="{{ url_for('static', filename='img/forgot_password.svg') }}" alt="{{ _('Forgot Password') }}"> 'csrfToken': csrf_token(),
</div> } %}
{% endblock %} {% extends "security/render_page.html" %}
{% block panel_title %}{{ _('Recover Password', appname=config.APP_NAME) }}{% endblock %} {% block title %}{{ _('Recover Password') }}{% 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 %}

View File

@ -1,43 +1,22 @@
{% extends "security/panel.html" %} {% set page_name = 'login_user' %}
{% block panel_image %} {% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
<div class="pr-4"> {% set ns = namespace(langOptions=[]) %}
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Login') }}"> {% for key, lang in config.LANGUAGES.items() %}
</div> {% set _ = ns.langOptions.append({'value': key, 'label': lang}) %}
{% endblock %} {% endfor %}
{% block panel_title %}{{ _('Login') }}{% endblock %} {% set page_props = {
{% block panel_body %} 'userLanguage': user_language,
{% if config.SERVER_MODE %} 'langOptions': ns.langOptions,
<form action="{{ url_for('authenticate.login') }}" method="POST" name="login_user_form"> 'forgotPassUrl': url_for('browser.forgot_password'),
{{ login_user_form.hidden_tag() }} 'loginUrl': url_for('authenticate.login'),
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %} 'csrfToken': csrf_token(),
{% set show_login_form = not ((config.OAUTH2 in config.AUTHENTICATION_SOURCES or config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 1 'authSources': config.AUTHENTICATION_SOURCES,
or (config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 2) %} 'authSourcesEnum': {
{% if show_login_form %} 'OAUTH2': config.OAUTH2,
{{ render_username_with_errors(login_user_form.email, "text") }} 'KERBEROS': config.KERBEROS,
{{ 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> 'oauth2Config': config.OAUTH2_CONFIG,
{% endif %} 'loginBanner': config.LOGIN_BANNER|safe
<div class="form-group row mb-3 c user-language"> } %}
{% if show_login_form %} {% extends "security/render_page.html" %}
<div class="col-7"> {% block title %}{{ _('Login') }}{% endblock %}
<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 %}

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" %} {% set page_name = 'reset_password' %}
{% block panel_title %}{{ _('%(appname)s Password Reset', appname=config.APP_NAME) }}{% endblock %} {% set page_props = {
{% block panel_body %} 'actionUrl': url_for('browser.reset_password', token=reset_password_token),
{% if config.SERVER_MODE %} 'csrfToken': csrf_token(),
<form action="{{ url_for('browser.reset_password', token=reset_password_token) }}" method="POST" } %}
name="reset_password_form"> {% extends "security/render_page.html" %}
{{ reset_password_form.hidden_tag() }} {% block title %}{{ _('Reset Password') }}{% endblock %}
<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 %}

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, is_linux=is_linux_platform,
client_platform=user_agent.platform, client_platform=user_agent.platform,
function_name_with_arguments=function_name_with_arguments, function_name_with_arguments=function_name_with_arguments,
requirejs=True,
basejs=True,
layout=layout layout=layout
) )

View File

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

View File

@ -113,8 +113,6 @@ def panel(trans_id):
title=underscore_unescape(params['title']), title=underscore_unescape(params['title']),
theme=params['theme'], theme=params['theme'],
o_db_name=o_db_name, o_db_name=o_db_name,
requirejs=True,
basejs=True,
platform=_platform platform=_platform
) )

View File

@ -127,8 +127,6 @@ def panel(trans_id, editor_title):
"schema_diff/index.html", "schema_diff/index.html",
_=gettext, _=gettext,
trans_id=trans_id, trans_id=trans_id,
requirejs=True,
basejs=True,
editor_title=editor_title, editor_title=editor_title,
) )

View File

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

View File

@ -9,10 +9,9 @@
import pgAdmin from 'sources/pgadmin'; import pgAdmin from 'sources/pgadmin';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { showUrlDialog } from '../../../../static/js/Dialogs/index'; import { showChangeUserPassword, showUrlDialog } from '../../../../static/js/Dialogs/index';
import { showUserManagement } from './UserManagementDialog'; import { showUserManagement } from './UserManagementDialog';
class UserManagement { class UserManagement {
static instance; static instance;
@ -31,7 +30,7 @@ class UserManagement {
// This is a callback function to show change user dialog. // This is a callback function to show change user dialog.
change_password(url) { 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. // 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_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}'
KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}' KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}'
KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}' 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 // pgAdmin 4 - PostgreSQL Tools
// //

View File

@ -46,7 +46,7 @@ const providePlugin = new webpack.ProvidePlugin({
'moment': 'moment', 'moment': 'moment',
'window.moment':'moment', 'window.moment':'moment',
process: 'process/browser', process: 'process/browser',
Buffer: ['buffer', 'Buffer'], Buffer: ['buffer', 'Buffer']
}); });
// Helps in debugging each single file, it extracts the module files // Helps in debugging each single file, it extracts the module files
@ -361,6 +361,7 @@ module.exports = [{
entry: { entry: {
'app.bundle': sourceDir + '/bundle/app.js', 'app.bundle': sourceDir + '/bundle/app.js',
codemirror: sourceDir + '/bundle/codemirror.js', codemirror: sourceDir + '/bundle/codemirror.js',
'security.pages': 'security.pages',
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js', sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/index.js', schema_diff: './pgadmin/tools/schema_diff/static/js/index.js',
erd_tool: './pgadmin/tools/erd/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'), 'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
'tools': path.join(__dirname, './pgadmin/tools/'), 'tools': path.join(__dirname, './pgadmin/tools/'),
'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'), 'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'),
'security.pages': path.join(__dirname, './pgadmin/static/js/SecurityPages/index.jsx'),
// Vendor JS // Vendor JS
'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker.min'), 'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker.min'),