Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Replace username picker with a template #9275

Merged
merged 7 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 4 additions & 35 deletions synapse/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
import argparse
import errno
import os
import time
import urllib.parse
from collections import OrderedDict
from hashlib import sha256
from textwrap import dedent
from typing import Any, Callable, Iterable, List, MutableMapping, Optional
from typing import Any, Iterable, List, MutableMapping, Optional

import attr
import jinja2
import pkg_resources
import yaml

from synapse.util.templates import _create_mxc_to_http_filter, _format_ts_filter


class ConfigError(Exception):
"""Represents a problem parsing the configuration
Expand Down Expand Up @@ -248,6 +248,7 @@ def read_templates(
# Search the custom template directory as well
search_directories.insert(0, custom_template_directory)

# TODO: switch to synapse.util.templates.build_jinja_env
loader = jinja2.FileSystemLoader(search_directories)
env = jinja2.Environment(loader=loader, autoescape=autoescape)

Expand All @@ -267,38 +268,6 @@ def read_templates(
return templates


def _format_ts_filter(value: int, format: str):
return time.strftime(format, time.localtime(value / 1000))


def _create_mxc_to_http_filter(public_baseurl: str) -> Callable:
"""Create and return a jinja2 filter that converts MXC urls to HTTP

Args:
public_baseurl: The public, accessible base URL of the homeserver
"""

def mxc_to_http_filter(value, width, height, resize_method="crop"):
if value[0:6] != "mxc://":
return ""

server_and_media_id = value[6:]
fragment = None
if "#" in server_and_media_id:
server_and_media_id, fragment = server_and_media_id.split("#", 1)
fragment = "#" + fragment

params = {"width": width, "height": height, "method": resize_method}
return "%s_matrix/media/v1/thumbnail/%s?%s%s" % (
public_baseurl,
server_and_media_id,
urllib.parse.urlencode(params),
fragment or "",
)

return mxc_to_http_filter


class RootConfig:
"""
Holder of an application's configuration.
Expand Down
4 changes: 2 additions & 2 deletions synapse/config/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def read_config(self, config, **kwargs):
sso_config = config.get("sso") or {} # type: Dict[str, Any]

# The sso-specific template_dir
template_dir = sso_config.get("template_dir")
self.sso_template_dir = sso_config.get("template_dir")

# Read templates from disk
(
Expand All @@ -48,7 +48,7 @@ def read_config(self, config, **kwargs):
"sso_auth_success.html",
"sso_auth_bad_user.html",
],
template_dir,
self.sso_template_dir,
)

# These templates have no placeholders, so render them here
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ async def _redirect_to_username_picker(
logger.info("Recorded registration session id %s", session_id)

# Set the cookie and redirect to the username picker
e = RedirectException(b"/_synapse/client/pick_username")
e = RedirectException(b"/_synapse/client/pick_username/account_details")
e.cookies.append(
b"%s=%s; path=/"
% (USERNAME_MAPPING_SESSION_COOKIE_NAME, session_id.encode("ascii"))
Expand Down
115 changes: 115 additions & 0 deletions synapse/res/templates/sso_auth_account_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!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 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 .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;
}
</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">
<label for="field-username">Username</label>
<div class="prefix">@</div>
<input type="text" name="username" id="field-username" autofocus required pattern="[a-z0-9\-=_\/\.]+">
<div class="postfix">:{{ server_name }}</div>
</div>
<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>
clokep marked this conversation as resolved.
Show resolved Hide resolved
<!-- {% if user_attributes.avatar_url %} -->
<div class="idp-detail idp-avatar">
<img src="{{user_attributes.avatar_url}}" class="avatar" />
</div>
<!-- {% endif %} -->
<!-- {% if user_attributes.display_name %} -->
<div class="idp-detail">
<p class="idp-value">{{ user_attributes.display_name }}</p>
</div>
<!-- {% endif %} -->
<!-- {% for email in user_attributes.emails %} -->
<div class="idp-detail">
<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>
76 changes: 76 additions & 0 deletions synapse/res/templates/sso_auth_account_details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const usernameField = document.getElementById("field-username");

function throttle(fn, wait) {
let timeout;
return function() {
const args = Array.from(arguments);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(fn.bind.apply(fn, [null].concat(args)), wait);
}
}

function checkUsernameAvailable(username) {
let check_uri = 'check?username=' + encodeURIComponent(username);
return fetch(check_uri, {
// include the cookie
"credentials": "same-origin",
}).then((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((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."};
}
});
}

function validateUsername(username) {
usernameField.setCustomValidity("");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL this is a thing. 👍

if (usernameField.validity.valueMissing) {
usernameField.setCustomValidity("Please provide a username");
return;
}
if (usernameField.validity.patternMismatch) {
usernameField.setCustomValidity("Invalid username, please only use " + allowedCharactersString);
return;
}
usernameField.setCustomValidity("Checking if username is available …");
throttledCheckUsernameAvailable(username);
}

const throttledCheckUsernameAvailable = throttle(function(username) {
const handleError = function(err) {
// don't prevent form submission on error
usernameField.setCustomValidity("");
console.log(err.message);
};
try {
checkUsernameAvailable(username).then(function(result) {
if (!result.available) {
usernameField.setCustomValidity(result.message);
usernameField.reportValidity();
} else {
usernameField.setCustomValidity("");
}
}, handleError);
} catch (err) {
handleError(err);
}
}, 500);

usernameField.addEventListener("input", function(evt) {
validateUsername(usernameField.value);
});
usernameField.addEventListener("change", function(evt) {
validateUsername(usernameField.value);
});
19 changes: 0 additions & 19 deletions synapse/res/username_picker/index.html

This file was deleted.

Loading