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

This commit is contained in:
Aditya Toshniwal
2023-06-30 16:08:33 +05:30
committed by GitHub
parent 732bcc2b4d
commit d6cddd8c29
59 changed files with 1354 additions and 663 deletions

View File

@@ -27,7 +27,8 @@ from flask_socketio import disconnect, ConnectionRefusedError
from pgadmin.model import db, User
from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP,\
MessageType
from pgadmin.authenticate.registry import AuthSourceRegistry
MODULE_NAME = 'authenticate'
@@ -132,7 +133,7 @@ def _login():
if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
flash(gettext('Your account is locked. Please contact the '
'Administrator.'),
'warning')
MessageType.WARNING)
logout_user()
return redirect(get_post_logout_redirect())
@@ -158,7 +159,7 @@ def _login():
if flash_login_attempt_error:
error = error + flash_login_attempt_error
flash_login_attempt_error = None
flash(error, 'warning')
flash(error, MessageType.WARNING)
return redirect(get_post_logout_redirect())
@@ -175,7 +176,7 @@ def _login():
return redirect('{0}?next={1}'.format(url_for(
'authenticate.kerberos_login'), url_for('browser.index')))
flash(msg, 'danger')
flash(msg, MessageType.ERROR)
return redirect(get_post_logout_redirect())
session['auth_source_manager'] = current_auth_obj
@@ -194,7 +195,7 @@ def _login():
return msg
if 'auth_obj' in session:
session.pop('auth_obj')
flash(msg, 'danger')
flash(msg, MessageType.ERROR)
form_class = _security.forms.get('login_form').cls
form = form_class()
@@ -268,7 +269,7 @@ class AuthSourceManager:
if status:
return True
if err_msg:
flash(err_msg, 'warning')
flash(err_msg, MessageType.WARNING)
return False
def authenticate(self):

View File

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

View File

@@ -24,6 +24,7 @@ from pgadmin.model import UserMFA
from .registry import BaseMFAuth
from .utils import ValidationException, fetch_auth_option, mfa_add
from pgadmin.utils.constants import MessageType
_TOTP_AUTH_METHOD = "authenticator"
@@ -119,14 +120,7 @@ class TOTPAuthenticator(BaseMFAuth):
Returns:
str: Authentication view as a string
"""
return (
"<div class='form-group'>{auth_description}</div>"
"<div class='form-group'>"
" <input class='form-control' placeholder='{otp_placeholder}'"
" name='code' type='password' autofocus='' pattern='\\d*'"
" autocomplete='one-time-code' require/>"
"</div>"
).format(
return dict(
auth_description=_(
"Enter the code shown in your authenticator application for "
"TOTP (Time-based One-Time Password)"
@@ -162,6 +156,17 @@ class TOTPAuthenticator(BaseMFAuth):
img.save(buffered, format="JPEG")
img_base64 = base64.b64encode(buffered.getvalue())
return dict(
auth_title=_(_TOTP_AUTHENTICATOR),
auth_method=_TOTP_AUTH_METHOD,
image=img_base64.decode("utf-8"),
qrcode_alt_text=_("TOTP Authenticator QRCode"),
auth_description=_(
"Scan the QR code and the enter the code from the "
"TOTP Authenticator application"
), otp_placeholder=_("Enter code")
)
return "".join([
"<h5 class='form-group text-center'>{auth_title}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
@@ -210,13 +215,13 @@ class TOTPAuthenticator(BaseMFAuth):
authenticator_opt = session.get('mfa_authenticator_opt', None)
if authenticator_opt is None or \
pyotp.TOTP(authenticator_opt).verify(code) is False:
flash(_("Failed to validate the code"), "danger")
flash(_("Failed to validate the code"), MessageType.ERROR)
return self._registration_view()
mfa_add(_TOTP_AUTH_METHOD, authenticator_opt)
flash(_(
"TOTP Authenticator registered successfully for authentication."
), "success")
), MessageType.SUCCESS)
session.pop('mfa_authenticator_opt', None)
return None

View File

@@ -18,6 +18,7 @@ import config
from pgadmin.utils.csrf import pgCSRFProtect
from .registry import BaseMFAuth
from .utils import ValidationException, mfa_add, fetch_auth_option
from pgadmin.utils.constants import MessageType
def __generate_otp() -> str:
@@ -154,10 +155,7 @@ def send_email_code() -> Response:
if success is False:
return Response(message, http_code, mimetype='text/html')
return Response(render_template(
"mfa/email_code_sent.html", _=_,
message=message,
), http_code, mimetype='text/html')
return dict(message=message)
@pgCSRFProtect.exempt
@@ -204,28 +202,15 @@ class EmailAuthentication(BaseMFAuth):
def validation_view(self):
session.pop("mfa_email_code", None)
return render_template(
"mfa/email_view.html", _=_
return dict(
description=_("Verify with Email Authentication"),
button_label=_("Send Code"),
button_label_sending=_("Sending Code...")
)
def _registration_view(self):
email = getattr(current_user, 'email', '')
return "\n".join([
"<h5 class='form-group text-center'>{label}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
"<input type='hidden' name='validate' value='send_code'/>",
"<div class='form-group pt-3'>{description}</div>",
"<div class='form-group'>",
" <input class='form-control' name='send_to' type='email'",
" placeholder='{email_address_placeholder}'",
" autofocus='' value='{email_address}' required",
" pattern='[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{{2,}}$'/>",
"</div></div>",
"<div class='alert alert-warning alert-dismissible fade show'",
" role='alert'>",
" <strong>{note_label}:</strong><span>{note}</span>",
"</div>",
]).format(
return dict(
label=email_authentication_label(),
auth_method=EMAIL_AUTH_METHOD,
description=_("Enter the email address to send a code"),
@@ -247,20 +232,10 @@ class EmailAuthentication(BaseMFAuth):
)
if success is False:
flash(message, 'danger')
flash(message, MessageType.ERROR)
return None
return "\n".join([
"<h5 class='form-group text-center'>{label}</h5>",
"<input type='hidden' name='{auth_method}' value='SETUP'/>",
"<input type='hidden' name='validate' value='verify_code'/>",
"<div class='form-group pt-3'>{message}</div>",
"<div class='form-group'>",
" <input class='form-control' placeholder='{otp_placeholder}'",
" pattern='\\d{{6}}' type='password' autofocus=''",
" autocomplete='one-time-code' name='code' require>",
"</div>",
]).format(
return dict(
label=email_authentication_label(),
auth_method=EMAIL_AUTH_METHOD,
message=message,
@@ -282,13 +257,13 @@ class EmailAuthentication(BaseMFAuth):
flash(_(
"Email Authentication registered successfully."
), "success")
), MessageType.SUCCESS)
session.pop('mfa_email_code', None)
return None
flash(_('Invalid code'), 'danger')
flash(_('Invalid code'), MessageType.ERROR)
return self._registration_view()

View File

@@ -1,66 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
let mfa_form_elem = document.getElementById('mfa_form');
if (mfa_form_elem)
mfa_form_elem.setAttribute('class', '');
function sendCodeToEmail(data, _json, _callback) {
const URL = '{{ url_for('mfa.send_email_code') }}';
let accept = 'text/html; charset=utf-8;';
let btn_send_code_elem = document.getElementById('btn_send_code');
if (btn_send_code_elem) btn_send_code_elem.disabled = true;
if (!data) {
data = {'code': ''};
}
if (_json) {
accept = 'application/json; charset=utf-8;';
}
clear_error();
fetch(URL, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: {
'Accept': accept,
'Content-Type': 'application/json; charset=utf-8;',
'{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}': '{{ csrf_token() }}'
},
redirect: 'follow',
body: JSON.stringify(data)
}).then((resp) => {
if (_callback) {
setTimeout(() => (_callback(resp)), 1);
return null;
}
if (!resp.ok) {
let btn_send_code_elem = document.getElementById('btn_send_code');
if (btn_send_code_elem) btn_send_code_elem.disabled = true;
resp.text().then(msg => render_error(msg));
return;
}
if (_json) return resp.json();
return resp.text();
}).then((string) => {
if (!string)
return;
document.getElementById("mfa_email_auth").innerHTML = string;
document.getElementById("mfa_form").classList = ["show_validate_btn"];
setTimeout(() => {
document.getElementById("showme").classList = [];
}, 20000);
});
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ from pgadmin.utils.ajax import bad_request
from .utils import user_supported_mfa_methods, mfa_user_registered, \
mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \
is_mfa_session_authenticated
from pgadmin.utils.constants import MessageType
_INDEX_URL = "browser.index"
@@ -118,11 +119,11 @@ def validate_view() -> Response:
"MFA validation failed for the user '{}' with an error: "
"{}"
).format(current_user.username, str(ve)))
flash(str(ve), "danger")
flash(str(ve), MessageType.ERROR)
return_code = 401
except Exception as ex:
current_app.logger.exception(ex)
flash(str(ex), "danger")
flash(str(ex), MessageType.ERROR)
return_code = 500
mfa_views = {
@@ -166,7 +167,8 @@ def _mfa_registration_view(
if form_data[mfa.name] == 'SETUP':
if supported_mfa['registered'] is True:
flash(_("'{}' is already registerd'").format(mfa.label), "success")
flash(_("'{}' is already registerd'").format(mfa.label),
MessageType.SUCCESS)
return None
return mfa.registration_view(form_data)
@@ -174,13 +176,13 @@ def _mfa_registration_view(
if mfa_delete(mfa.name) is True:
flash(_(
"'{}' unregistered from the authentication list."
).format(mfa.label), "success")
).format(mfa.label), MessageType.SUCCESS)
return None
flash(_(
"'{}' is not found in the authentication list."
).format(mfa.label), "warning")
).format(mfa.label), MessageType.WARNING)
return None
@@ -255,7 +257,7 @@ def __handle_registration_view_for_post_method(
if view is False:
if next_url != 'internal':
return None, redirect(next_url), None
flash(_("Please close the dialog."), "info")
flash(_("Please close the dialog."), MessageType.INFO)
if view is not None:
return None, Response(
@@ -336,7 +338,8 @@ def registration_view() -> Response:
)
elif is_mfa_session_authenticated() is False and \
found_one_mfa is True:
flash(_("Complete the authentication process first"), "danger")
flash(_("Complete the authentication process first"),
MessageType.ERROR)
return redirect(login_url("mfa.validate", next_url=next_url))
return Response(render_template(

View File

@@ -21,7 +21,7 @@ from flask_security.utils import get_post_logout_redirect, logout_user
from pgadmin.authenticate.internal import BaseAuthentication
from pgadmin.model import User
from pgadmin.tools.user_management import create_user
from pgadmin.utils.constants import OAUTH2
from pgadmin.utils.constants import OAUTH2, MessageType
from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.model import db
@@ -61,7 +61,7 @@ def init_app(app):
if 'auth_obj' in session:
session.pop('auth_obj')
logout_user()
flash(msg, 'danger')
flash(msg, MessageType.ERROR)
return redirect(get_safe_post_login_redirect())
@blueprint.route('/logout', endpoint="logout",