From 0ed0c66e1df18fb19b2329dde2117413467667ff Mon Sep 17 00:00:00 2001 From: Maarten de Waard Date: Fri, 14 Oct 2022 14:46:37 +0200 Subject: [PATCH] Show Kratos general failure messages (for example for r wrong password), fix welcome message --- CHANGELOG.md | 7 + backend/web/login/login.py | 8 +- backend/web/static/base.js | 599 +++++++++++++--------------- backend/web/templates/loggedin.html | 12 +- backend/web/templates/login.html | 2 +- 5 files changed, 305 insertions(+), 323 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f59063..9df2fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/backend/web/login/login.py b/backend/web/login/login.py index 8ea982c..3601a00 100644 --- a/backend/web/login/login.py +++ b/backend/web/login/login.py @@ -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") diff --git a/backend/web/static/base.js b/backend/web/static/base.js index 4424760..d431ea6 100644 --- a/backend/web/static/base.js +++ b/backend/web/static/base.js @@ -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 = "
"; - // Create form - var action = data.ui.action; - var method = data.ui.method; - var form = ""; + 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 += "
"; - return 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 = ''; + 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 ( + ` + ` + ); + } - - if (type == 'hidden' || name == 'traits.uuid') { - - return ` - `; - } - - if (type == 'submit') { - - return `
- + if (type == 'submit') { + return ( + `
+ -
`; - } - - - 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 @@ -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 = '
'; - element += ''; - element += '' + label + ''; + element += '` + help + ` + if (help) { + element += + `` + + help + + ` `; - } + } - element += '
'; + element += ''; - 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; }; diff --git a/backend/web/templates/loggedin.html b/backend/web/templates/loggedin.html index 530e0da..5d5a437 100644 --- a/backend/web/templates/loggedin.html +++ b/backend/web/templates/loggedin.html @@ -1,5 +1,3 @@ - - {% extends 'base.html' %} {% block content %} @@ -17,11 +15,11 @@ -
-
Welcome {{ id['name'] }},

- You are already logged in. - - +
+
+ Welcome{{ name }}, +

+ You are logged in.
diff --git a/backend/web/templates/login.html b/backend/web/templates/login.html index b715367..844eef7 100644 --- a/backend/web/templates/login.html +++ b/backend/web/templates/login.html @@ -19,7 +19,7 @@
{% endblock %}