Remove Bootstrap and jQuery from authentication pages and rewrite them in ReactJS. #6295
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 113 KiB |
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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"> </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 %}
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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)
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
};
|
|
@ -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: {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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 %}
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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">×</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 %}
|
|
|
@ -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 %}
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
//////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// pgAdmin 4 - PostgreSQL Tools
|
// pgAdmin 4 - PostgreSQL Tools
|
||||||
//
|
//
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'),
|
||||||
|
|