Merge branch '96-notify-user-about-incorrect-email-pw-combination' into 'main'

Resolve "Notify user about incorrect email/pw combination"

Closes #94 and #96

See merge request stackspin/dashboard!62
This commit is contained in:
Arie Peterson 2022-10-18 12:43:39 +00:00
commit 696ffba9fe
5 changed files with 303 additions and 323 deletions

View file

@ -1,5 +1,12 @@
# Changelog # Changelog
## Unreleased
- Fix login welcome message
- Clarify "set new password" button (#94)
- Show error messages when login fails, for example when a wrong password was
entered (#96)
## [0.5.1] ## [0.5.1]
- Fix bug of missing "Monitoring" app access when creating a new user. - Fix bug of missing "Monitoring" app access when creating a new user.

View file

@ -118,7 +118,13 @@ def login():
identity = get_auth() identity = get_auth()
if identity: if identity:
return render_template("loggedin.html", api_url=KRATOS_PUBLIC_URL, id=id) if 'name' in identity['traits']:
# Add a space in front of the "name" so the template can put it
# between "Welcome" and the comma
name = " " + identity['traits']['name']
else:
name = ""
return render_template("loggedin.html", api_url=KRATOS_PUBLIC_URL, name=name)
flow = request.args.get("flow") flow = request.args.get("flow")

View file

@ -16,7 +16,6 @@
*/ */
// Check if an auth flow is configured and redirect to auth page in that // Check if an auth flow is configured and redirect to auth page in that
// case. // case.
function check_flow_auth() { function check_flow_auth() {
@ -24,7 +23,7 @@ function check_flow_auth() {
var url = Cookies.get('auth_url'); var url = Cookies.get('auth_url');
if (state == 'auth') { if (state == 'auth') {
Cookies.set('flow_state',''); Cookies.set('flow_state', '');
window.location.href = url; window.location.href = url;
} }
} }
@ -34,55 +33,50 @@ function check_flow_expired() {
var state = Cookies.get('flow_state'); var state = Cookies.get('flow_state');
if (state == 'flow_expired') { if (state == 'flow_expired') {
Cookies.set('flow_state',''); Cookies.set('flow_state', '');
$("#contentFlowExpired").show(); $('#contentFlowExpired').show();
} }
} }
// The script executed on login flows // The script executed on login flows
function flow_login() { function flow_login() {
var flow = $.urlParam('flow'); var flow = $.urlParam('flow');
var uri = api_url + 'self-service/login/flows?id=' + flow; var uri = api_url + 'self-service/login/flows?id=' + flow;
// Query the Kratos backend to know what fields to render for the // Query the Kratos backend to know what fields to render for the
// current flow // current flow
$.ajax({ $.ajax({
type: "GET", type: 'GET',
url: uri, url: uri,
success: function(data) { success: function (data) {
// Render login form (group: password) // Render login form (group: password)
var html = render_form(data, 'password'); var form_html = render_form(data, 'password');
$("#contentLogin").html(html); $('#contentLogin').html(form_html);
var messages_html = render_messages(data);
$('#contentMessages').html(messages_html);
}, },
complete: function(obj) { complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow // If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) { if (obj.status == 410) {
Cookies.set('flow_state','flow_expired'); Cookies.set('flow_state', 'flow_expired');
// If we call the page without arguments, we get a new flow // If we call the page without arguments, we get a new flow
window.location.href = 'login'; window.location.href = 'login';
} }
} },
}); });
} }
// This is called after a POST on settings. It tells if the save was // This is called after a POST on settings. It tells if the save was
// successful and display / handles based on that outcome // successful and display / handles based on that outcome
function flow_settings_validate() { function flow_settings_validate() {
var flow = $.urlParam('flow'); var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow; var uri = api_url + 'self-service/settings/flows?id=' + flow;
$.ajax( { $.ajax({
type: "GET", type: 'GET',
url: uri, url: uri,
success: function(data) { success: function (data) {
// We had success. We save that fact in our flow_state // We had success. We save that fact in our flow_state
// cookie and regenerate a new flow // cookie and regenerate a new flow
if (data.state == 'success') { if (data.state == 'success') {
@ -90,41 +84,37 @@ function flow_settings_validate() {
// Redirect to generate new flow ID // Redirect to generate new flow ID
window.location.href = 'settings'; window.location.href = 'settings';
} } else {
else {
// There was an error, Kratos does not specify what is // There was an error, Kratos does not specify what is
// wrong. So we just show the general error message and // wrong. So we just show the general error message and
// let the user figure it out. We can re-use the flow-id // let the user figure it out. We can re-use the flow-id
$("#contentProfileSaveFailed").show(); $('#contentProfileSaveFailed').show();
// For now, this code assumes that only the password can fail // For now, this code assumes that only the password can fail
// validation. Other forms might need to be added in the future. // validation. Other forms might need to be added in the future.
html = render_form(data, 'password') html = render_form(data, 'password');
$("#contentPassword").html(html) $('#contentPassword').html(html);
}
} }
},
}); });
} }
// Render the settings flow, this is where users can change their personal // Render the settings flow, this is where users can change their personal
// settings, like name and password. The form contents are defined by Kratos // settings, like name and password. The form contents are defined by Kratos
function flow_settings() { function flow_settings() {
// Get the details from the current flow from kratos // Get the details from the current flow from kratos
var flow = $.urlParam('flow'); var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow; var uri = api_url + 'self-service/settings/flows?id=' + flow;
$.ajax({ $.ajax({
type: "GET", type: 'GET',
url: uri, url: uri,
success: function(data) { success: function (data) {
var state = Cookies.get('flow_state');
var state = Cookies.get('flow_state')
// If we have confirmation the settings are saved, show the // If we have confirmation the settings are saved, show the
// notification // notification
if (state == 'settings_saved') { if (state == 'settings_saved') {
$("#contentProfileSaved").show(); $('#contentProfileSaved').show();
Cookies.set('flow_state', 'settings'); Cookies.set('flow_state', 'settings');
} }
@ -132,21 +122,19 @@ function flow_settings() {
// so the user is not confused by other fields. The user // so the user is not confused by other fields. The user
// probably want to setup a password only first. // probably want to setup a password only first.
if (state == 'recovery') { if (state == 'recovery') {
$("#contentProfile").hide(); $('#contentProfile').hide();
} }
// Render the password & profile form based on the fields we got // Render the password & profile form based on the fields we got
// from the API // from the API
var html = render_form(data, 'password'); var html = render_form(data, 'password');
$("#contentPassword").html(html); $('#contentPassword').html(html);
html = render_form(data, 'profile'); html = render_form(data, 'profile');
$("#contentProfile").html(html); $('#contentProfile').html(html);
// If the submit button is hit, execute the POST with Ajax. // If the submit button is hit, execute the POST with Ajax.
$("#formpassword").submit(function(e) { $('#formpassword').submit(function (e) {
// avoid to execute the actual submit of the form. // avoid to execute the actual submit of the form.
e.preventDefault(); e.preventDefault();
@ -154,48 +142,40 @@ function flow_settings() {
var url = form.attr('action'); var url = form.attr('action');
$.ajax({ $.ajax({
type: "POST", type: 'POST',
url: url, url: url,
data: form.serialize(), data: form.serialize(),
complete: function(obj) { complete: function (obj) {
// Validate the settings // Validate the settings
flow_settings_validate(); flow_settings_validate();
}, },
}); });
}); });
}, },
complete: function(obj) { complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow // If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) { if (obj.status == 410) {
Cookies.set('flow_state','flow_expired'); Cookies.set('flow_state', 'flow_expired');
window.location.href = 'settings'; window.location.href = 'settings';
} }
},
}
}); });
} }
function flow_recover() { function flow_recover() {
var flow = $.urlParam('flow'); var flow = $.urlParam('flow');
var uri = api_url + 'self-service/recovery/flows?id=' + flow; var uri = api_url + 'self-service/recovery/flows?id=' + flow;
$.ajax( { $.ajax({
type: "GET", type: 'GET',
url: uri, url: uri,
success: function(data) { success: function (data) {
// Render the recover form, method 'link' // Render the recover form, method 'link'
var html = render_form(data, 'link'); var html = render_form(data, 'link');
$("#contentRecover").html(html); $('#contentRecover').html(html);
// Do form post as an AJAX call // Do form post as an AJAX call
$("#formlink").submit(function(e) { $('#formlink').submit(function (e) {
// avoid to execute the actual submit of the form. // avoid to execute the actual submit of the form.
e.preventDefault(); e.preventDefault();
@ -205,30 +185,24 @@ function flow_recover() {
// keep stat we are in recovery // keep stat we are in recovery
Cookies.set('flow_state', 'recovery'); Cookies.set('flow_state', 'recovery');
$.ajax({ $.ajax({
type: "POST", type: 'POST',
url: url, url: url,
data: form.serialize(), // serializes the form's elements. data: form.serialize(), // serializes the form's elements.
success: function(data) success: function (data) {
{
// Show the request is sent out // Show the request is sent out
$("#contentRecover").hide(); $('#contentRecover').hide();
$("#contentRecoverRequested").show(); $('#contentRecoverRequested').show();
}
});
});
}, },
complete: function(obj) { });
});
},
complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow // If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) { if (obj.status == 410) {
Cookies.set('flow_state','flow_expired'); Cookies.set('flow_state', 'flow_expired');
window.location.href = 'recovery'; window.location.href = 'recovery';
}
} }
},
}); });
} }
@ -240,27 +214,40 @@ function flow_recover() {
// data: data object as returned form the API // data: data object as returned form the API
// group: group to render. // group: group to render.
function render_form(data, group) { function render_form(data, group) {
// Create form // Create form
var action = data.ui.action; var action = data.ui.action;
var method = data.ui.method; var method = data.ui.method;
var form = "<form id='form"+group+"' method='"+method+"' action='"+action+"'>"; var form = "<form id='form" + group + "' method='" + method + "' action='" + action + "'>";
for (const node of data.ui.nodes) { for (const node of data.ui.nodes) {
var name = node.attributes.name; var name = node.attributes.name;
var type = node.attributes.type; var type = node.attributes.type;
var value = node.attributes.value; var value = node.attributes.value;
var messages = node.messages var messages = node.messages;
if (node.group == 'default' || node.group == group) { if (node.group == 'default' || node.group == group) {
var elm = getFormElement(type, name, value, messages); var elm = getFormElement(type, name, value, messages);
form += elm; form += elm;
} }
} }
form += "</form>"; form += '</form>';
return form; return form;
}
// Check if there are any general messages to show to the user and render them
function render_messages(data) {
var messages = data.ui.messages;
if (messages == []) {
return '';
}
var html = '<ul>';
messages.forEach((message) => {
html += '<li>';
html += message.text;
html += '</li>';
});
html += '</ul>';
return html;
} }
// Return form element based on name, including help text (sub), placeholder etc. // Return form element based on name, including help text (sub), placeholder etc.
@ -274,14 +261,14 @@ function render_form(data, group) {
// value: If there is already a value known, show it // value: If there is already a value known, show it
// messages: error messages related to the field // messages: error messages related to the field
function getFormElement(type, name, value, messages) { function getFormElement(type, name, value, messages) {
console.log("Getting form element", type, name, value, messages) console.log('Getting form element', type, name, value, messages);
if (value == undefined) { if (value == undefined) {
value = ''; value = '';
} }
if (typeof(messages) == "undefined") { if (typeof messages == 'undefined') {
messages = [] messages = [];
} }
if (name == 'email' || name == 'traits.email') { if (name == 'email' || name == 'traits.email') {
@ -291,37 +278,19 @@ function getFormElement(type, name, value, messages) {
value, value,
'E-mail address', 'E-mail address',
'Please enter your e-mail address here', 'Please enter your e-mail address here',
'Please provide your e-mail address. We will send a recovery ' + 'Please provide your e-mail address. We will send a recovery link to that e-mail address.',
'link to that e-mail address.',
messages, messages,
); );
} }
if (name == 'traits.username') { if (name == 'traits.username') {
return getFormInput( return getFormInput('name', name, value, 'Username', 'Please provide an username', null, messages);
'name',
name,
value,
'Username',
'Please provide an username',
null,
messages,
);
} }
if (name == 'traits.name') { if (name == 'traits.name') {
return getFormInput( return getFormInput('name', name, value, 'Full name', 'Please provide your full name', null, messages);
'name',
name,
value,
'Full name',
'Please provide your full name',
null,
messages,
);
} }
if (name == 'identifier') { if (name == 'identifier') {
return getFormInput( return getFormInput(
'email', 'email',
@ -335,37 +304,37 @@ function getFormElement(type, name, value, messages) {
} }
if (name == 'password') { if (name == 'password') {
return getFormInput( return getFormInput('password', name, value, 'Password', 'Please provide your password', null, messages);
'password', }
name,
value, if (type == 'hidden' || name == 'traits.uuid') {
'Password', return (
'Please provide your password', `
null, <input type="hidden" class="form-control" id="` +
messages, name +
`"
name="` +
name +
`" value='` +
value +
`'>`
); );
} }
if (type == 'hidden' || name == 'traits.uuid') {
return `
<input type="hidden" class="form-control" id="`+name+`"
name="`+name+`" value='`+value+`'>`;
}
if (type == 'submit') { if (type == 'submit') {
return (
return `<div class="form-group"> `<div class="form-group">
<input type="hidden" name="`+name+`" value="`+value+`"> <input type="hidden" name="` +
name +
`" value="` +
value +
`">
<button type="submit" class="btn btn-primary">Go!</button> <button type="submit" class="btn btn-primary">Go!</button>
</div>`; </div>`
);
} }
return getFormInput('input', name, value, name, null, null, messages);
return getFormInput('input', name, value, name, null,null, messages);
} }
// Usually called by getFormElement, generic function to generate an // Usually called by getFormElement, generic function to generate an
@ -378,41 +347,45 @@ function getFormElement(type, name, value, messages) {
// param help: Additional help text, displayed below the field in small font // param help: Additional help text, displayed below the field in small font
// param messages: Message about failed input // param messages: Message about failed input
function getFormInput(type, name, value, label, placeHolder, help, messages) { function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof(help) == "undefined" || help == null) { if (typeof help == 'undefined' || help == null) {
help = "" help = '';
} }
console.log("Messages: ", messages); console.log('Messages: ', messages);
// Id field for help element // Id field for help element
var nameHelp = name + "Help"; var nameHelp = name + 'Help';
var element = '<div class="form-group">'; var element = '<div class="form-group">';
element += '<label for="'+name+'">'+label+'</label>'; element += '<label for="' + name + '">' + label + '</label>';
element += '<input type="'+type+'" class="form-control" id="'+name+'" name="'+name+'" '; element += '<input type="' + type + '" class="form-control" id="' + name + '" name="' + name + '" ';
// messages get appended to help info // messages get appended to help info
if (messages.length) { if (messages.length) {
for (message in messages) { for (message in messages) {
console.log("adding message", messages[message]) console.log('adding message', messages[message]);
help += messages[message]['text'] help += messages[message]['text'];
} }
} }
// If we are a password field, add a eye icon to reveal password // If we are a password field, add a eye icon to reveal password
if (value) { if (value) {
element += 'value="'+value+'" '; element += 'value="' + value + '" ';
} }
if (help) { if (help) {
element += 'aria-describedby="' + nameHelp +'" '; element += 'aria-describedby="' + nameHelp + '" ';
} }
if (placeHolder) { if (placeHolder) {
element += 'placeholder="'+placeHolder+'" '; element += 'placeholder="' + placeHolder + '" ';
} }
element += ">"; element += '>';
if (help) { if (help) {
element += element +=
`<small id="`+nameHelp+`" class="form-text text-muted">` + help + ` `<small id="` +
nameHelp +
`" class="form-text text-muted">` +
help +
`
</small>`; </small>`;
} }
@ -421,12 +394,10 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
return element; return element;
} }
// $.urlParam get parameters from the URI. Example: id = $.urlParam('id'); // $.urlParam get parameters from the URI. Example: id = $.urlParam('id');
$.urlParam = function(name) { $.urlParam = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) { if (results == null) {
return null; return null;
} }
return decodeURI(results[1]) || 0; return decodeURI(results[1]) || 0;

View file

@ -1,5 +1,3 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
@ -17,11 +15,9 @@
<div id="contentMessages"></div> <div id="contentMessages"></div>
<div id="contentWelcome">Welcome {{ id['name'] }},<br/><br/> <div id="contentWelcome">
You are already logged in. Welcome{{ name }}, you are logged in.
</div> </div>

View file

@ -19,7 +19,7 @@
<div id="contentMessages"></div> <div id="contentMessages"></div>
<div id="contentLogin"></div> <div id="contentLogin"></div>
<div id="contentHelp"> <div id="contentHelp">
<a href='recovery'>Forget password?</a> | <a href='https://stackspin.net'>About stackspin</a> <a href='recovery'>Set new password</a> | <a href='https://stackspin.net'>About stackspin</a>
</div> </div>
{% endblock %} {% endblock %}