Show Kratos general failure messages (for example for r wrong password), fix welcome message

main^2
Maarten de Waard 2022-10-14 14:46:37 +02:00
parent 2fec6dec32
commit 0ed0c66e1d
No known key found for this signature in database
GPG Key ID: 1D3E893A657CC8DA
5 changed files with 305 additions and 323 deletions

View File

@ -1,5 +1,12 @@
# 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]
- Fix bug of missing "Monitoring" app access when creating a new user.

View File

@ -118,7 +118,13 @@ def login():
identity = get_auth()
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")

View File

@ -16,257 +16,244 @@
*/
// Check if an auth flow is configured and redirect to auth page in that
// case.
function check_flow_auth() {
var state = Cookies.get('flow_state');
var url = Cookies.get('auth_url');
var state = Cookies.get('flow_state');
var url = Cookies.get('auth_url');
if (state == 'auth') {
Cookies.set('flow_state','');
window.location.href = url;
}
if (state == 'auth') {
Cookies.set('flow_state', '');
window.location.href = url;
}
}
// Check if there if the flow is expired, if so, reset the cookie
function check_flow_expired() {
var state = Cookies.get('flow_state');
var state = Cookies.get('flow_state');
if (state == 'flow_expired') {
Cookies.set('flow_state','');
$("#contentFlowExpired").show();
}
if (state == 'flow_expired') {
Cookies.set('flow_state', '');
$('#contentFlowExpired').show();
}
}
// The script executed on login flows
function flow_login() {
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/login/flows?id=' + flow;
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/login/flows?id=' + flow;
// Query the Kratos backend to know what fields to render for the
// current flow
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
// Render login form (group: password)
var form_html = render_form(data, 'password');
$('#contentLogin').html(form_html);
// Query the Kratos backend to know what fields to render for the
// current flow
$.ajax({
type: "GET",
url: uri,
success: function(data) {
// Render login form (group: password)
var html = render_form(data, 'password');
$("#contentLogin").html(html);
},
complete: function(obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state','flow_expired');
// If we call the page without arguments, we get a new flow
window.location.href = 'login';
}
}
});
var messages_html = render_messages(data);
$('#contentMessages').html(messages_html);
},
complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state', 'flow_expired');
// If we call the page without arguments, we get a new flow
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
function flow_settings_validate() {
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow;
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow;
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
// We had success. We save that fact in our flow_state
// cookie and regenerate a new flow
if (data.state == 'success') {
Cookies.set('flow_state', 'settings_saved');
$.ajax( {
type: "GET",
url: uri,
success: function(data) {
// Redirect to generate new flow ID
window.location.href = 'settings';
} else {
// There was an error, Kratos does not specify what is
// wrong. So we just show the general error message and
// let the user figure it out. We can re-use the flow-id
$('#contentProfileSaveFailed').show();
// We had success. We save that fact in our flow_state
// cookie and regenerate a new flow
if (data.state == 'success') {
Cookies.set('flow_state', 'settings_saved');
// Redirect to generate new flow ID
window.location.href = 'settings';
}
else {
// There was an error, Kratos does not specify what is
// wrong. So we just show the general error message and
// let the user figure it out. We can re-use the flow-id
$("#contentProfileSaveFailed").show();
// For now, this code assumes that only the password can fail
// validation. Other forms might need to be added in the future.
html = render_form(data, 'password')
$("#contentPassword").html(html)
}
}
});
// For now, this code assumes that only the password can fail
// validation. Other forms might need to be added in the future.
html = render_form(data, 'password');
$('#contentPassword').html(html);
}
},
});
}
// Render the settings flow, this is where users can change their personal
// settings, like name and password. The form contents are defined by Kratos
function flow_settings() {
// Get the details from the current flow from kratos
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow;
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
var state = Cookies.get('flow_state');
// Get the details from the current flow from kratos
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/settings/flows?id=' + flow;
$.ajax({
type: "GET",
url: uri,
success: function(data) {
// If we have confirmation the settings are saved, show the
// notification
if (state == 'settings_saved') {
$('#contentProfileSaved').show();
Cookies.set('flow_state', 'settings');
}
var state = Cookies.get('flow_state')
// Hide prfile section if we are in recovery state
// so the user is not confused by other fields. The user
// probably want to setup a password only first.
if (state == 'recovery') {
$('#contentProfile').hide();
}
// If we have confirmation the settings are saved, show the
// notification
if (state == 'settings_saved') {
$("#contentProfileSaved").show();
Cookies.set('flow_state', 'settings');
}
// Render the password & profile form based on the fields we got
// from the API
var html = render_form(data, 'password');
$('#contentPassword').html(html);
// Hide prfile section if we are in recovery state
// so the user is not confused by other fields. The user
// probably want to setup a password only first.
if (state == 'recovery') {
$("#contentProfile").hide();
}
html = render_form(data, 'profile');
$('#contentProfile').html(html);
// If the submit button is hit, execute the POST with Ajax.
$('#formpassword').submit(function (e) {
// avoid to execute the actual submit of the form.
e.preventDefault();
// Render the password & profile form based on the fields we got
// from the API
var html = render_form(data, 'password');
$("#contentPassword").html(html);
html = render_form(data, 'profile');
$("#contentProfile").html(html);
// If the submit button is hit, execute the POST with Ajax.
$("#formpassword").submit(function(e) {
// avoid to execute the actual submit of the form.
e.preventDefault();
var form = $(this);
var url = form.attr('action');
$.ajax({
type: "POST",
url: url,
data: form.serialize(),
complete: function(obj) {
// Validate the settings
flow_settings_validate();
},
});
});
},
complete: function(obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state','flow_expired');
window.location.href = 'settings';
}
}
});
var form = $(this);
var url = form.attr('action');
$.ajax({
type: 'POST',
url: url,
data: form.serialize(),
complete: function (obj) {
// Validate the settings
flow_settings_validate();
},
});
});
},
complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state', 'flow_expired');
window.location.href = 'settings';
}
},
});
}
function flow_recover() {
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/recovery/flows?id=' + flow;
var flow = $.urlParam('flow');
var uri = api_url + 'self-service/recovery/flows?id=' + flow;
$.ajax( {
type: "GET",
url: uri,
success: function(data) {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
// Render the recover form, method 'link'
var html = render_form(data, 'link');
$('#contentRecover').html(html);
// Render the recover form, method 'link'
var html = render_form(data, 'link');
$("#contentRecover").html(html);
// Do form post as an AJAX call
$('#formlink').submit(function (e) {
// avoid to execute the actual submit of the form.
e.preventDefault();
// Do form post as an AJAX call
$("#formlink").submit(function(e) {
var form = $(this);
var url = form.attr('action');
// avoid to execute the actual submit of the form.
e.preventDefault();
var form = $(this);
var url = form.attr('action');
// keep stat we are in recovery
Cookies.set('flow_state', 'recovery');
$.ajax({
type: "POST",
url: url,
data: form.serialize(), // serializes the form's elements.
success: function(data)
{
// Show the request is sent out
$("#contentRecover").hide();
$("#contentRecoverRequested").show();
}
});
});
},
complete: function(obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state','flow_expired');
window.location.href = 'recovery';
}
}
});
// keep stat we are in recovery
Cookies.set('flow_state', 'recovery');
$.ajax({
type: 'POST',
url: url,
data: form.serialize(), // serializes the form's elements.
success: function (data) {
// Show the request is sent out
$('#contentRecover').hide();
$('#contentRecoverRequested').show();
},
});
});
},
complete: function (obj) {
// If we get a 410, the flow is expired, need to refresh the flow
if (obj.status == 410) {
Cookies.set('flow_state', 'flow_expired');
window.location.href = 'recovery';
}
},
});
}
// Based on Kratos UI data and a group name, get the full form for that group.
// kratos groups elements which belongs together in a group and should be posted
// kratos groups elements which belongs together in a group and should be posted
// at once. The elements in the default group should be part of all other
// groups.
//
// data: data object as returned form the API
// group: group to render.
function render_form(data, group) {
function render_form(data, group) {
// Create form
var action = data.ui.action;
var method = data.ui.method;
var form = "<form id='form" + group + "' method='" + method + "' action='" + action + "'>";
// Create form
var action = data.ui.action;
var method = data.ui.method;
var form = "<form id='form"+group+"' method='"+method+"' action='"+action+"'>";
for (const node of data.ui.nodes) {
var name = node.attributes.name;
var type = node.attributes.type;
var value = node.attributes.value;
var messages = node.messages;
for (const node of data.ui.nodes) {
var name = node.attributes.name;
var type = node.attributes.type;
var value = node.attributes.value;
var messages = node.messages
if (node.group == 'default' || node.group == group) {
var elm = getFormElement(type, name, value, messages);
form += elm;
}
if (node.group == 'default' || node.group == group) {
var elm = getFormElement(type, name, value, messages);
form += elm;
}
form += "</form>";
return form;
}
form += '</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.
// Kratos give us form names and types and specifies what to render. However
// it does not provide labels or translations. This function returns a HTML
// form element based on the fields provided by Kratos with proper names and
// form element based on the fields provided by Kratos with proper names and
// labels
// type: input type, usual "input", "hidden" or "submit". But bootstrap types
// like "email" are also supported
@ -274,98 +261,80 @@ function render_form(data, group) {
// value: If there is already a value known, show it
// messages: error messages related to the field
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) {
value = '';
}
if (value == undefined) {
value = '';
}
if (typeof(messages) == "undefined") {
messages = []
}
if (typeof messages == 'undefined') {
messages = [];
}
if (name == 'email' || name == 'traits.email') {
return getFormInput(
'email',
name,
value,
'E-mail address',
'Please enter your e-mail address here',
'Please provide your e-mail address. We will send a recovery ' +
'link to that e-mail address.',
messages,
);
}
if (name == 'email' || name == 'traits.email') {
return getFormInput(
'email',
name,
value,
'E-mail address',
'Please enter your e-mail address here',
'Please provide your e-mail address. We will send a recovery ' + 'link to that e-mail address.',
messages,
);
}
if (name == 'traits.username') {
return getFormInput(
'name',
name,
value,
'Username',
'Please provide an username',
null,
messages,
);
}
if (name == 'traits.username') {
return getFormInput('name', name, value, 'Username', 'Please provide an username', null, messages);
}
if (name == 'traits.name') {
return getFormInput(
'name',
name,
value,
'Full name',
'Please provide your full name',
null,
messages,
);
}
if (name == 'traits.name') {
return getFormInput('name', name, value, 'Full name', 'Please provide your full name', null, messages);
}
if (name == 'identifier') {
return getFormInput(
'email',
name,
value,
'E-mail address',
'Please provide your e-mail address to log in',
null,
messages,
);
}
if (name == 'identifier') {
return getFormInput(
'email',
name,
value,
'E-mail address',
'Please provide your e-mail address to log in',
null,
messages,
);
}
if (name == 'password') {
return getFormInput('password', name, value, 'Password', 'Please provide your password', null, messages);
}
if (name == 'password') {
return getFormInput(
'password',
name,
value,
'Password',
'Please provide your password',
null,
messages,
);
}
if (type == 'hidden' || name == 'traits.uuid') {
return (
`
<input type="hidden" class="form-control" id="` +
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') {
return `<div class="form-group">
<input type="hidden" name="`+name+`" value="`+value+`">
if (type == 'submit') {
return (
`<div class="form-group">
<input type="hidden" name="` +
name +
`" value="` +
value +
`">
<button type="submit" class="btn btn-primary">Go!</button>
</div>`;
}
return getFormInput('input', name, value, name, null,null, messages);
</div>`
);
}
return getFormInput('input', name, value, name, null, null, messages);
}
// Usually called by getFormElement, generic function to generate an
@ -378,56 +347,58 @@ function getFormElement(type, name, value, messages) {
// param help: Additional help text, displayed below the field in small font
// param messages: Message about failed input
function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof(help) == "undefined" || help == null) {
help = ""
}
console.log("Messages: ", messages);
if (typeof help == 'undefined' || help == null) {
help = '';
}
console.log('Messages: ', messages);
// Id field for help element
var nameHelp = name + "Help";
// Id field for help element
var nameHelp = name + 'Help';
var element = '<div class="form-group">';
element += '<label for="'+name+'">'+label+'</label>';
element += '<input type="'+type+'" class="form-control" id="'+name+'" name="'+name+'" ';
var element = '<div class="form-group">';
element += '<label for="' + name + '">' + label + '</label>';
element += '<input type="' + type + '" class="form-control" id="' + name + '" name="' + name + '" ';
// messages get appended to help info
if (messages.length) {
for (message in messages) {
console.log("adding message", messages[message])
help += messages[message]['text']
}
// messages get appended to help info
if (messages.length) {
for (message in messages) {
console.log('adding message', messages[message]);
help += messages[message]['text'];
}
}
// If we are a password field, add a eye icon to reveal password
if (value) {
element += 'value="'+value+'" ';
}
if (help) {
element += 'aria-describedby="' + nameHelp +'" ';
}
if (placeHolder) {
element += 'placeholder="'+placeHolder+'" ';
}
element += ">";
// If we are a password field, add a eye icon to reveal password
if (value) {
element += 'value="' + value + '" ';
}
if (help) {
element += 'aria-describedby="' + nameHelp + '" ';
}
if (placeHolder) {
element += 'placeholder="' + placeHolder + '" ';
}
element += '>';
if (help) {
element +=
`<small id="`+nameHelp+`" class="form-text text-muted">` + help + `
if (help) {
element +=
`<small id="` +
nameHelp +
`" class="form-text text-muted">` +
help +
`
</small>`;
}
}
element += '</div>';
element += '</div>';
return element;
return element;
}
// $.urlParam get parameters from the URI. Example: id = $.urlParam('id');
$.urlParam = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) {
return null;
}
return decodeURI(results[1]) || 0;
$.urlParam = function (name) {
var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results == null) {
return null;
}
return decodeURI(results[1]) || 0;
};

View File

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

View File

@ -19,7 +19,7 @@
<div id="contentMessages"></div>
<div id="contentLogin"></div>
<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>
{% endblock %}