diff options
author | Ben Banfield-Zanin <benbz@matrix.org> | 2021-02-16 13:33:20 +0000 |
---|---|---|
committer | Ben Banfield-Zanin <benbz@matrix.org> | 2021-02-16 13:33:20 +0000 |
commit | dcf1b9c276e22bb6f5200fc029301c4d40e87a1f (patch) | |
tree | 1f5badce24645d99534133a7a989069906088fff /synapse/res/templates | |
parent | Merge remote-tracking branch 'origin/release-v1.24.0' into bbz/info-mainline-... (diff) | |
parent | Fixup CHANGES (diff) | |
download | synapse-dcf1b9c276e22bb6f5200fc029301c4d40e87a1f.tar.xz |
Merge remote-tracking branch 'origin/release-v1.27.0' into bbz/info-mainline-1.27.0 github/bbz/info-mainline-1.27.0 bbz/info-mainline-1.27.0
Diffstat (limited to 'synapse/res/templates')
-rw-r--r-- | synapse/res/templates/notif.html | 2 | ||||
-rw-r--r-- | synapse/res/templates/sso.css | 88 | ||||
-rw-r--r-- | synapse/res/templates/sso_account_deactivated.html | 26 | ||||
-rw-r--r-- | synapse/res/templates/sso_auth_account_details.html | 161 | ||||
-rw-r--r-- | synapse/res/templates/sso_auth_account_details.js | 116 | ||||
-rw-r--r-- | synapse/res/templates/sso_auth_bad_user.html | 25 | ||||
-rw-r--r-- | synapse/res/templates/sso_auth_confirm.html | 32 | ||||
-rw-r--r-- | synapse/res/templates/sso_auth_success.html | 39 | ||||
-rw-r--r-- | synapse/res/templates/sso_error.html | 101 | ||||
-rw-r--r-- | synapse/res/templates/sso_login_idp_picker.html | 31 | ||||
-rw-r--r-- | synapse/res/templates/sso_new_user_consent.html | 39 | ||||
-rw-r--r-- | synapse/res/templates/sso_redirect_confirm.html | 34 |
12 files changed, 614 insertions, 80 deletions
diff --git a/synapse/res/templates/notif.html b/synapse/res/templates/notif.html index 6d76064d13..0aaef97df8 100644 --- a/synapse/res/templates/notif.html +++ b/synapse/res/templates/notif.html @@ -29,7 +29,7 @@ {{ message.body_text_html }} {%- elif message.msgtype == "m.notice" %} {{ message.body_text_html }} - {%- elif message.msgtype == "m.image" %} + {%- elif message.msgtype == "m.image" and message.image_url %} <img src="{{ message.image_url|mxc_to_http(640, 480, scale) }}" /> {%- elif message.msgtype == "m.file" %} <span class="filename">{{ message.body_text_plain }}</span> diff --git a/synapse/res/templates/sso.css b/synapse/res/templates/sso.css new file mode 100644 index 0000000000..46b309ea4e --- /dev/null +++ b/synapse/res/templates/sso.css @@ -0,0 +1,88 @@ +body { + font-family: "Inter", "Helvetica", "Arial", sans-serif; + font-size: 14px; + color: #17191C; +} + +header { + max-width: 480px; + width: 100%; + margin: 24px auto; + text-align: center; +} + +header p { + color: #737D8C; + line-height: 24px; +} + +h1 { + font-size: 24px; +} + +.error_page h1 { + color: #FE2928; +} + +h2 { + font-size: 14px; +} + +h2 img { + vertical-align: middle; + margin-right: 8px; + width: 24px; + height: 24px; +} + +label { + cursor: pointer; +} + +main { + max-width: 360px; + width: 100%; + margin: 24px auto; +} + +.primary-button { + border: none; + text-decoration: none; + padding: 12px; + color: white; + background-color: #418DED; + font-weight: bold; + display: block; + border-radius: 12px; + width: 100%; + box-sizing: border-box; + margin: 16px 0; + cursor: pointer; + text-align: center; +} + +.profile { + display: flex; + justify-content: center; + margin: 24px 0; +} + +.profile .avatar { + width: 36px; + height: 36px; + border-radius: 100%; + display: block; + margin-right: 8px; +} + +.profile .display-name { + font-weight: bold; + margin-bottom: 4px; +} +.profile .user-id { + color: #737D8C; +} + +.profile .display-name, .profile .user-id { + line-height: 18px; +} diff --git a/synapse/res/templates/sso_account_deactivated.html b/synapse/res/templates/sso_account_deactivated.html index 4eb8db9fb4..50a0979c2f 100644 --- a/synapse/res/templates/sso_account_deactivated.html +++ b/synapse/res/templates/sso_account_deactivated.html @@ -1,10 +1,24 @@ <!DOCTYPE html> <html lang="en"> -<head> - <meta charset="UTF-8"> - <title>SSO account deactivated</title> -</head> - <body> - <p>This account has been deactivated.</p> + <head> + <meta charset="UTF-8"> + <title>SSO account deactivated</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + </style> + </head> + <body class="error_page"> + <header> + <h1>Your account has been deactivated</h1> + <p> + <strong>No account found</strong> + </p> + <p> + Your account might have been deactivated by the server administrator. + You can either try to create a new account or contact the server’s + administrator. + </p> + </header> </body> </html> diff --git a/synapse/res/templates/sso_auth_account_details.html b/synapse/res/templates/sso_auth_account_details.html new file mode 100644 index 0000000000..36850a2d6a --- /dev/null +++ b/synapse/res/templates/sso_auth_account_details.html @@ -0,0 +1,161 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Synapse Login</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + + .username_input { + display: flex; + border: 2px solid #418DED; + border-radius: 8px; + padding: 12px; + position: relative; + margin: 16px 0; + align-items: center; + font-size: 12px; + } + + .username_input.invalid { + border-color: #FE2928; + } + + .username_input.invalid input, .username_input.invalid label { + color: #FE2928; + } + + .username_input div, .username_input input { + line-height: 18px; + font-size: 14px; + } + + .username_input label { + position: absolute; + top: -8px; + left: 14px; + font-size: 80%; + background: white; + padding: 2px; + } + + .username_input input { + flex: 1; + display: block; + min-width: 0; + border: none; + } + + .username_input div { + color: #8D99A5; + } + + .idp-pick-details { + border: 1px solid #E9ECF1; + border-radius: 8px; + margin: 24px 0; + } + + .idp-pick-details h2 { + margin: 0; + padding: 8px 12px; + } + + .idp-pick-details .idp-detail { + border-top: 1px solid #E9ECF1; + padding: 12px; + } + .idp-pick-details .check-row { + display: flex; + align-items: center; + } + + .idp-pick-details .check-row .name { + flex: 1; + } + + .idp-pick-details .use, .idp-pick-details .idp-value { + color: #737D8C; + } + + .idp-pick-details .idp-value { + margin: 0; + margin-top: 8px; + } + + .idp-pick-details .avatar { + width: 53px; + height: 53px; + border-radius: 100%; + display: block; + margin-top: 8px; + } + + output { + padding: 0 14px; + display: block; + } + + output.error { + color: #FE2928; + } + </style> + </head> + <body> + <header> + <h1>Your account is nearly ready</h1> + <p>Check your details before creating an account on {{ server_name }}</p> + </header> + <main> + <form method="post" class="form__input" id="form"> + <div class="username_input" id="username_input"> + <label for="field-username">Username</label> + <div class="prefix">@</div> + <input type="text" name="username" id="field-username" autofocus> + <div class="postfix">:{{ server_name }}</div> + </div> + <output for="username_input" id="field-username-output"></output> + <input type="submit" value="Continue" class="primary-button"> + {% if user_attributes %} + <section class="idp-pick-details"> + <h2><img src="{{ idp.idp_icon | mxc_to_http(24, 24) }}"/>Information from {{ idp.idp_name }}</h2> + {% if user_attributes.avatar_url %} + <div class="idp-detail idp-avatar"> + <div class="check-row"> + <label for="idp-avatar" class="name">Avatar</label> + <label for="idp-avatar" class="use">Use</label> + <input type="checkbox" name="use_avatar" id="idp-avatar" value="true" checked> + </div> + <img src="{{ user_attributes.avatar_url }}" class="avatar" /> + </div> + {% endif %} + {% if user_attributes.display_name %} + <div class="idp-detail"> + <div class="check-row"> + <label for="idp-displayname" class="name">Display name</label> + <label for="idp-displayname" class="use">Use</label> + <input type="checkbox" name="use_display_name" id="idp-displayname" value="true" checked> + </div> + <p class="idp-value">{{ user_attributes.display_name }}</p> + </div> + {% endif %} + {% for email in user_attributes.emails %} + <div class="idp-detail"> + <div class="check-row"> + <label for="idp-email{{ loop.index }}" class="name">E-mail</label> + <label for="idp-email{{ loop.index }}" class="use">Use</label> + <input type="checkbox" name="use_email" id="idp-email{{ loop.index }}" value="{{ email }}" checked> + </div> + <p class="idp-value">{{ email }}</p> + </div> + {% endfor %} + </section> + {% endif %} + </form> + </main> + <script type="text/javascript"> + {% include "sso_auth_account_details.js" without context %} + </script> + </body> +</html> diff --git a/synapse/res/templates/sso_auth_account_details.js b/synapse/res/templates/sso_auth_account_details.js new file mode 100644 index 0000000000..3c45df9078 --- /dev/null +++ b/synapse/res/templates/sso_auth_account_details.js @@ -0,0 +1,116 @@ +const usernameField = document.getElementById("field-username"); +const usernameOutput = document.getElementById("field-username-output"); +const form = document.getElementById("form"); + +// needed to validate on change event when no input was changed +let needsValidation = true; +let isValid = false; + +function throttle(fn, wait) { + let timeout; + const throttleFn = function() { + const args = Array.from(arguments); + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(fn.bind.apply(fn, [null].concat(args)), wait); + }; + throttleFn.cancelQueued = function() { + clearTimeout(timeout); + }; + return throttleFn; +} + +function checkUsernameAvailable(username) { + let check_uri = 'check?username=' + encodeURIComponent(username); + return fetch(check_uri, { + // include the cookie + "credentials": "same-origin", + }).then(function(response) { + if(!response.ok) { + // for non-200 responses, raise the body of the response as an exception + return response.text().then((text) => { throw new Error(text); }); + } else { + return response.json(); + } + }).then(function(json) { + if(json.error) { + return {message: json.error}; + } else if(json.available) { + return {available: true}; + } else { + return {message: username + " is not available, please choose another."}; + } + }); +} + +const allowedUsernameCharacters = new RegExp("^[a-z0-9\\.\\_\\-\\/\\=]+$"); +const allowedCharactersString = "lowercase letters, digits, ., _, -, /, ="; + +function reportError(error) { + throttledCheckUsernameAvailable.cancelQueued(); + usernameOutput.innerText = error; + usernameOutput.classList.add("error"); + usernameField.parentElement.classList.add("invalid"); + usernameField.focus(); +} + +function validateUsername(username) { + isValid = false; + needsValidation = false; + usernameOutput.innerText = ""; + usernameField.parentElement.classList.remove("invalid"); + usernameOutput.classList.remove("error"); + if (!username) { + return reportError("Please provide a username"); + } + if (username.length > 255) { + return reportError("Too long, please choose something shorter"); + } + if (!allowedUsernameCharacters.test(username)) { + return reportError("Invalid username, please only use " + allowedCharactersString); + } + usernameOutput.innerText = "Checking if username is available …"; + throttledCheckUsernameAvailable(username); +} + +const throttledCheckUsernameAvailable = throttle(function(username) { + const handleError = function(err) { + // don't prevent form submission on error + usernameOutput.innerText = ""; + isValid = true; + }; + try { + checkUsernameAvailable(username).then(function(result) { + if (!result.available) { + reportError(result.message); + } else { + isValid = true; + usernameOutput.innerText = ""; + } + }, handleError); + } catch (err) { + handleError(err); + } +}, 500); + +form.addEventListener("submit", function(evt) { + if (needsValidation) { + validateUsername(usernameField.value); + evt.preventDefault(); + return; + } + if (!isValid) { + evt.preventDefault(); + usernameField.focus(); + return; + } +}); +usernameField.addEventListener("input", function(evt) { + validateUsername(usernameField.value); +}); +usernameField.addEventListener("change", function(evt) { + if (needsValidation) { + validateUsername(usernameField.value); + } +}); diff --git a/synapse/res/templates/sso_auth_bad_user.html b/synapse/res/templates/sso_auth_bad_user.html new file mode 100644 index 0000000000..c9bd4bef20 --- /dev/null +++ b/synapse/res/templates/sso_auth_bad_user.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Authentication failed</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + </style> + </head> + <body class="error_page"> + <header> + <h1>That doesn't look right</h1> + <p> + <strong>We were unable to validate your {{ server_name }} account</strong> + via single sign‑on (SSO), because the SSO Identity + Provider returned different details than when you logged in. + </p> + <p> + Try the operation again, and ensure that you use the same details on + the Identity Provider as when you log into your account. + </p> + </header> + </body> +</html> diff --git a/synapse/res/templates/sso_auth_confirm.html b/synapse/res/templates/sso_auth_confirm.html index 0d9de9d465..2099c2f1f8 100644 --- a/synapse/res/templates/sso_auth_confirm.html +++ b/synapse/res/templates/sso_auth_confirm.html @@ -1,14 +1,28 @@ -<html> -<head> - <title>Authentication</title> -</head> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Authentication</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + </style> + </head> <body> - <div> + <header> + <h1>Confirm it's you to continue</h1> <p> - A client is trying to {{ description | e }}. To confirm this action, - <a href="{{ redirect_url | e }}">re-authenticate with single sign-on</a>. - If you did not expect this, your account may be compromised! + A client is trying to {{ description }}. To confirm this action + re-authorize your account with single sign-on. </p> - </div> + <p><strong> + If you did not expect this, your account may be compromised. + </strong></p> + </header> + <main> + <a href="{{ redirect_url }}" class="primary-button"> + Continue with {{ idp.idp_name }} + </a> + </main> </body> </html> diff --git a/synapse/res/templates/sso_auth_success.html b/synapse/res/templates/sso_auth_success.html index 03f1419467..3b975d7219 100644 --- a/synapse/res/templates/sso_auth_success.html +++ b/synapse/res/templates/sso_auth_success.html @@ -1,18 +1,27 @@ -<html> -<head> - <title>Authentication Successful</title> - <script> - if (window.onAuthDone) { - window.onAuthDone(); - } else if (window.opener && window.opener.postMessage) { - window.opener.postMessage("authDone", "*"); - } - </script> -</head> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Authentication successful</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + </style> + <script> + if (window.onAuthDone) { + window.onAuthDone(); + } else if (window.opener && window.opener.postMessage) { + window.opener.postMessage("authDone", "*"); + } + </script> + </head> <body> - <div> - <p>Thank you</p> - <p>You may now close this window and return to the application</p> - </div> + <header> + <h1>Thank you</h1> + <p> + Now we know it’s you, you can close this window and return to the + application. + </p> + </header> </body> </html> diff --git a/synapse/res/templates/sso_error.html b/synapse/res/templates/sso_error.html index 944bc9c9ca..b223ca0f56 100644 --- a/synapse/res/templates/sso_error.html +++ b/synapse/res/templates/sso_error.html @@ -1,53 +1,68 @@ <!DOCTYPE html> <html lang="en"> -<head> - <meta charset="UTF-8"> - <title>SSO error</title> -</head> -<body> + <head> + <meta charset="UTF-8"> + <title>Authentication failed</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + + #error_code { + margin-top: 56px; + } + </style> + </head> + <body class="error_page"> {# If an error of unauthorised is returned it means we have actively rejected their login #} {% if error == "unauthorised" %} - <p>You are not allowed to log in here.</p> + <header> + <p>You are not allowed to log in here.</p> + </header> {% else %} - <p> - There was an error during authentication: - </p> - <div id="errormsg" style="margin:20px 80px">{{ error_description | e }}</div> - <p> - If you are seeing this page after clicking a link sent to you via email, make - sure you only click the confirmation link once, and that you open the - validation link in the same client you're logging in from. - </p> - <p> - Try logging in again from your Matrix client and if the problem persists - please contact the server's administrator. - </p> - <p>Error: <code>{{ error }}</code></p> + <header> + <h1>There was an error</h1> + <p> + <strong id="errormsg">{{ error_description }}</strong> + </p> + <p> + If you are seeing this page after clicking a link sent to you via email, + make sure you only click the confirmation link once, and that you open + the validation link in the same client you're logging in from. + </p> + <p> + Try logging in again from your Matrix client and if the problem persists + please contact the server's administrator. + </p> + <div id="error_code"> + <p><strong>Error code</strong></p> + <p>{{ error }}</p> + </div> + </header> - <script type="text/javascript"> - // Error handling to support Auth0 errors that we might get through a GET request - // to the validation endpoint. If an error is provided, it's either going to be - // located in the query string or in a query string-like URI fragment. - // We try to locate the error from any of these two locations, but if we can't - // we just don't print anything specific. - let searchStr = ""; - if (window.location.search) { - // window.location.searchParams isn't always defined when - // window.location.search is, so it's more reliable to parse the latter. - searchStr = window.location.search; - } else if (window.location.hash) { - // Replace the # with a ? so that URLSearchParams does the right thing and - // doesn't parse the first parameter incorrectly. - searchStr = window.location.hash.replace("#", "?"); - } + <script type="text/javascript"> + // Error handling to support Auth0 errors that we might get through a GET request + // to the validation endpoint. If an error is provided, it's either going to be + // located in the query string or in a query string-like URI fragment. + // We try to locate the error from any of these two locations, but if we can't + // we just don't print anything specific. + let searchStr = ""; + if (window.location.search) { + // window.location.searchParams isn't always defined when + // window.location.search is, so it's more reliable to parse the latter. + searchStr = window.location.search; + } else if (window.location.hash) { + // Replace the # with a ? so that URLSearchParams does the right thing and + // doesn't parse the first parameter incorrectly. + searchStr = window.location.hash.replace("#", "?"); + } - // We might end up with no error in the URL, so we need to check if we have one - // to print one. - let errorDesc = new URLSearchParams(searchStr).get("error_description") - if (errorDesc) { - document.getElementById("errormsg").innerText = errorDesc; - } - </script> + // We might end up with no error in the URL, so we need to check if we have one + // to print one. + let errorDesc = new URLSearchParams(searchStr).get("error_description") + if (errorDesc) { + document.getElementById("errormsg").innerText = errorDesc; + } + </script> {% endif %} </body> </html> diff --git a/synapse/res/templates/sso_login_idp_picker.html b/synapse/res/templates/sso_login_idp_picker.html new file mode 100644 index 0000000000..62a640dad2 --- /dev/null +++ b/synapse/res/templates/sso_login_idp_picker.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="/_matrix/static/client/login/style.css"> + <title>{{ server_name }} Login</title> + </head> + <body> + <div id="container"> + <h1 id="title">{{ server_name }} Login</h1> + <div class="login_flow"> + <p>Choose one of the following identity providers:</p> + <form> + <input type="hidden" name="redirectUrl" value="{{ redirect_url }}"> + <ul class="radiobuttons"> +{% for p in providers %} + <li> + <input type="radio" name="idp" id="prov{{ loop.index }}" value="{{ p.idp_id }}"> + <label for="prov{{ loop.index }}">{{ p.idp_name }}</label> +{% if p.idp_icon %} + <img src="{{ p.idp_icon | mxc_to_http(32, 32) }}"/> +{% endif %} + </li> +{% endfor %} + </ul> + <input type="submit" class="button button--full-width" id="button-submit" value="Submit"> + </form> + </div> + </div> + </body> +</html> diff --git a/synapse/res/templates/sso_new_user_consent.html b/synapse/res/templates/sso_new_user_consent.html new file mode 100644 index 0000000000..8c33787c54 --- /dev/null +++ b/synapse/res/templates/sso_new_user_consent.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>SSO redirect confirmation</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + + #consent_form { + margin-top: 56px; + } + </style> +</head> + <body> + <header> + <h1>Your account is nearly ready</h1> + <p>Agree to the terms to create your account.</p> + </header> + <main> + <!-- {% if user_profile.avatar_url and user_profile.display_name %} --> + <div class="profile"> + <img src="{{ user_profile.avatar_url | mxc_to_http(64, 64) }}" class="avatar" /> + <div class="profile-details"> + <div class="display-name">{{ user_profile.display_name }}</div> + <div class="user-id">{{ user_id }}</div> + </div> + </div> + <!-- {% endif %} --> + <form method="post" action="{{my_url}}" id="consent_form"> + <p> + <input id="accepted_version" type="checkbox" name="accepted_version" value="{{ consent_version }}" required> + <label for="accepted_version">I have read and agree to the <a href="{{ terms_url }}" target="_blank">terms and conditions</a>.</label> + </p> + <input type="submit" class="primary-button" value="Continue"/> + </form> + </main> + </body> +</html> diff --git a/synapse/res/templates/sso_redirect_confirm.html b/synapse/res/templates/sso_redirect_confirm.html index 20a15e1e74..d1328a6969 100644 --- a/synapse/res/templates/sso_redirect_confirm.html +++ b/synapse/res/templates/sso_redirect_confirm.html @@ -3,12 +3,34 @@ <head> <meta charset="UTF-8"> <title>SSO redirect confirmation</title> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <style type="text/css"> + {% include "sso.css" without context %} + </style> </head> <body> - <p>The application at <span style="font-weight:bold">{{ display_url | e }}</span> is requesting full access to your <span style="font-weight:bold">{{ server_name }}</span> Matrix account.</p> - <p>If you don't recognise this address, you should ignore this and close this tab.</p> - <p> - <a href="{{ redirect_url | e }}">I trust this address</a> - </p> + <header> + {% if new_user %} + <h1>Your account is now ready</h1> + <p>You've made your account on {{ server_name }}.</p> + {% else %} + <h1>Log in</h1> + {% endif %} + <p>Continue to confirm you trust <strong>{{ display_url }}</strong>.</p> + </header> + <main> + {% if user_profile.avatar_url %} + <div class="profile"> + <img src="{{ user_profile.avatar_url | mxc_to_http(64, 64) }}" class="avatar" /> + <div class="profile-details"> + {% if user_profile.display_name %} + <div class="display-name">{{ user_profile.display_name }}</div> + {% endif %} + <div class="user-id">{{ user_id }}</div> + </div> + </div> + {% endif %} + <a href="{{ redirect_url }}" class="primary-button">Continue</a> + </main> </body> -</html> \ No newline at end of file +</html> |