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
|
||||
through kerberos. If that authentication fails, then it will return back
|
||||
to the login dialog where you need to provide internal pgAdmin user
|
||||
to the login page where you need to provide internal pgAdmin user
|
||||
credentials for authentication."
|
||||
"KERBEROS_AUTO_CREATE_USER", "Set the value to *True* if you want to
|
||||
automatically create a pgAdmin user corresponding to a successfully
|
||||
|
@ -1,16 +1,16 @@
|
||||
.. _login:
|
||||
|
||||
*********************
|
||||
`Login Dialog`:index:
|
||||
`Login Page`:index:
|
||||
*********************
|
||||
|
||||
Use the *Login* dialog to log in to pgAdmin:
|
||||
Use the *Login* page to log in to pgAdmin:
|
||||
|
||||
.. image:: images/login.png
|
||||
:alt: pgAdmin login dialog
|
||||
:alt: pgAdmin login page
|
||||
:align: center
|
||||
|
||||
Use the fields in the *Login* dialog to authenticate your connection. There are
|
||||
Use the fields in the *Login* page to authenticate your connection. There are
|
||||
two ways to authenticate your connection:
|
||||
|
||||
- From pgAdmin version 4.21 onwards, support for LDAP authentication
|
||||
@ -52,7 +52,7 @@ to launch a password recovery utility.
|
||||
If you have forgotten the email associated with your account, please contact
|
||||
your administrator.
|
||||
|
||||
Please note that your LDAP password cannot be recovered using this dialog. If
|
||||
Please note that your LDAP password cannot be recovered using this page. If
|
||||
you enter your LDAP username in the *Email Address/Username* field, and then
|
||||
enter your email to recover your password, an error message will be displayed
|
||||
asking you to contact the LDAP administrator to recover your LDAP password.
|
||||
|
@ -27,7 +27,7 @@ and modify the values for the following parameters:
|
||||
|
||||
* [‘webserver’, ‘internal’]: pgAdmin will first try to authenticate the user
|
||||
through webserver. If that authentication fails, then it will return back
|
||||
to the login dialog where you need to provide internal pgAdmin user
|
||||
to the login page where you need to provide internal pgAdmin user
|
||||
credentials for authentication."
|
||||
"WEBSERVER_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically
|
||||
create a pgAdmin user corresponding to a successfully authenticated Webserver user.
|
||||
|
@ -27,7 +27,8 @@ from flask_socketio import disconnect, ConnectionRefusedError
|
||||
|
||||
from pgadmin.model import db, User
|
||||
from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect
|
||||
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP
|
||||
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP,\
|
||||
MessageType
|
||||
from pgadmin.authenticate.registry import AuthSourceRegistry
|
||||
|
||||
MODULE_NAME = 'authenticate'
|
||||
@ -132,7 +133,7 @@ def _login():
|
||||
if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
|
||||
flash(gettext('Your account is locked. Please contact the '
|
||||
'Administrator.'),
|
||||
'warning')
|
||||
MessageType.WARNING)
|
||||
logout_user()
|
||||
return redirect(get_post_logout_redirect())
|
||||
|
||||
@ -158,7 +159,7 @@ def _login():
|
||||
if flash_login_attempt_error:
|
||||
error = error + flash_login_attempt_error
|
||||
flash_login_attempt_error = None
|
||||
flash(error, 'warning')
|
||||
flash(error, MessageType.WARNING)
|
||||
|
||||
return redirect(get_post_logout_redirect())
|
||||
|
||||
@ -175,7 +176,7 @@ def _login():
|
||||
return redirect('{0}?next={1}'.format(url_for(
|
||||
'authenticate.kerberos_login'), url_for('browser.index')))
|
||||
|
||||
flash(msg, 'danger')
|
||||
flash(msg, MessageType.ERROR)
|
||||
return redirect(get_post_logout_redirect())
|
||||
|
||||
session['auth_source_manager'] = current_auth_obj
|
||||
@ -194,7 +195,7 @@ def _login():
|
||||
return msg
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
flash(msg, 'danger')
|
||||
flash(msg, MessageType.ERROR)
|
||||
form_class = _security.forms.get('login_form').cls
|
||||
form = form_class()
|
||||
|
||||
@ -268,7 +269,7 @@ class AuthSourceManager:
|
||||
if status:
|
||||
return True
|
||||
if err_msg:
|
||||
flash(err_msg, 'warning')
|
||||
flash(err_msg, MessageType.WARNING)
|
||||
return False
|
||||
|
||||
def authenticate(self):
|
||||
|
@ -23,7 +23,7 @@ from flask_security import login_required
|
||||
import config
|
||||
from pgadmin.model import User
|
||||
from pgadmin.tools.user_management import create_user
|
||||
from pgadmin.utils.constants import KERBEROS
|
||||
from pgadmin.utils.constants import KERBEROS, MessageType
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
||||
|
||||
@ -199,7 +199,7 @@ class KerberosAuthentication(BaseAuthentication):
|
||||
retval = self.__auto_create_user(
|
||||
str(negotiate.initiator_name))
|
||||
elif isinstance(negotiate, Exception):
|
||||
flash(gettext(negotiate), 'danger')
|
||||
flash(gettext(negotiate), MessageType.ERROR)
|
||||
retval = [status,
|
||||
Response(render_template(
|
||||
"security/login_user.html",
|
||||
@ -209,8 +209,8 @@ class KerberosAuthentication(BaseAuthentication):
|
||||
str(base64.b64encode(negotiate), 'utf-8'))
|
||||
return False, Response("Success", 200, headers)
|
||||
else:
|
||||
flash(gettext("Kerberos authentication failed."
|
||||
" Couldn't find kerberos ticket."), 'danger')
|
||||
flash(gettext("Kerberos authentication failed. Couldn't find "
|
||||
"kerberos ticket."), MessageType.ERROR)
|
||||
headers.add('WWW-Authenticate', 'Negotiate')
|
||||
retval = [False,
|
||||
Response(render_template(
|
||||
|
@ -24,6 +24,7 @@ from pgadmin.model import UserMFA
|
||||
|
||||
from .registry import BaseMFAuth
|
||||
from .utils import ValidationException, fetch_auth_option, mfa_add
|
||||
from pgadmin.utils.constants import MessageType
|
||||
|
||||
|
||||
_TOTP_AUTH_METHOD = "authenticator"
|
||||
@ -119,14 +120,7 @@ class TOTPAuthenticator(BaseMFAuth):
|
||||
Returns:
|
||||
str: Authentication view as a string
|
||||
"""
|
||||
return (
|
||||
"<div class='form-group'>{auth_description}</div>"
|
||||
"<div class='form-group'>"
|
||||
" <input class='form-control' placeholder='{otp_placeholder}'"
|
||||
" name='code' type='password' autofocus='' pattern='\\d*'"
|
||||
" autocomplete='one-time-code' require/>"
|
||||
"</div>"
|
||||
).format(
|
||||
return dict(
|
||||
auth_description=_(
|
||||
"Enter the code shown in your authenticator application for "
|
||||
"TOTP (Time-based One-Time Password)"
|
||||
@ -162,6 +156,17 @@ class TOTPAuthenticator(BaseMFAuth):
|
||||
img.save(buffered, format="JPEG")
|
||||
img_base64 = base64.b64encode(buffered.getvalue())
|
||||
|
||||
return dict(
|
||||
auth_title=_(_TOTP_AUTHENTICATOR),
|
||||
auth_method=_TOTP_AUTH_METHOD,
|
||||
image=img_base64.decode("utf-8"),
|
||||
qrcode_alt_text=_("TOTP Authenticator QRCode"),
|
||||
auth_description=_(
|
||||
"Scan the QR code and the enter the code from the "
|
||||
"TOTP Authenticator application"
|
||||
), otp_placeholder=_("Enter code")
|
||||
)
|
||||
|
||||
return "".join([
|
||||
"<h5 class='form-group text-center'>{auth_title}</h5>",
|
||||
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
|
||||
@ -210,13 +215,13 @@ class TOTPAuthenticator(BaseMFAuth):
|
||||
authenticator_opt = session.get('mfa_authenticator_opt', None)
|
||||
if authenticator_opt is None or \
|
||||
pyotp.TOTP(authenticator_opt).verify(code) is False:
|
||||
flash(_("Failed to validate the code"), "danger")
|
||||
flash(_("Failed to validate the code"), MessageType.ERROR)
|
||||
return self._registration_view()
|
||||
|
||||
mfa_add(_TOTP_AUTH_METHOD, authenticator_opt)
|
||||
flash(_(
|
||||
"TOTP Authenticator registered successfully for authentication."
|
||||
), "success")
|
||||
), MessageType.SUCCESS)
|
||||
session.pop('mfa_authenticator_opt', None)
|
||||
|
||||
return None
|
||||
|
@ -18,6 +18,7 @@ import config
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
from .registry import BaseMFAuth
|
||||
from .utils import ValidationException, mfa_add, fetch_auth_option
|
||||
from pgadmin.utils.constants import MessageType
|
||||
|
||||
|
||||
def __generate_otp() -> str:
|
||||
@ -154,10 +155,7 @@ def send_email_code() -> Response:
|
||||
if success is False:
|
||||
return Response(message, http_code, mimetype='text/html')
|
||||
|
||||
return Response(render_template(
|
||||
"mfa/email_code_sent.html", _=_,
|
||||
message=message,
|
||||
), http_code, mimetype='text/html')
|
||||
return dict(message=message)
|
||||
|
||||
|
||||
@pgCSRFProtect.exempt
|
||||
@ -204,28 +202,15 @@ class EmailAuthentication(BaseMFAuth):
|
||||
|
||||
def validation_view(self):
|
||||
session.pop("mfa_email_code", None)
|
||||
return render_template(
|
||||
"mfa/email_view.html", _=_
|
||||
return dict(
|
||||
description=_("Verify with Email Authentication"),
|
||||
button_label=_("Send Code"),
|
||||
button_label_sending=_("Sending Code...")
|
||||
)
|
||||
|
||||
def _registration_view(self):
|
||||
email = getattr(current_user, 'email', '')
|
||||
return "\n".join([
|
||||
"<h5 class='form-group text-center'>{label}</h5>",
|
||||
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
|
||||
"<input type='hidden' name='validate' value='send_code'/>",
|
||||
"<div class='form-group pt-3'>{description}</div>",
|
||||
"<div class='form-group'>",
|
||||
" <input class='form-control' name='send_to' type='email'",
|
||||
" placeholder='{email_address_placeholder}'",
|
||||
" autofocus='' value='{email_address}' required",
|
||||
" pattern='[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{{2,}}$'/>",
|
||||
"</div></div>",
|
||||
"<div class='alert alert-warning alert-dismissible fade show'",
|
||||
" role='alert'>",
|
||||
" <strong>{note_label}:</strong><span>{note}</span>",
|
||||
"</div>",
|
||||
]).format(
|
||||
return dict(
|
||||
label=email_authentication_label(),
|
||||
auth_method=EMAIL_AUTH_METHOD,
|
||||
description=_("Enter the email address to send a code"),
|
||||
@ -247,20 +232,10 @@ class EmailAuthentication(BaseMFAuth):
|
||||
)
|
||||
|
||||
if success is False:
|
||||
flash(message, 'danger')
|
||||
flash(message, MessageType.ERROR)
|
||||
return None
|
||||
|
||||
return "\n".join([
|
||||
"<h5 class='form-group text-center'>{label}</h5>",
|
||||
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
|
||||
"<input type='hidden' name='validate' value='verify_code'/>",
|
||||
"<div class='form-group pt-3'>{message}</div>",
|
||||
"<div class='form-group'>",
|
||||
" <input class='form-control' placeholder='{otp_placeholder}'",
|
||||
" pattern='\\d{{6}}' type='password' autofocus=''",
|
||||
" autocomplete='one-time-code' name='code' require>",
|
||||
"</div>",
|
||||
]).format(
|
||||
return dict(
|
||||
label=email_authentication_label(),
|
||||
auth_method=EMAIL_AUTH_METHOD,
|
||||
message=message,
|
||||
@ -282,13 +257,13 @@ class EmailAuthentication(BaseMFAuth):
|
||||
|
||||
flash(_(
|
||||
"Email Authentication registered successfully."
|
||||
), "success")
|
||||
), MessageType.SUCCESS)
|
||||
|
||||
session.pop('mfa_email_code', None)
|
||||
|
||||
return None
|
||||
|
||||
flash(_('Invalid code'), 'danger')
|
||||
flash(_('Invalid code'), MessageType.ERROR)
|
||||
|
||||
return self._registration_view()
|
||||
|
||||
|
@ -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 %}
|
||||
{% extends "security/panel.html" %}
|
||||
{% block panel_image %}
|
||||
<div class="pr-4">
|
||||
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Registration') }}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block panel_title %}{{ _('Authentication registration') }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
<style>
|
||||
|
||||
div.form-group > label > .icon {
|
||||
min-width: 30px;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
{% if error_message is none %}
|
||||
|
||||
{% for mfa in mfa_list %}{% if mfa.icon|length > 0 %}
|
||||
|
||||
div#mfa-{{mfa.id | e}} .icon {
|
||||
background: url({{ mfa.icon }}) 0% 0% no-repeat transparent;
|
||||
min-height: 30px;
|
||||
min-width: 30px;
|
||||
}{% endif %}{% endfor %}
|
||||
</style>
|
||||
|
||||
<form id='mfa_view' method='post'
|
||||
action='{{ url_for("mfa.register") }}'>
|
||||
<div class='form-group'>
|
||||
{% if mfa_view is not defined or mfa_view is none %}
|
||||
<div class='form-group'>
|
||||
{% for mfa in mfa_list %}
|
||||
<div id="mfa-{{ mfa.id }}">
|
||||
<label class="my-1 d-flex align-items-center">
|
||||
<i class="fas mr-2 icon"></i>
|
||||
<span>{{ mfa.label | safe }}</span>
|
||||
<span class="ml-auto">
|
||||
<button class='btn btn-primary btn-block btn-validate' name='{{ mfa.id }}'
|
||||
type='submit'
|
||||
value='{% if mfa.registered %}DELETE{% else %}SETUP{% endif %}'>{% if mfa.registered %}{{ _("Delete") }}{% else %}{{ _("Setup") }}{% endif %}</button>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if next_url != 'internal' %}
|
||||
<div class="row align-items-center p-2">
|
||||
<button class='btn btn-primary btn-block btn-validate col' type='submit'
|
||||
value='{{ _("Continue") }}'>{{ _("Continue") }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class='form-group'>
|
||||
{{ mfa_view | safe }}
|
||||
</div>
|
||||
<div class="row align-items-center p-2">
|
||||
<button class="btn btn-primary col mr-1" type="submit" name="continue" value="Continue">{{ _("Continue") }}</button>
|
||||
<button class="btn btn-secondary col" type="submit" name="cancel" value="Cancel">{{ _("Cancel") }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="hidden" name="next" value="{{ next_url | safe }}"/>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="form-group pb-3">
|
||||
<div class="card ">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning d-flex flex-row mb-0">
|
||||
<i class="fas fa-lg fa-exclamation-triangle mr-3 align-self-center"></i>
|
||||
<div class="h-100">
|
||||
<span class="text-primary text-danger">{{ error_message }}</a></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% set page_name = 'mfa_register' %}
|
||||
{% set page_props = {
|
||||
'actionUrl': url_for('mfa.register'),
|
||||
'mfaList': mfa_list,
|
||||
'nextUrl': next_url,
|
||||
'mfaView': mfa_view,
|
||||
'errorMessage': error_message,
|
||||
} %}
|
||||
{% extends "security/render_page.html" %}
|
||||
{% block title %}{{ _('Authentication Registration') }}{% endblock %}
|
||||
|
@ -1,121 +1,11 @@
|
||||
{% extends "security/panel.html" %}
|
||||
{% block panel_image %}
|
||||
<div class="pr-4">
|
||||
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Authentication') }}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block panel_title %}{{ _('Authentication') }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
<script>
|
||||
function onMFAChange(val) {
|
||||
const mfa_methods = {
|
||||
{% for key in views %}"{{views[key].id | e}}": { "label": "{{ views[key].label | e }}", "view": {{ views[key].view | tojson }}, "script": {{ views[key].script | tojson }} },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
var method = mfa_methods[val];
|
||||
|
||||
if (method == undefined)
|
||||
return false;
|
||||
|
||||
window.init_mfa_method = null;
|
||||
|
||||
// Reset form classes - to show the 'Validate' button by default.
|
||||
document.getElementById(
|
||||
"mfa_form"
|
||||
).classList = ["show_validate_btn"];
|
||||
document.getElementById("mfa_method_prepend").setAttribute(
|
||||
'data-auth-method', val
|
||||
);
|
||||
document.getElementById("mfa_view").innerHTML = method.view;
|
||||
var elem = document.getElementById("mfa_method_script");
|
||||
|
||||
if (elem) {
|
||||
elem.remove();
|
||||
}
|
||||
clear_error();
|
||||
|
||||
if (method.script) {
|
||||
var elem = document.createElement('script');
|
||||
elem.src = method.script;
|
||||
elem.id = "mfa_method_script";
|
||||
document.body.appendChild(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function render_error(err) {
|
||||
let divElem = document.createElement('div');
|
||||
|
||||
divElem.setAttribute(
|
||||
'style',
|
||||
'position: fixed; top: 20px; right: 20px; width: 400px; z-index: 9999'
|
||||
);
|
||||
divElem.setAttribute('id', 'alert-container');
|
||||
|
||||
divElem.innerHTML = [
|
||||
"<div class='alert alert-danger alert-dismissible fade show'",
|
||||
" role='alert'>",
|
||||
" <span id='alert_msg'></span>",
|
||||
" <button onclick='hide()' type='button' class='close'",
|
||||
" data-dismiss='alert' aria-label='Close'>",
|
||||
" <span aria-hidden='true'>×</span>",
|
||||
" </button>",
|
||||
"</div>",
|
||||
].join('')
|
||||
|
||||
var alertContainer = document.getElementById("alert-container");
|
||||
if (alertContainer) {
|
||||
alertContainer.remove();
|
||||
}
|
||||
document.body.appendChild(divElem);
|
||||
var alertMsg = document.getElementById("alert_msg");
|
||||
|
||||
alertMsg.innerHTML = err;
|
||||
};
|
||||
|
||||
function clear_error() {
|
||||
var alertContainer = document.getElementById("alert-container");
|
||||
if (alertContainer) {
|
||||
alertContainer.remove();
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
{% for key in views %}{% if views[key].selected is true %}onMFAChange("{{ views[key].id | e }}");{% endif %}{% endfor %}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
form #validate-btn-group {
|
||||
display: none;
|
||||
}
|
||||
form.show_validate_btn #validate-btn-group {
|
||||
display: block;
|
||||
}
|
||||
|
||||
{% for key in views %}{% if views[key].icon|length > 0 %}
|
||||
|
||||
form #mfa_method_prepend[data-auth-method={{views[key].id | e}}] {
|
||||
background: url({{ views[key].icon }}) 0% 0% no-repeat #eee;
|
||||
}{% endif %}{% endfor %}
|
||||
|
||||
</style>
|
||||
<form action="{{ url_for('mfa.validate') }}" method="POST"
|
||||
name="mfa_form" id="mfa_form" class="show_validate_btn">
|
||||
<div class="form-group">
|
||||
<div class="from-group">
|
||||
<div class="input-group pb-2">
|
||||
<div class="input-group-prepend">
|
||||
<label class="input-group-text" for="mfa_method" id="mfa_method_prepend"> </label>
|
||||
</div>
|
||||
<select name="mfa_method" id="mfa_method" class="auth-select custom-select" onchange="onMFAChange(this.value);">
|
||||
{% for key in views %}<option value="{{views[key].id | e}}" {% if views[key].selected is true %}selected{% endif %}>{{ views[key].label | e }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="from-group pt-2 pb-2" id="mfa_view"></div>
|
||||
</div>
|
||||
<div class="row align-items-center p-2" id='validate-btn-group'>
|
||||
<button class="col btn btn-primary btn-block btn-validate" type="submit" value="{{ _('Validate') }}">{{ _('Validate') }}</button>
|
||||
</div>
|
||||
<div class="form-group text-right p-2"><a class="text-right" role="link" href="{{ logout_url }}">{{ _('Logout') }}</a></div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% set page_name = 'mfa_validate' %}
|
||||
{% set page_props = {
|
||||
'actionUrl': url_for('mfa.validate'),
|
||||
'views': views,
|
||||
'logoutUrl': logout_url,
|
||||
'sendEmailUrl': url_for("mfa.send_email_code"),
|
||||
'csrfHeader': current_app.config.get("WTF_CSRF_HEADERS")[0],
|
||||
'csrfToken': csrf_token()
|
||||
} %}
|
||||
{% extends "security/render_page.html" %}
|
||||
{% block title %}{{ _('Authentication') }}{% endblock %}
|
||||
|
@ -22,6 +22,7 @@ from pgadmin.utils.ajax import bad_request
|
||||
from .utils import user_supported_mfa_methods, mfa_user_registered, \
|
||||
mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \
|
||||
is_mfa_session_authenticated
|
||||
from pgadmin.utils.constants import MessageType
|
||||
|
||||
|
||||
_INDEX_URL = "browser.index"
|
||||
@ -118,11 +119,11 @@ def validate_view() -> Response:
|
||||
"MFA validation failed for the user '{}' with an error: "
|
||||
"{}"
|
||||
).format(current_user.username, str(ve)))
|
||||
flash(str(ve), "danger")
|
||||
flash(str(ve), MessageType.ERROR)
|
||||
return_code = 401
|
||||
except Exception as ex:
|
||||
current_app.logger.exception(ex)
|
||||
flash(str(ex), "danger")
|
||||
flash(str(ex), MessageType.ERROR)
|
||||
return_code = 500
|
||||
|
||||
mfa_views = {
|
||||
@ -166,7 +167,8 @@ def _mfa_registration_view(
|
||||
|
||||
if form_data[mfa.name] == 'SETUP':
|
||||
if supported_mfa['registered'] is True:
|
||||
flash(_("'{}' is already registerd'").format(mfa.label), "success")
|
||||
flash(_("'{}' is already registerd'").format(mfa.label),
|
||||
MessageType.SUCCESS)
|
||||
return None
|
||||
|
||||
return mfa.registration_view(form_data)
|
||||
@ -174,13 +176,13 @@ def _mfa_registration_view(
|
||||
if mfa_delete(mfa.name) is True:
|
||||
flash(_(
|
||||
"'{}' unregistered from the authentication list."
|
||||
).format(mfa.label), "success")
|
||||
).format(mfa.label), MessageType.SUCCESS)
|
||||
|
||||
return None
|
||||
|
||||
flash(_(
|
||||
"'{}' is not found in the authentication list."
|
||||
).format(mfa.label), "warning")
|
||||
).format(mfa.label), MessageType.WARNING)
|
||||
|
||||
return None
|
||||
|
||||
@ -255,7 +257,7 @@ def __handle_registration_view_for_post_method(
|
||||
if view is False:
|
||||
if next_url != 'internal':
|
||||
return None, redirect(next_url), None
|
||||
flash(_("Please close the dialog."), "info")
|
||||
flash(_("Please close the dialog."), MessageType.INFO)
|
||||
|
||||
if view is not None:
|
||||
return None, Response(
|
||||
@ -336,7 +338,8 @@ def registration_view() -> Response:
|
||||
)
|
||||
elif is_mfa_session_authenticated() is False and \
|
||||
found_one_mfa is True:
|
||||
flash(_("Complete the authentication process first"), "danger")
|
||||
flash(_("Complete the authentication process first"),
|
||||
MessageType.ERROR)
|
||||
return redirect(login_url("mfa.validate", next_url=next_url))
|
||||
|
||||
return Response(render_template(
|
||||
|
@ -21,7 +21,7 @@ from flask_security.utils import get_post_logout_redirect, logout_user
|
||||
from pgadmin.authenticate.internal import BaseAuthentication
|
||||
from pgadmin.model import User
|
||||
from pgadmin.tools.user_management import create_user
|
||||
from pgadmin.utils.constants import OAUTH2
|
||||
from pgadmin.utils.constants import OAUTH2, MessageType
|
||||
from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
from pgadmin.model import db
|
||||
@ -61,7 +61,7 @@ def init_app(app):
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
logout_user()
|
||||
flash(msg, 'danger')
|
||||
flash(msg, MessageType.ERROR)
|
||||
return redirect(get_safe_post_login_redirect())
|
||||
|
||||
@blueprint.route('/logout', endpoint="logout",
|
||||
|
@ -452,7 +452,7 @@ def check_browser_upgrade():
|
||||
download_url=data[config.UPGRADE_CHECK_KEY]['download_url']
|
||||
)
|
||||
|
||||
flash(msg, 'warning')
|
||||
flash(msg, MessageType.WARNING)
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
@ -487,7 +487,7 @@ def index():
|
||||
known=browser_known
|
||||
)
|
||||
|
||||
flash(msg, 'warning')
|
||||
flash(msg, MessageType.WARNING)
|
||||
|
||||
# Get the current version info from the website, and flash a message if
|
||||
# the user is out of date, and the check is enabled.
|
||||
@ -507,8 +507,6 @@ def index():
|
||||
response = Response(render_template(
|
||||
MODULE_NAME + "/index.html",
|
||||
username=current_user.username,
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
_=gettext
|
||||
))
|
||||
|
||||
@ -1061,66 +1059,55 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
|
||||
def change_password():
|
||||
"""View function which handles a change password request."""
|
||||
|
||||
has_error = False
|
||||
form_class = _security.forms.get('change_password_form').cls
|
||||
req_json = request.get_json(silent=True)
|
||||
|
||||
if req_json:
|
||||
form = form_class(MultiDict(req_json))
|
||||
else:
|
||||
if not req_json:
|
||||
form = form_class()
|
||||
return {
|
||||
'csrf_token': form.csrf_token._value()
|
||||
}
|
||||
elif req_json:
|
||||
form = form_class(MultiDict(req_json))
|
||||
if form.validate_on_submit():
|
||||
errormsg = None
|
||||
try:
|
||||
change_user_password(current_user._get_current_object(),
|
||||
form.new_password.data,
|
||||
autologin=False)
|
||||
except SOCKETErrorException as e:
|
||||
# Handle socket errors which are not covered by
|
||||
# SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
errormsg = gettext(SMTP_SOCKET_ERROR).format(e)
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
SMTPException, SMTPAuthenticationError,
|
||||
SMTPSenderRefused, SMTPRecipientsRefused) as ex:
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(ex), exc_info=True)
|
||||
errormsg = gettext(SMTP_ERROR).format(ex)
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
errormsg = gettext(PASS_ERROR).format(e)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
change_user_password(current_user._get_current_object(),
|
||||
form.new_password.data)
|
||||
except SOCKETErrorException as e:
|
||||
# Handle socket errors which are not covered by SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_SOCKET_ERROR).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
SMTPException, SMTPAuthenticationError, SMTPSenderRefused,
|
||||
SMTPRecipientsRefused) as e:
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_ERROR).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(
|
||||
gettext(PASS_ERROR).format(e),
|
||||
'danger'
|
||||
)
|
||||
has_error = True
|
||||
if request.get_json(silent=True) is None and errormsg is None:
|
||||
old_key = get_crypt_key()[1]
|
||||
set_crypt_key(form.new_password.data, False)
|
||||
|
||||
if request.get_json(silent=True) is None and not has_error:
|
||||
after_this_request(view_commit)
|
||||
do_flash(*get_message('PASSWORD_CHANGE'))
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import reencrpyt_server_passwords
|
||||
reencrpyt_server_passwords(
|
||||
current_user.id, old_key, form.new_password.data)
|
||||
|
||||
old_key = get_crypt_key()[1]
|
||||
set_crypt_key(form.new_password.data, False)
|
||||
return redirect(get_url(_security.post_change_view) or
|
||||
get_url(_security.post_login_view))
|
||||
else:
|
||||
return internal_server_error(errormsg)
|
||||
else:
|
||||
return bad_request(list(form.errors.values())[0][0])
|
||||
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import reencrpyt_server_passwords
|
||||
reencrpyt_server_passwords(
|
||||
current_user.id, old_key, form.new_password.data)
|
||||
|
||||
return redirect(get_url(_security.post_change_view) or
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
if request.get_json(silent=True) and not has_error:
|
||||
form.user = current_user
|
||||
return default_render_json(form)
|
||||
|
||||
return _security.render_template(
|
||||
config_value('CHANGE_PASSWORD_TEMPLATE'),
|
||||
change_password_form=form,
|
||||
**_ctx('change_password'))
|
||||
|
||||
# Only register route if SECURITY_RECOVERABLE is set to True
|
||||
if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
@ -1171,7 +1158,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
'Please contact the administrators of this '
|
||||
'service if you need to reset your password.'
|
||||
).format(form.user.auth_source),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
if not has_error:
|
||||
try:
|
||||
@ -1181,7 +1168,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
# covered by SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_SOCKET_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
@ -1191,13 +1178,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(PASS_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
|
||||
if request.get_json(silent=True) is None and not has_error:
|
||||
@ -1247,7 +1234,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
# Handle socket errors which are not covered by SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_SOCKET_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
@ -1257,13 +1244,13 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(SMTP_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(PASS_ERROR).format(e),
|
||||
'danger')
|
||||
MessageType.ERROR)
|
||||
has_error = True
|
||||
|
||||
if not has_error:
|
||||
@ -1275,7 +1262,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
flash(gettext('You successfully reset your password but'
|
||||
' your account is locked. Please contact '
|
||||
'the Administrator.'),
|
||||
'warning')
|
||||
MessageType.WARNING)
|
||||
return redirect(get_post_logout_redirect())
|
||||
do_flash(*get_message('PASSWORD_RESET'))
|
||||
login_user(user)
|
||||
|
@ -28,6 +28,7 @@ samp,
|
||||
min-height: 100%;
|
||||
background: $loading-bg;
|
||||
z-index: 1056;
|
||||
top: 0;
|
||||
|
||||
.pg-sp-content {
|
||||
position: absolute;
|
||||
|
@ -67,6 +67,10 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
|
||||
def runTest(self):
|
||||
"""This function checks spnego/kerberos login functionality."""
|
||||
if self.flag == 1:
|
||||
if app_config.SERVER_MODE is False:
|
||||
self.skipTest(
|
||||
"Can not run Kerberos Authentication in the Desktop mode."
|
||||
)
|
||||
self.test_unauthorized()
|
||||
elif self.flag == 2:
|
||||
if app_config.SERVER_MODE is False:
|
||||
|
@ -125,7 +125,7 @@ export default function AppMenuBar() {
|
||||
<div className={classes.gravatar}>
|
||||
{userMenuInfo.gravatar &&
|
||||
<img src={userMenuInfo.gravatar} width = "18" height = "18"
|
||||
alt = "Gravatar image for {{ username }}" />}
|
||||
alt ={`Gravatar image for ${ userMenuInfo.username }`} />}
|
||||
{!userMenuInfo.gravatar && <AccountCircleRoundedIcon />}
|
||||
</div>
|
||||
{ userMenuInfo.username } ({userMenuInfo.auth_source})
|
||||
|
@ -15,7 +15,7 @@ import BaseUISchema from '../SchemaView/base_schema.ui';
|
||||
import SchemaView from '../SchemaView';
|
||||
|
||||
class ChangePasswordSchema extends BaseUISchema {
|
||||
constructor(user, isPgpassFileUsed) {
|
||||
constructor(user, isPgpassFileUsed, hasCsrfToken=false, showUser=true) {
|
||||
super({
|
||||
user: user,
|
||||
password: '',
|
||||
@ -23,13 +23,15 @@ class ChangePasswordSchema extends BaseUISchema {
|
||||
confirmPassword: ''
|
||||
});
|
||||
this.isPgpassFileUsed = isPgpassFileUsed;
|
||||
this.hasCsrfToken = hasCsrfToken;
|
||||
this.showUser = showUser;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let self = this;
|
||||
return [
|
||||
{
|
||||
id: 'user', label: gettext('User'), type: 'text', disabled: true
|
||||
id: 'user', label: gettext('User'), type: 'text', disabled: true, visible: this.showUser
|
||||
}, {
|
||||
id: 'password', label: gettext('Current Password'), type: 'password',
|
||||
disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true,
|
||||
@ -42,14 +44,18 @@ class ChangePasswordSchema extends BaseUISchema {
|
||||
controlProps: {
|
||||
maxLength: null
|
||||
}
|
||||
}, {
|
||||
}, {
|
||||
id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password',
|
||||
noEmpty: true,
|
||||
controlProps: {
|
||||
maxLength: null
|
||||
}
|
||||
}
|
||||
];
|
||||
].concat(this.hasCsrfToken ? [
|
||||
{
|
||||
id: 'csrf_token', visible: false, type: 'text'
|
||||
}
|
||||
]: []);
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
@ -72,13 +78,14 @@ const useStyles = makeStyles((theme)=>({
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ChangePasswordContent({onSave, onClose, userName, isPgpassFileUsed}) {
|
||||
export default function ChangePasswordContent({getInitData=() => { /*This is intentional (SonarQube)*/ },
|
||||
onSave, onClose, hasCsrfToken=false, showUser=true}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={() => { /*This is intentional (SonarQube)*/ }}
|
||||
schema={new ChangePasswordSchema(userName, isPgpassFileUsed)}
|
||||
getInitData={getInitData}
|
||||
schema={new ChangePasswordSchema('', false, hasCsrfToken, showUser)}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
@ -96,5 +103,8 @@ ChangePasswordContent.propTypes = {
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
userName: PropTypes.string,
|
||||
isPgpassFileUsed: PropTypes.bool
|
||||
isPgpassFileUsed: PropTypes.bool,
|
||||
getInitData: PropTypes.func,
|
||||
hasCsrfToken: PropTypes.bool,
|
||||
showUser: PropTypes.bool
|
||||
};
|
||||
|
@ -292,6 +292,52 @@ export function showChangeServerPassword() {
|
||||
});
|
||||
}
|
||||
|
||||
export function showChangeUserPassword(url) {
|
||||
mountDialog(gettext('Change pgAdmin User Password'), (onClose)=> {
|
||||
const api = getApiInstance();
|
||||
return <Theme>
|
||||
<ChangePasswordContent
|
||||
getInitData={()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
api.get(url)
|
||||
.then((res)=>{
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}}
|
||||
onClose={()=>{
|
||||
onClose();
|
||||
}}
|
||||
onSave={(_isNew, data)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
const formData = {
|
||||
'password': data.password,
|
||||
'new_password': data.newPassword,
|
||||
'new_password_confirm': data.confirmPassword,
|
||||
'csrf_token': data.csrf_token
|
||||
};
|
||||
|
||||
api({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: formData,
|
||||
}).then((res)=>{
|
||||
resolve(res);
|
||||
}).catch((err)=>{
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}}
|
||||
hasCsrfToken={true}
|
||||
showUser={false}
|
||||
/>
|
||||
</Theme>;
|
||||
}, undefined, undefined, pgAdmin.Browser.stdH.sm);
|
||||
}
|
||||
|
||||
export function showNamedRestorePoint() {
|
||||
let title = arguments[0],
|
||||
nodeData = arguments[1],
|
||||
|
101
web/pgadmin/static/js/SecurityPages/BasePage.jsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { Box, Button, darken, makeStyles } from '@material-ui/core';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React, { useEffect } from 'react';
|
||||
import { MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents';
|
||||
import { FinalNotifyContent } from '../helpers/Notifier';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const contentBg = '#2b709b';
|
||||
const loginBtnBg = '#038bba';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
pageContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '16px',
|
||||
backgroundColor: contentBg,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
maxHeight: '100%',
|
||||
minWidth: '450px',
|
||||
maxWidth: '450px'
|
||||
},
|
||||
logo: {
|
||||
width: '96px',
|
||||
height: '40px',
|
||||
background: 'url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDUgNTAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZmZmO30uY2xzLTJ7ZmlsbDojMzI2ODkzO308L3N0eWxlPjwvZGVmcz48dGl0bGU+cGdBZG1pbjwvdGl0bGU+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTguOTQsNDEuNGEyLjQ4LDIuNDgsMCwwLDEtMi4yNy0zLjQ5TDY0LDIxLjI5VjZhNiw2LDAsMCwwLTYtNkg2QTYsNiwwLDAsMCwwLDZWNDRhNiw2LDAsMCwwLDYsNkg1OGE2LDYsMCwwLDAsNi02VjQxLjRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjkuMjUsMzAuMTdhMTMuMTMsMTMuMTMsMCwwLDEtMS44Mi02LjkzLDEzLDEzLDAsMCwxLDEuODItNi44OCwxMi41LDEyLjUsMCwwLDEsMS40OC0xLjk1LDEwLjQ0LDEwLjQ0LDAsMCwwLTMuMjUtMi44OSwxMS4xNiwxMS4xNiwwLDAsMC01LjY1LTEuNDVxLTQuNDgsMC02LjcyLDIuNjRWMTAuNDRINy41MVY0MC4zNmExLDEsMCwwLDAsMSwxaDZhMSwxLDAsMCwwLDEtMVYzMS4xOWE4LjQ3LDguNDcsMCwwLDAsNi4zNCwyLjQsMTEuMjYsMTEuMjYsMCwwLDAsNS42NS0xLjQ1LDEwLjUzLDEwLjUzLDAsMCwwLDIuMDYtMS41NkMyOS40NCwzMC40NCwyOS4zNCwzMC4zMSwyOS4yNSwzMC4xN1pNMjMuNiwyNS44YTQuNTIsNC41MiwwLDAsMS0zLjQ1LDEuNDQsNC40OCw0LjQ4LDAsMCwxLTMuNDQtMS40NCw1LjYsNS42LDAsMCwxLTEuMzUtNCw1LjU5LDUuNTksMCwwLDEsMS4zNS00LDQuNDYsNC40NiwwLDAsMSwzLjQ0LTEuNDUsNC40OSw0LjQ5LDAsMCwxLDMuNDUsMS40NSw1LjYzLDUuNjMsMCwwLDEsMS4zNCw0QTUuNjQsNS42NCwwLDAsMSwyMy42LDI1LjhaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTYuNDksMTIuNjNWMzEuMjRxMCw2LjM1LTMuNDQsOS41MXQtOS45MiwzLjE3YTI1LjQyLDI1LjQyLDAsMCwxLTYuMy0uNzUsMTUsMTUsMCwwLDEtNS0yLjIzbDIuODktNS41OWExMC4xNywxMC4xNywwLDAsMCwzLjUxLDEuNzksMTQuMzcsMTQuMzcsMCwwLDAsNC4xOC42NUE2LjUzLDYuNTMsMCwwLDAsNDcsMzYuNGE1LjM3LDUuMzcsMCwwLDAsMS40Ny00LjExdi0uNzZjLTEuNTQsMS44LTMuNzksMi42OS02Ljc2LDIuNjlhMTEuNywxMS43LDAsMCwxLTUuNTktMS4zNkExMC4zNywxMC4zNywwLDAsMSwzMi4wOSwyOWExMC44OSwxMC44OSwwLDAsMS0xLjUxLTUuNzcsMTAuODYsMTAuODYsMCwwLDEsMS41MS01Ljc0LDEwLjQyLDEwLjQyLDAsMCwxLDQuMDctMy44NiwxMS43MSwxMS43MSwwLDAsMSw1LjU5LTEuMzdjMy4yNSwwLDUuNjMsMS4wNiw3LjE0LDMuMTVWMTIuNjNabS05LjMsMTMuOTVhNC40LDQuNCwwLDAsMCwxLjQtMy4zNiw0LjM0LDQuMzQsMCwwLDAtMS4zOC0zLjM0LDUuNjUsNS42NSwwLDAsMC03LjE2LDAsNC4zLDQuMywwLDAsMC0xLjQxLDMuMzQsNC4zNSw0LjM1LDAsMCwwLDEuNDMsMy4zNiw1LjA4LDUuMDgsMCwwLDAsMy41NywxLjNBNSw1LDAsMCwwLDQ3LjE5LDI2LjU4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTgzLjQzLDMyLjg5SDcxbC0yLDUuMDlhMSwxLDAsMCwxLS45My42Mkg2MS43M2ExLDEsMCwwLDEtLjkxLTEuNEw3Mi45MSw5LjhhMSwxLDAsMCwxLC45Mi0uNmg2Ljg5YTEsMSwwLDAsMSwuOTEuNkw5My43NywzNy4yYTEsMSwwLDAsMS0uOTIsMS40SDg2LjQxYTEsMSwwLDAsMS0uOTMtLjYyWk04MSwyNi43NmwtMy43OC05LjQxLTMuNzgsOS40MVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xMjAuNDQsOC40NFYzNy42YTEsMSwwLDAsMS0xLDFoLTUuNmExLDEsMCwwLDEtMS0xVjM2LjMzUTExMC42MiwzOSwxMDYuMTYsMzlhMTEuMjksMTEuMjksMCwwLDEtNS42Ny0xLjQ1LDEwLjU0LDEwLjU0LDAsMCwxLTQtNC4xNEExMi42MiwxMi42MiwwLDAsMSw5NSwyNy4xOCwxMi41MywxMi41MywwLDAsMSw5Ni40NCwyMWExMC4zNSwxMC4zNSwwLDAsMSw0LTQuMDksMTEuNDgsMTEuNDgsMCwwLDEsNS42Ny0xLjQzLDguMjQsOC4yNCwwLDAsMSw2LjMsMi4zNVY4LjQ0YTEsMSwwLDAsMSwxLTFoNkExLDEsMCwwLDEsMTIwLjQ0LDguNDRabS05LjE5LDIyLjc1YTUuNzEsNS43MSwwLDAsMCwxLjM0LTQsNS42LDUuNiwwLDAsMC0xLjMyLTMuOTUsNC40Nyw0LjQ3LDAsMCwwLTMuNDMtMS40Myw0LjUzLDQuNTMsMCwwLDAtMy40NCwxLjQzLDUuNTEsNS41MSwwLDAsMC0xLjM0LDMuOTUsNS42Nyw1LjY3LDAsMCwwLDEuMzQsNCw0Ljc3LDQuNzcsMCwwLDAsNi44NSwwWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTE2MSwxOGMxLjY2LDEuNjgsMi41LDQuMjEsMi41LDcuNnYxMmExLDEsMCwwLDEtMSwxaC02YTEsMSwwLDAsMS0xLTFWMjYuODhhNS42Nyw1LjY3LDAsMCwwLS45LTMuNTMsMy4wOSwzLjA5LDAsMCwwLTIuNTUtMS4xMywzLjYyLDMuNjIsMCwwLDAtMi44OSwxLjI2LDUuNzEsNS43MSwwLDAsMC0xLjEsMy44MlYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYyNi44OGMwLTMuMTEtMS4xNC00LjY2LTMuNDQtNC42NmEzLjcsMy43LDAsMCwwLTIuOTQsMS4yNiw1LjcxLDUuNzEsMCwwLDAtMS4wOSwzLjgyVjM3LjZhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjE2Ljg0YTEsMSwwLDAsMSwxLTFoNS42YTEsMSwwLDAsMSwxLDF2MS4zOWE4LDgsMCwwLDEsMy0yLjA4LDEwLjIzLDEwLjIzLDAsMCwxLDMuOC0uNjksMTAsMTAsMCwwLDEsNC4yOS44OEE3LjI4LDcuMjgsMCwwLDEsMTQ2LjQyLDE5YTguODUsOC44NSwwLDAsMSwzLjQxLTIuNjUsMTAuOTMsMTAuOTMsMCwwLDEsNC40OS0uOTJBOSw5LDAsMCwxLDE2MSwxOFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xNjguMTIsMTIuMWEzLjkxLDMuOTEsMCwwLDEtMS4zNC0yLjc5QTQuMTYsNC4xNiwwLDAsMSwxNjgsNi4xOWE1LDUsMCwwLDEsMy42Ny0xLjM2QTUuMjUsNS4yNSwwLDAsMSwxNzUuMTgsNmEzLjc1LDMuNzUsMCwwLDEsMS4zNCwzLDQuMSw0LjEsMCwwLDEtMS4zNCwzLjEzLDUuNjgsNS42OCwwLDAsMS03LjA2LDBabS41NCwzLjc0aDZhMSwxLDAsMCwxLDEsMVYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NEExLDEsMCwwLDEsMTY4LjY2LDE1Ljg0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTIwMS41NSwxOHEyLjU5LDIuNTIsMi41OSw3LjZ2MTJhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjI2Ljg4cTAtNC42Ni0zLjc0LTQuNjZhNC4zLDQuMywwLDAsMC0zLjMsMS4zNCw1LjgzLDUuODMsMCwwLDAtMS4yNCw0djEwYTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NGExLDEsMCwwLDEsMS0xaDUuNjFhMSwxLDAsMCwxLDEsMXYxLjQ3YTkuMDUsOS4wNSwwLDAsMSwzLjE5LTIuMTIsMTAuNzgsMTAuNzgsMCwwLDEsNC0uNzNBOS4zNCw5LjM0LDAsMCwxLDIwMS41NSwxOFoiLz48L3N2Zz4=) 0 0 no-repeat',
|
||||
backgroundPositionY: 'center',
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '15px',
|
||||
fontSize: '1.2rem'
|
||||
},
|
||||
button: {
|
||||
backgroundColor: loginBtnBg,
|
||||
color: '#fff',
|
||||
padding: '4px 8px',
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
backgroundColor: darken(loginBtnBg, 0.1),
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.60,
|
||||
color: '#fff'
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
||||
export function SecurityButton({...props}) {
|
||||
const classes = useStyles();
|
||||
return <Button type="submit" className={classes.button} {...props} />;
|
||||
}
|
||||
|
||||
export default function BasePage({pageImage, title, children, messages}) {
|
||||
const classes = useStyles();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
useEffect(()=>{
|
||||
messages?.forEach((m)=>{
|
||||
snackbar.enqueueSnackbar(null, {
|
||||
autoHideDuration: null,
|
||||
content: (key)=>{
|
||||
if(Array.isArray(m[0])) m[0] = m[0][0];
|
||||
const type = Object.values(MESSAGE_TYPE).includes(m[0]) ? m[0] : MESSAGE_TYPE.INFO;
|
||||
return <FinalNotifyContent>
|
||||
<NotifierMessage type={type} message={m[1]} closable={true} onClose={()=>{snackbar.closeSnackbar(key);}} style={{maxWidth: '400px'}} />
|
||||
</FinalNotifyContent>;
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [messages]);
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box display="flex" minWidth="80%" gridGap='40px' alignItems="center" padding="20px 80px">
|
||||
<Box flexGrow={1} height="80%" textAlign="center">
|
||||
{pageImage}
|
||||
</Box>
|
||||
<Box className={classes.pageContent}>
|
||||
<Box className={classes.item}><div className={classes.logo} /></Box>
|
||||
<Box className={classes.item}>{title}</Box>
|
||||
<Box display="flex" flexDirection="column" minHeight={0}>{children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
BasePage.propTypes = {
|
||||
pageImage: CustomPropTypes.children,
|
||||
title: PropTypes.string,
|
||||
children: CustomPropTypes.children,
|
||||
messages: PropTypes.arrayOf(PropTypes.array)
|
||||
};
|
30
web/pgadmin/static/js/SecurityPages/ForgotPasswordPage.jsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import ForgotPasswordImage from '../../img/forgot_password.svg?svgr';
|
||||
import { InputText } from '../components/FormComponents';
|
||||
import BasePage, { SecurityButton } from './BasePage';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function ForgotPasswordPage({csrfToken, actionUrl, ...props}) {
|
||||
const [form, setForm] = useState(({email: ''}));
|
||||
|
||||
const onTextChange = (n, val)=>{
|
||||
setForm((prev)=>({...prev, [n]: val}));
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage title={gettext('Forget Password')} pageImage={<ForgotPasswordImage style={{height: '100%', width: '100%'}} />} {...props} >
|
||||
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={actionUrl} method="POST">
|
||||
<input name="csrf_token" defaultValue={csrfToken} hidden/>
|
||||
<div>{gettext('Enter the email address for the user account you wish to recover the password for:')}</div>
|
||||
<InputText name="email" value={form.email} onChange={(v)=>onTextChange('email', v)} placeholder={gettext('Email Address')} autoFocus />
|
||||
<SecurityButton name="internal_button" value="Recover Password">{gettext('Recover Password')}</SecurityButton>
|
||||
</form>
|
||||
</BasePage>
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPasswordPage.propTypes = {
|
||||
csrfToken: PropTypes.string,
|
||||
actionUrl: PropTypes.string,
|
||||
};
|
67
web/pgadmin/static/js/SecurityPages/LoginPage.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { Box, Icon } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import LoginImage from '../../img/login.svg?svgr';
|
||||
import { InputSelectNonSearch, InputText, MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents';
|
||||
import BasePage, { SecurityButton } from './BasePage';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function LoginPage({userLanguage, langOptions, forgotPassUrl, csrfToken, loginUrl, authSources, authSourcesEnum, oauth2Config, loginBanner, ...props}) {
|
||||
const [form, setForm] = useState(({email: '', password: '', language: userLanguage}));
|
||||
const showLoginForm = authSources?.includes('internal');
|
||||
|
||||
const onTextChange = (n, val)=>{
|
||||
setForm((prev)=>({...prev, [n]: val}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loginBanner && <NotifierMessage showIcon={false} closable={false} type={MESSAGE_TYPE.ERROR} message={loginBanner} style={{
|
||||
position: 'absolute',
|
||||
width: '80%',
|
||||
top: '30px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto'
|
||||
}} textCenter />}
|
||||
<BasePage title={gettext('Login')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
|
||||
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={loginUrl} method="POST">
|
||||
{showLoginForm &&
|
||||
<>
|
||||
<input name="csrf_token" defaultValue={csrfToken} hidden/>
|
||||
<InputText name="email" value={form.email} onChange={(v)=>onTextChange('email', v)} placeholder={gettext('Email Address / Username')} autoFocus />
|
||||
<InputText name="password" value={form.password} onChange={(v)=>onTextChange('password', v)} type="password" placeholder={gettext('Password')} />
|
||||
<Box textAlign="right" marginTop="-10px">
|
||||
<a style={{color: 'inherit'}} href={forgotPassUrl}>{gettext('Forgotten your password?')}</a>
|
||||
</Box>
|
||||
<InputSelectNonSearch name="language" options={langOptions} value={form.language} onChange={(v)=>onTextChange('language', v.target.value)} />
|
||||
<SecurityButton name="internal_button" value="Login">{gettext('Login')}</SecurityButton>
|
||||
</>
|
||||
}
|
||||
{authSources?.includes?.(authSourcesEnum.OAUTH2) &&
|
||||
oauth2Config.map((oauth)=>{
|
||||
return (
|
||||
<SecurityButton key={oauth.OAUTH2_NAME} name="oauth2_button" value={oauth.OAUTH2_NAME} style={{backgroundColor: oauth.OAUTH2_BUTTON_COLOR}}>
|
||||
<Icon className={'fab '+oauth.OAUTH2_ICON} style={{ fontSize: '1.5em', marginRight: '8px' }} />{gettext('Login with %s', oauth.OAUTH2_DISPLAY_NAME)}
|
||||
</SecurityButton>
|
||||
);
|
||||
})
|
||||
}
|
||||
</form>
|
||||
</BasePage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LoginPage.propTypes = {
|
||||
userLanguage: PropTypes.string,
|
||||
langOptions: PropTypes.arrayOf(PropTypes.object),
|
||||
forgotPassUrl: PropTypes.string,
|
||||
csrfToken: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
authSources: PropTypes.arrayOf(PropTypes.string),
|
||||
authSourcesEnum: PropTypes.object,
|
||||
oauth2Config: PropTypes.arrayOf(PropTypes.object),
|
||||
loginBanner: PropTypes.string
|
||||
};
|
112
web/pgadmin/static/js/SecurityPages/MfaRegisterPage.jsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { Box } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import LoginImage from '../../img/login.svg?svgr';
|
||||
import { FormNote, InputText } from '../components/FormComponents';
|
||||
import BasePage, { SecurityButton } from './BasePage';
|
||||
import { DefaultButton } from '../components/Buttons';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function EmailRegisterView({mfaView}) {
|
||||
const [inputEmail, setInputEmail] = useState(mfaView.email_address);
|
||||
const [inputCode, setInputCode] = useState('');
|
||||
|
||||
if(mfaView.email_address_placeholder) {
|
||||
return <>
|
||||
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.label}</div>
|
||||
<div>
|
||||
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
|
||||
<input type='hidden' name='validate' value='send_code'/>
|
||||
</div>
|
||||
<div>{mfaView.description}</div>
|
||||
<InputText value={inputEmail} type="email" name="send_to" placeholder={mfaView.email_address_placeholder}
|
||||
onChange={setInputEmail} required
|
||||
/>
|
||||
<FormNote text={mfaView.note} />
|
||||
</>;
|
||||
} else if(mfaView.otp_placeholder) {
|
||||
return <>
|
||||
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.label}</div>
|
||||
<div>
|
||||
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
|
||||
<input type='hidden' name='validate' value='verify_code'/>
|
||||
</div>
|
||||
<div>{mfaView.message}</div>
|
||||
<InputText value={inputCode} pattern="\d{6}" type="password" name="code" placeholder={mfaView.otp_placeholder}
|
||||
onChange={setInputCode} required autoComplete="one-time-code"
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
EmailRegisterView.propTypes = {
|
||||
mfaView: PropTypes.object,
|
||||
};
|
||||
|
||||
function AuthenticatorRegisterView({mfaView}) {
|
||||
const [inputValue, setInputValue] = useState(mfaView.email_address);
|
||||
|
||||
return <>
|
||||
<div style={{textAlign: 'center', fontSize: '1.2em'}}>{mfaView.auth_title}</div>
|
||||
<div>
|
||||
<input type='hidden' name={mfaView.auth_method} value='SETUP'/>
|
||||
<input type='hidden' name='VALIDATE' value='validate'/>
|
||||
</div>
|
||||
<div style={{minHeight: 0, display: 'flex'}}>
|
||||
<img src={`data:image/jpeg;base64,${mfaView.image}`} style={{maxWidth: '100%', objectFit: 'contain'}} alt={mfaView.qrcode_alt_text} />
|
||||
</div>
|
||||
<div>{mfaView.auth_description}</div>
|
||||
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
|
||||
onChange={setInputValue}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
AuthenticatorRegisterView.propTypes = {
|
||||
mfaView: PropTypes.object,
|
||||
};
|
||||
|
||||
export default function MfaRegisterPage({actionUrl, mfaList, nextUrl, mfaView, ...props}) {
|
||||
return (
|
||||
<>
|
||||
<BasePage title={gettext('Authentication Registration')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
|
||||
<form style={{display:'flex', gap:'15px', flexDirection:'column', minHeight: 0}} action={actionUrl} method="POST">
|
||||
{mfaView ? <>
|
||||
{mfaView.auth_method == 'email' && <EmailRegisterView mfaView={mfaView} />}
|
||||
{mfaView.auth_method == 'authenticator' && <AuthenticatorRegisterView mfaView={mfaView} />}
|
||||
<Box display="flex" gridGap="15px">
|
||||
<SecurityButton name="continue" value="Continue">{gettext('Continue')}</SecurityButton>
|
||||
<DefaultButton type="submit" name="cancel" value="Cancel" style={{width: '100%'}}>{gettext('Cancel')}</DefaultButton>
|
||||
</Box>
|
||||
</>:<>
|
||||
{mfaList?.map((m)=>{
|
||||
return (
|
||||
<Box display="flex" width="100%" key={m.label}>
|
||||
<div style={{
|
||||
width: '10%', mask: `url(${m.icon})`, maskRepeat: 'no-repeat',
|
||||
WebkitMask: `url(${m.icon})`, WebkitMaskRepeat: 'no-repeat',
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
</div>
|
||||
<div style={{width: '70%'}}>{m.label}</div>
|
||||
<div style={{width: '20%'}}>
|
||||
<SecurityButton name={m.id} value={m.registered ? 'DELETE' : 'SETUP'}>{m.registered ? gettext('Delete') : gettext('Setup')}</SecurityButton>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{nextUrl != 'internal' && <SecurityButton value="Continue">{gettext('Continue')}</SecurityButton>}
|
||||
</>}
|
||||
<div><input type="hidden" name="next" value={nextUrl}/></div>
|
||||
</form>
|
||||
</BasePage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
MfaRegisterPage.propTypes = {
|
||||
actionUrl: PropTypes.string,
|
||||
mfaList: PropTypes.arrayOf(PropTypes.object),
|
||||
nextUrl: PropTypes.string,
|
||||
mfaView: PropTypes.object
|
||||
};
|
125
web/pgadmin/static/js/SecurityPages/MfaValidatePage.jsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import LoginImage from '../../img/login.svg?svgr';
|
||||
import { InputSelect, InputText, MESSAGE_TYPE, NotifierMessage } from '../components/FormComponents';
|
||||
import BasePage, { SecurityButton } from './BasePage';
|
||||
import { useDelayedCaller } from '../custom_hooks';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function EmailValidateView({mfaView, sendEmailUrl, csrfHeader, csrfToken}) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [sentMessage, setSentMessage] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [showResend, setShowResend] = useState(false);
|
||||
|
||||
const showResendAfter = useDelayedCaller(()=>{
|
||||
setShowResend(true);
|
||||
});
|
||||
|
||||
const sendCodeToEmail = ()=>{
|
||||
setSending(true);
|
||||
let accept = 'text/html; charset=utf-8;';
|
||||
|
||||
fetch(sendEmailUrl, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Accept': accept,
|
||||
'Content-Type': 'application/json; charset=utf-8;',
|
||||
[csrfHeader]: csrfToken,
|
||||
},
|
||||
redirect: 'follow'
|
||||
}).then((resp) => {
|
||||
if (!resp.ok) {
|
||||
resp.text().then(msg => setError(msg));
|
||||
return;
|
||||
}
|
||||
return resp.json();
|
||||
}).then((resp) => {
|
||||
if (!resp)
|
||||
return;
|
||||
setSentMessage(resp.message);
|
||||
showResendAfter(20000);
|
||||
}).finally(()=>{
|
||||
setSending(false);
|
||||
});
|
||||
};
|
||||
|
||||
return <>
|
||||
<div style={{textAlign: 'center'}}>{mfaView.description}</div>
|
||||
{sentMessage && <>
|
||||
<div>{sentMessage}</div>
|
||||
{showResend && <div>
|
||||
<span>{gettext('Haven\'t received an email?')} <a style={{color:'inherit', fontWeight: 'bold'}} href="#" onClick={sendCodeToEmail}>{gettext('Send again')}</a></span>
|
||||
</div>}
|
||||
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
|
||||
onChange={setInputValue} autoFocus
|
||||
/>
|
||||
<SecurityButton value='Validate'>{gettext('Validate')}</SecurityButton>
|
||||
</>}
|
||||
{error && <NotifierMessage message={error} type={MESSAGE_TYPE.ERROR} closable={false} />}
|
||||
{!sentMessage &&
|
||||
<SecurityButton type="button" name="send_code" onClick={sendCodeToEmail} disabled={sending}>
|
||||
{sending ? mfaView.button_label_sending : mfaView.button_label}
|
||||
</SecurityButton>}
|
||||
</>;
|
||||
}
|
||||
|
||||
EmailValidateView.propTypes = {
|
||||
mfaView: PropTypes.object,
|
||||
sendEmailUrl: PropTypes.string,
|
||||
csrfHeader: PropTypes.string,
|
||||
csrfToken: PropTypes.string
|
||||
};
|
||||
|
||||
function AuthenticatorValidateView({mfaView}) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
return <>
|
||||
<div>{mfaView.auth_description}</div>
|
||||
<InputText value={inputValue} type="password" name="code" placeholder={mfaView.otp_placeholder}
|
||||
onChange={setInputValue} autoFocus
|
||||
/>
|
||||
<SecurityButton value='Validate'>{gettext('Validate')}</SecurityButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
AuthenticatorValidateView.propTypes = {
|
||||
mfaView: PropTypes.object,
|
||||
};
|
||||
|
||||
export default function MfaValidatePage({actionUrl, views, logoutUrl, sendEmailUrl, csrfHeader, csrfToken, ...props}) {
|
||||
const [method, setMethod] = useState(Object.values(views).find((v)=>v.selected)?.id);
|
||||
return (
|
||||
<>
|
||||
<BasePage title={gettext('Authentication')} pageImage={<LoginImage style={{height: '100%', width: '100%'}} />} {...props}>
|
||||
<form style={{display:'flex', gap:'15px', flexDirection:'column', minHeight: 0}} action={actionUrl} method="POST">
|
||||
<InputSelect value={method} options={Object.keys(views).map((k)=>({
|
||||
label: views[k].label,
|
||||
value: views[k].id,
|
||||
imageUrl: views[k].icon
|
||||
}))} onChange={setMethod} controlProps={{
|
||||
allowClear: false,
|
||||
}} />
|
||||
<div><input type='hidden' name='mfa_method' defaultValue={method} /></div>
|
||||
{method == 'email' && <EmailValidateView mfaView={views[method].view} sendEmailUrl={sendEmailUrl} csrfHeader={csrfHeader} csrfToken={csrfToken} />}
|
||||
{method == 'authenticator' && <AuthenticatorValidateView mfaView={views[method].view} />}
|
||||
<div style={{textAlign: 'right'}}>
|
||||
<a style={{color:'inherit'}} href={logoutUrl}>{gettext('Logout')}</a>
|
||||
</div>
|
||||
</form>
|
||||
</BasePage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
MfaValidatePage.propTypes = {
|
||||
actionUrl: PropTypes.string,
|
||||
views: PropTypes.object,
|
||||
logoutUrl: PropTypes.string,
|
||||
sendEmailUrl: PropTypes.string,
|
||||
csrfHeader: PropTypes.string,
|
||||
csrfToken: PropTypes.string
|
||||
};
|
30
web/pgadmin/static/js/SecurityPages/PasswordResetPage.jsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import ForgotPasswordImage from '../../img/forgot_password.svg?svgr';
|
||||
import { InputText } from '../components/FormComponents';
|
||||
import BasePage, { SecurityButton } from './BasePage';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function PasswordResetPage({csrfToken, actionUrl, ...props}) {
|
||||
const [form, setForm] = useState(({password: '', password_confirm: ''}));
|
||||
|
||||
const onTextChange = (n, val)=>{
|
||||
setForm((prev)=>({...prev, [n]: val}));
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage title={gettext('Reset Password')} pageImage={<ForgotPasswordImage style={{height: '100%', width: '100%'}} />} {...props} >
|
||||
<form style={{display:'flex', gap:'15px', flexDirection:'column'}} action={actionUrl} method="POST">
|
||||
<input name="csrf_token" defaultValue={csrfToken} hidden/>
|
||||
<InputText name="password" value={form.password} onChange={(v)=>onTextChange('password', v)} type="password" placeholder={gettext('Password')} autoFocus/>
|
||||
<InputText name="password_confirm" value={form.password_confirm} onChange={(v)=>onTextChange('password_confirm', v)} type="password" placeholder={gettext('Retype Password')} />
|
||||
<SecurityButton value="Reset Password">{gettext('Reset Password')}</SecurityButton>
|
||||
</form>
|
||||
</BasePage>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordResetPage.propTypes = {
|
||||
csrfToken: PropTypes.string,
|
||||
actionUrl: PropTypes.string
|
||||
};
|
37
web/pgadmin/static/js/SecurityPages/index.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import Theme from '../Theme/index';
|
||||
import LoginPage from './LoginPage';
|
||||
import ForgotPasswordPage from './ForgotPasswordPage';
|
||||
import PasswordResetPage from './PasswordResetPage';
|
||||
import MfaRegisterPage from './MfaRegisterPage';
|
||||
import MfaValidatePage from './MfaValidatePage';
|
||||
|
||||
window.renderSecurityPage = function(pageName, pageProps, otherProps) {
|
||||
let ComponentPageMap = {
|
||||
'login_user': LoginPage,
|
||||
'forgot_password': ForgotPasswordPage,
|
||||
'reset_password': PasswordResetPage,
|
||||
'mfa_register': MfaRegisterPage,
|
||||
'mfa_validate': MfaValidatePage,
|
||||
};
|
||||
|
||||
const Page = ComponentPageMap[pageName];
|
||||
|
||||
if(Page) {
|
||||
ReactDOM.render(
|
||||
<Theme>
|
||||
<SnackbarProvider
|
||||
maxSnack={5}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}>
|
||||
<Page {...pageProps} {...otherProps} />
|
||||
</SnackbarProvider>
|
||||
</Theme>
|
||||
, document.querySelector('#root'));
|
||||
} else {
|
||||
ReactDOM.render(
|
||||
<h1>Invalid Page</h1>
|
||||
, document.querySelector('#root'));
|
||||
}
|
||||
};
|
@ -760,17 +760,19 @@ const customReactSelectStyles = (theme, readonly) => ({
|
||||
}),
|
||||
});
|
||||
|
||||
function OptionView({ image, label }) {
|
||||
function OptionView({ image, imageUrl, label }) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
{image && <span className={clsx(classes.optionIcon, image)}></span>}
|
||||
{imageUrl && <img style={{height: '20px', marginRight: '4px'}} src={imageUrl} />}
|
||||
<span>{label}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
OptionView.propTypes = {
|
||||
image: PropTypes.string,
|
||||
imageUrl: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -787,7 +789,7 @@ CustomSelectInput.propTypes = {
|
||||
function CustomSelectOption(props) {
|
||||
return (
|
||||
<RSComponents.Option {...props}>
|
||||
<OptionView image={props.data.image} label={props.data.label} />
|
||||
<OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />
|
||||
</RSComponents.Option>
|
||||
);
|
||||
}
|
||||
@ -798,7 +800,7 @@ CustomSelectOption.propTypes = {
|
||||
function CustomSelectSingleValue(props) {
|
||||
return (
|
||||
<RSComponents.SingleValue {...props}>
|
||||
<OptionView image={props.data.image} label={props.data.label} />
|
||||
<OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />
|
||||
</RSComponents.SingleValue>
|
||||
);
|
||||
}
|
||||
@ -1111,9 +1113,11 @@ const useStylesFormFooter = makeStyles((theme) => ({
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
message: {
|
||||
color: theme.palette.text.primary,
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
messageCenter: {
|
||||
color: theme.palette.text.primary,
|
||||
margin: 'auto',
|
||||
},
|
||||
closeButton: {
|
||||
|
@ -76,7 +76,7 @@ export function initializeModalProvider(modalContainer) {
|
||||
);
|
||||
}
|
||||
|
||||
const FinalNotifyContent = React.forwardRef(({children}, ref) => {
|
||||
export const FinalNotifyContent = React.forwardRef(({children}, ref) => {
|
||||
return <SnackbarContent style= {{justifyContent:'end', maxWidth: '700px'}} ref={ref}>{children}</SnackbarContent>;
|
||||
});
|
||||
FinalNotifyContent.displayName = 'FinalNotifyContent';
|
||||
|
@ -1,12 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7]>
|
||||
<html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
|
||||
<!--[if IE 7]>
|
||||
<html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
|
||||
<!--[if IE 8]>
|
||||
<html class="no-js lt-ie9" lang="en"> <![endif]-->
|
||||
<!--[if gt IE 8]><!-->
|
||||
<html class="no-js" lang="en"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
@ -17,6 +9,32 @@
|
||||
|
||||
<!-- To set pgAdmin4 shortcut icon in browser -->
|
||||
<link rel="shortcut icon" href="{{ url_for('redirects.favicon') }}"/>
|
||||
<style>
|
||||
.pg-sp-container {
|
||||
position: absolute;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
background: rgba(#000,0.6);
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
}
|
||||
.pg-sp-container .pg-sp-content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 40%;
|
||||
}
|
||||
.pg-sp-icon {
|
||||
background: url("data:image/svg+xml;charset=UTF-8,%3c?xml version='1.0' encoding='utf-8'?%3e%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 38 38' style='enable-background:new 0 0 38 38;' xml:space='preserve'%3e%3cstyle type='text/css'%3e .st0%7bfill:none;stroke:%23ebeef3;stroke-width:2;%7d .st1%7bfill:none;stroke:%23326690;stroke-width:2;%7d %3c/style%3e%3cg%3e%3cg transform='translate(1 1)'%3e%3ccircle class='st0' cx='18' cy='18' r='18'/%3e%3cpath class='st1' d='M36,18c0-9.9-8.1-18-18-18 '%3e%3canimateTransform accumulate='none' additive='replace' attributeName='transform' calcMode='linear' dur='0.7s' fill='remove' from='0 18 18' repeatCount='indefinite' restart='always' to='360 18 18' type='rotate'%3e%3c/animateTransform%3e%3c/path%3e%3c/g%3e%3c/g%3e%3c/svg%3e") center center no-repeat;
|
||||
height: 75px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.pg-sp-text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Base template stylesheets -->
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/generated/style.css')}}"/>
|
||||
@ -31,10 +49,9 @@
|
||||
/* This is used to change publicPath of webpack at runtime */
|
||||
window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/";
|
||||
</script>
|
||||
<!-- Base template scripts -->
|
||||
{% if requirejs is defined and requirejs is true %}
|
||||
<script type="application/javascript"
|
||||
src="{{ url_for('static', filename='vendor/require/require.js' if config.DEBUG else 'vendor/require/require.min.js') }}"></script>
|
||||
src="{{ url_for('static', filename='vendor/require/require.js' if config.DEBUG else 'vendor/require/require.min.js') }}"></script>
|
||||
<!-- Base template scripts -->
|
||||
<script type="application/javascript">
|
||||
require.config({
|
||||
baseUrl: '',
|
||||
@ -53,18 +70,17 @@
|
||||
'pgadmin.browser.constants': "{{ url_for('browser.index') }}" + "js/constants",
|
||||
'pgadmin.server.supported_servers': "{{ url_for('browser.index') }}" + "server/supported_servers",
|
||||
'pgadmin.user_management.current_user': "{{ url_for('user_management.index') }}" + "current_user",
|
||||
'translations': "{{ url_for('tools.index') }}" + "translations"
|
||||
'translations': "{{ url_for('tools.index') }}" + "translations",
|
||||
'security.pages': "{{ url_for('static', filename='js/generated/security.pages') }}"
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if basejs is defined and basejs is true %}
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
|
||||
<!-- View specified scripts -->
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.others.js') }}" ></script>
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script>
|
||||
{% endif %}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
@ -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" %}
|
||||
{% block panel_image %}
|
||||
<div class="p-5">
|
||||
<img src="{{ url_for('static', filename='img/forgot_password.svg') }}" alt="{{ _('Forgot Password') }}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block panel_title %}{{ _('Recover Password', appname=config.APP_NAME) }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
{% if config.SERVER_MODE %}
|
||||
<p>{{ _('Enter the email address for the user account you wish to recover the password for:') }}</p>
|
||||
<form action="{{ url_for('browser.forgot_password') }}" method="POST" name="forgot_password_form">
|
||||
{{ forgot_password_form.hidden_tag() }}
|
||||
<fieldset>
|
||||
<legend class="skip-navigation">{{ _('Forget Password Form') }}</legend>
|
||||
{{ render_field_with_errors(forgot_password_form.email, "text") }}
|
||||
<input class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Recover Password') }}"
|
||||
title="{{ _('Recover Password') }}">
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% set page_name = 'forgot_password' %}
|
||||
{% set page_props = {
|
||||
'actionUrl': url_for('browser.forgot_password'),
|
||||
'csrfToken': csrf_token(),
|
||||
} %}
|
||||
{% extends "security/render_page.html" %}
|
||||
{% block title %}{{ _('Recover Password') }}{% endblock %}
|
||||
|
||||
|
@ -1,43 +1,22 @@
|
||||
{% extends "security/panel.html" %}
|
||||
{% block panel_image %}
|
||||
<div class="pr-4">
|
||||
<img src="{{ url_for('static', filename='img/login.svg') }}" alt="{{ _('Login') }}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block panel_title %}{{ _('Login') }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
{% if config.SERVER_MODE %}
|
||||
<form action="{{ url_for('authenticate.login') }}" method="POST" name="login_user_form">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
||||
{% set show_login_form = not ((config.OAUTH2 in config.AUTHENTICATION_SOURCES or config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 1
|
||||
or (config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.KERBEROS in config.AUTHENTICATION_SOURCES) and config.AUTHENTICATION_SOURCES | length == 2) %}
|
||||
{% if show_login_form %}
|
||||
{{ render_username_with_errors(login_user_form.email, "text") }}
|
||||
{{ render_field_with_errors(login_user_form.password, "password") }}
|
||||
<button name="internal_button" class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
|
||||
{% endif %}
|
||||
<div class="form-group row mb-3 c user-language">
|
||||
{% if show_login_form %}
|
||||
<div class="col-7">
|
||||
<span class="help-block">{{ _('<a href="%(url)s" class="text-white">Forgotten your password</a>?', url=url_for('browser.forgot_password')) }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="{{'col-5' if show_login_form else 'col-12'}}">
|
||||
<select class="form-control" name="language" value="{{user_language}}">
|
||||
{% for key, lang in config.LANGUAGES.items() %}
|
||||
<option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if config.OAUTH2 in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES %}
|
||||
{% for oauth_config in config.OAUTH2_CONFIG %}
|
||||
<button name="oauth2_button" class="btn btn-primary btn-block btn-oauth" style="background-color: {{oauth_config['OAUTH2_BUTTON_COLOR']}}" value="{{ oauth_config['OAUTH2_NAME'] }}" type="submit">
|
||||
<i class="fab {{ oauth_config['OAUTH2_ICON'] }} fa-lg mr-2" aria-hidden="true" role="image"></i>
|
||||
{{ _('Login with %(oauth_name)s', oauth_name=oauth_config['OAUTH2_DISPLAY_NAME']) }}</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% set page_name = 'login_user' %}
|
||||
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
||||
{% set ns = namespace(langOptions=[]) %}
|
||||
{% for key, lang in config.LANGUAGES.items() %}
|
||||
{% set _ = ns.langOptions.append({'value': key, 'label': lang}) %}
|
||||
{% endfor %}
|
||||
{% set page_props = {
|
||||
'userLanguage': user_language,
|
||||
'langOptions': ns.langOptions,
|
||||
'forgotPassUrl': url_for('browser.forgot_password'),
|
||||
'loginUrl': url_for('authenticate.login'),
|
||||
'csrfToken': csrf_token(),
|
||||
'authSources': config.AUTHENTICATION_SOURCES,
|
||||
'authSourcesEnum': {
|
||||
'OAUTH2': config.OAUTH2,
|
||||
'KERBEROS': config.KERBEROS,
|
||||
},
|
||||
'oauth2Config': config.OAUTH2_CONFIG,
|
||||
'loginBanner': config.LOGIN_BANNER|safe
|
||||
} %}
|
||||
{% extends "security/render_page.html" %}
|
||||
{% block title %}{{ _('Login') }}{% endblock %}
|
||||
|
@ -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 %}
|
32
web/pgadmin/templates/security/render_page.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% set other_props = {
|
||||
'messages': get_flashed_messages(with_categories=true)
|
||||
} %}
|
||||
{% block body %}
|
||||
<style>
|
||||
#root:not(:empty) + .pg-sp-container {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<div id="root" style="height: 100%"></div>
|
||||
<div class="pg-sp-container">
|
||||
<div class="pg-sp-content">
|
||||
<div class="pg-sp-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block init_script %}
|
||||
try {
|
||||
require(
|
||||
['security.pages'],
|
||||
function() {
|
||||
window.renderSecurityPage('{{page_name}}', {{page_props|tojson|safe}},
|
||||
{{other_props|tojson|safe}});
|
||||
}, function() {
|
||||
console.log(arguments);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
{% endblock %}
|
@ -1,17 +1,8 @@
|
||||
{% extends "security/panel.html" %}
|
||||
{% block panel_title %}{{ _('%(appname)s Password Reset', appname=config.APP_NAME) }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
{% if config.SERVER_MODE %}
|
||||
<form action="{{ url_for('browser.reset_password', token=reset_password_token) }}" method="POST"
|
||||
name="reset_password_form">
|
||||
{{ reset_password_form.hidden_tag() }}
|
||||
<fieldset>
|
||||
<legend class="skip-navigation">{{ _('Reset Password Form') }}</legend>
|
||||
{{ render_field_with_errors(reset_password_form.password, "password") }}
|
||||
{{ render_field_with_errors(reset_password_form.password_confirm, "password") }}
|
||||
<input class="btn btn-lg btn-success btn-block" type="submit" value="{{ _('Reset Password') }}"
|
||||
title="{{ _('Reset Password') }}">
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% set page_name = 'reset_password' %}
|
||||
{% set page_props = {
|
||||
'actionUrl': url_for('browser.reset_password', token=reset_password_token),
|
||||
'csrfToken': csrf_token(),
|
||||
} %}
|
||||
{% extends "security/render_page.html" %}
|
||||
{% block title %}{{ _('Reset Password') }}{% endblock %}
|
||||
|
||||
|
@ -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,
|
||||
client_platform=user_agent.platform,
|
||||
function_name_with_arguments=function_name_with_arguments,
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
layout=layout
|
||||
)
|
||||
|
||||
|
@ -491,8 +491,6 @@ def panel(trans_id):
|
||||
return render_template(
|
||||
"erd/index.html",
|
||||
title=underscore_unescape(params['title']),
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
params=json.dumps(params),
|
||||
)
|
||||
|
||||
|
@ -113,8 +113,6 @@ def panel(trans_id):
|
||||
title=underscore_unescape(params['title']),
|
||||
theme=params['theme'],
|
||||
o_db_name=o_db_name,
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
platform=_platform
|
||||
)
|
||||
|
||||
|
@ -127,8 +127,6 @@ def panel(trans_id, editor_title):
|
||||
"schema_diff/index.html",
|
||||
_=gettext,
|
||||
trans_id=trans_id,
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
editor_title=editor_title,
|
||||
)
|
||||
|
||||
|
@ -333,8 +333,6 @@ def panel(trans_id):
|
||||
"sqleditor/index.html",
|
||||
title=underscore_unescape(params['title']),
|
||||
params=json.dumps(params),
|
||||
requirejs=True,
|
||||
basejs=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -9,10 +9,9 @@
|
||||
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import { showUrlDialog } from '../../../../static/js/Dialogs/index';
|
||||
import { showChangeUserPassword, showUrlDialog } from '../../../../static/js/Dialogs/index';
|
||||
import { showUserManagement } from './UserManagementDialog';
|
||||
|
||||
|
||||
class UserManagement {
|
||||
static instance;
|
||||
|
||||
@ -31,7 +30,7 @@ class UserManagement {
|
||||
|
||||
// This is a callback function to show change user dialog.
|
||||
change_password(url) {
|
||||
showUrlDialog(gettext('Change Password'), url, 'change_user_password.html', undefined, pgAdmin.Browser.stdH.lg);
|
||||
showChangeUserPassword(url);
|
||||
}
|
||||
|
||||
// This is a callback function to show 2FA dialog.
|
||||
|
@ -132,3 +132,11 @@ KEY_RING_SERVICE_NAME = 'pgAdmin4'
|
||||
KEY_RING_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}'
|
||||
KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}'
|
||||
KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}'
|
||||
|
||||
|
||||
class MessageType:
|
||||
SUCCESS = 'Success',
|
||||
ERROR = 'Error',
|
||||
INFO = 'Info',
|
||||
CLOSE = 'Close',
|
||||
WARNING = 'Warning'
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
100
web/regression/javascript/SecurityPages/LoginPage.spec.js
Normal file
@ -0,0 +1,100 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import Theme from '../../../pgadmin/static/js/Theme';
|
||||
import LoginPage from '../../../pgadmin/static/js/SecurityPages/LoginPage';
|
||||
|
||||
describe('LoginPage', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
// spyOn(Notify, 'alert');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
let ctrlMount = (props)=>{
|
||||
return mount(<Theme>
|
||||
<LoginPage {...props}/>
|
||||
</Theme>);
|
||||
};
|
||||
|
||||
it('internal', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
userLanguage: 'en',
|
||||
langOptions: [{
|
||||
label: 'English',
|
||||
value: 'en',
|
||||
}],
|
||||
forgotPassUrl: '/forgot/url',
|
||||
csrfToken: 'some-token',
|
||||
loginUrl: '/login/url',
|
||||
authSources: ['internal'],
|
||||
authSourcesEnum: {
|
||||
OAUTH2: 'oauth2',
|
||||
KERBEROS: 'kerberos'
|
||||
},
|
||||
oauth2Config: [],
|
||||
loginBanner: 'login banner'
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/login/url');
|
||||
expect(ctrl.find('input[name="email"]')).toExist();
|
||||
expect(ctrl.find('input[name="password"]')).toExist();
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('oauth2', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
userLanguage: 'en',
|
||||
langOptions: [{
|
||||
label: 'English',
|
||||
value: 'en',
|
||||
}],
|
||||
forgotPassUrl: '/forgot/url',
|
||||
csrfToken: 'some-token',
|
||||
loginUrl: '/login/url',
|
||||
authSources: ['internal', 'oauth2'],
|
||||
authSourcesEnum: {
|
||||
OAUTH2: 'oauth2',
|
||||
KERBEROS: 'kerberos'
|
||||
},
|
||||
oauth2Config: [{
|
||||
OAUTH2_NAME: 'github',
|
||||
OAUTH2_BUTTON_COLOR: '#fff',
|
||||
OAUTH2_ICON: 'fa-github',
|
||||
OAUTH2_DISPLAY_NAME: 'Github'
|
||||
}],
|
||||
loginBanner: ''
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/login/url');
|
||||
expect(ctrl.find('input[name="email"]')).toExist();
|
||||
expect(ctrl.find('input[name="password"]')).toExist();
|
||||
expect(ctrl.find('button[name="oauth2_button"]')).toHaveProp('value', 'github');
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
196
web/regression/javascript/SecurityPages/MfaRegisterPage.spec.js
Normal file
@ -0,0 +1,196 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import Theme from '../../../pgadmin/static/js/Theme';
|
||||
import MfaRegisterPage from '../../../pgadmin/static/js/SecurityPages/MfaRegisterPage';
|
||||
|
||||
describe('MfaRegisterPage', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
// spyOn(Notify, 'alert');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
let ctrlMount = (props)=>{
|
||||
return mount(<Theme>
|
||||
<MfaRegisterPage {...props}/>
|
||||
</Theme>);
|
||||
};
|
||||
|
||||
it('email registered', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/register',
|
||||
mfaList: [{
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
registered: true,
|
||||
},{
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
registered: false,
|
||||
}],
|
||||
nextUrl: '',
|
||||
mfaView: null,
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/register');
|
||||
expect(ctrl.find('EmailRegisterView')).not.toExist();
|
||||
expect(ctrl.find('AuthenticatorRegisterView')).not.toExist();
|
||||
expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(1);
|
||||
expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(1);
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('both registered', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/register',
|
||||
mfaList: [{
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
registered: true,
|
||||
},{
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
registered: true,
|
||||
}],
|
||||
nextUrl: '',
|
||||
mfaView: null,
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/register');
|
||||
expect(ctrl.find('EmailRegisterView')).not.toExist();
|
||||
expect(ctrl.find('AuthenticatorRegisterView')).not.toExist();
|
||||
expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(2);
|
||||
expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0);
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('email view register', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/register',
|
||||
mfaList: [{
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
registered: false,
|
||||
},{
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
registered: false,
|
||||
}],
|
||||
nextUrl: '',
|
||||
mfaView: {
|
||||
label: 'email_authentication_label',
|
||||
auth_method: 'email',
|
||||
description:'Enter the email address to send a code',
|
||||
email_address_placeholder:'Email address',
|
||||
email_address:'email@test.com',
|
||||
note_label:'Note',
|
||||
note:'This email address will only be used for two factor'
|
||||
},
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/register');
|
||||
expect(ctrl.find('EmailRegisterView')).toExist();
|
||||
expect(ctrl.find('input[name="send_to"]')).toExist();
|
||||
expect(ctrl.find('AuthenticatorRegisterView')).not.toExist();
|
||||
expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0);
|
||||
expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0);
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('email view otp code', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/register',
|
||||
mfaList: [{
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
registered: false,
|
||||
},{
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
registered: false,
|
||||
}],
|
||||
nextUrl: '',
|
||||
mfaView: {
|
||||
label: 'email_authentication_label',
|
||||
auth_method: 'email',
|
||||
description:'Enter the email address to send a code',
|
||||
otp_placeholder:'Enter OTP',
|
||||
email_address:'email@test.com',
|
||||
note_label:'Note',
|
||||
note:'This email address will only be used for two factor'
|
||||
},
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/register');
|
||||
expect(ctrl.find('EmailRegisterView')).toExist();
|
||||
expect(ctrl.find('input[name="code"]')).toExist();
|
||||
expect(ctrl.find('AuthenticatorRegisterView')).not.toExist();
|
||||
expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0);
|
||||
expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0);
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('authenticator view register', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/register',
|
||||
mfaList: [{
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
registered: false,
|
||||
},{
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
registered: false,
|
||||
}],
|
||||
nextUrl: '',
|
||||
mfaView: {
|
||||
auth_title:'_TOTP_AUTHENTICATOR',
|
||||
auth_method: 'authenticator',
|
||||
image: 'image',
|
||||
qrcode_alt_text: 'TOTP Authenticator QRCode',
|
||||
auth_description: 'Scan the QR code and the enter the code',
|
||||
otp_placeholder: 'Enter code'
|
||||
},
|
||||
});
|
||||
setTimeout(()=>{
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/register');
|
||||
expect(ctrl.find('EmailRegisterView')).not.toExist();
|
||||
expect(ctrl.find('AuthenticatorRegisterView')).toExist();
|
||||
expect(ctrl.find('input[name="code"]')).toExist();
|
||||
expect(ctrl.find('SecurityButton[value="DELETE"]').length).toBe(0);
|
||||
expect(ctrl.find('SecurityButton[value="SETUP"]').length).toBe(0);
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
128
web/regression/javascript/SecurityPages/MfaValidatePage.spec.js
Normal file
@ -0,0 +1,128 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import Theme from '../../../pgadmin/static/js/Theme';
|
||||
import MfaValidatePage from '../../../pgadmin/static/js/SecurityPages/MfaValidatePage';
|
||||
|
||||
describe('MfaValidatePage', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
// spyOn(Notify, 'alert');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
let ctrlMount = (props)=>{
|
||||
return mount(<Theme>
|
||||
<MfaValidatePage {...props}/>
|
||||
</Theme>);
|
||||
};
|
||||
|
||||
it('email selected', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/validate',
|
||||
views: {
|
||||
'email': {
|
||||
id: 'email',
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
selected: true,
|
||||
view: {
|
||||
description: 'description',
|
||||
otp_placeholder: 'otp_placeholder',
|
||||
button_label: 'button_label',
|
||||
button_label_sending: 'button_label_sending'
|
||||
}
|
||||
},
|
||||
'authenticator': {
|
||||
id: 'authenticator',
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
selected: false,
|
||||
view: {
|
||||
auth_description: 'auth_description',
|
||||
otp_placeholder: 'otp_placeholder',
|
||||
}
|
||||
}
|
||||
},
|
||||
logoutUrl: '/logout/url',
|
||||
sendEmailUrl: '/send/email',
|
||||
csrfHeader: 'csrfHeader',
|
||||
csrfToken: 'csrfToken',
|
||||
});
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/validate');
|
||||
expect(ctrl.find('EmailValidateView')).toExist();
|
||||
expect(ctrl.find('AuthenticatorValidateView')).not.toExist();
|
||||
expect(ctrl.find('button[name="send_code"]')).toExist();
|
||||
expect(ctrl.find('input[name="mfa_method"]').instance().value).toBe('email');
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('authenticator selected', (done)=>{
|
||||
const ctrl = ctrlMount({
|
||||
actionUrl: '/mfa/validate',
|
||||
views: {
|
||||
'email': {
|
||||
id: 'email',
|
||||
label: 'Email',
|
||||
icon: '',
|
||||
selected: false,
|
||||
view: {
|
||||
description: 'description',
|
||||
otp_placeholder: 'otp_placeholder',
|
||||
button_label: 'button_label',
|
||||
button_label_sending: 'button_label_sending'
|
||||
}
|
||||
},
|
||||
'authenticator': {
|
||||
id: 'authenticator',
|
||||
label: 'Authenticator',
|
||||
icon: '',
|
||||
selected: true,
|
||||
view: {
|
||||
auth_description: 'auth_description',
|
||||
otp_placeholder: 'otp_placeholder',
|
||||
}
|
||||
}
|
||||
},
|
||||
logoutUrl: '/logout/url',
|
||||
sendEmailUrl: '/send/email',
|
||||
csrfHeader: 'csrfHeader',
|
||||
csrfToken: 'csrfToken',
|
||||
});
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('form')).toHaveProp('action', '/mfa/validate');
|
||||
expect(ctrl.find('EmailValidateView')).not.toExist();
|
||||
expect(ctrl.find('AuthenticatorValidateView')).toExist();
|
||||
expect(ctrl.find('input[name="code"]')).toExist();
|
||||
expect(ctrl.find('input[name="mfa_method"]').instance().value).toBe('authenticator');
|
||||
ctrl.unmount();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
@ -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
|
||||
//
|
||||
|
@ -46,7 +46,7 @@ const providePlugin = new webpack.ProvidePlugin({
|
||||
'moment': 'moment',
|
||||
'window.moment':'moment',
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
});
|
||||
|
||||
// Helps in debugging each single file, it extracts the module files
|
||||
@ -361,6 +361,7 @@ module.exports = [{
|
||||
entry: {
|
||||
'app.bundle': sourceDir + '/bundle/app.js',
|
||||
codemirror: sourceDir + '/bundle/codemirror.js',
|
||||
'security.pages': 'security.pages',
|
||||
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
|
||||
schema_diff: './pgadmin/tools/schema_diff/static/js/index.js',
|
||||
erd_tool: './pgadmin/tools/erd/static/js/index.js',
|
||||
|
@ -37,6 +37,7 @@ let webpackShimConfig = {
|
||||
'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
|
||||
'tools': path.join(__dirname, './pgadmin/tools/'),
|
||||
'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'),
|
||||
'security.pages': path.join(__dirname, './pgadmin/static/js/SecurityPages/index.jsx'),
|
||||
|
||||
// Vendor JS
|
||||
'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker.min'),
|
||||
|