mirror of
https://github.com/matrix-org/dendrite.git
synced 2024-11-26 00:01:55 -06:00
Add login fallback (#3302)
Part of https://github.com/matrix-org/dendrite/issues/3216 The files are basically copied from Synapse, with minor changes to the called endpoints. We never seem to have had the `/_matrix/static/client/login/` endpoint, this adds it.
This commit is contained in:
parent
dae1ef2e46
commit
bebf701dce
|
@ -50,6 +50,9 @@ import (
|
|||
//go:embed static/*.gotmpl
|
||||
var staticContent embed.FS
|
||||
|
||||
//go:embed static/client/login
|
||||
var loginFallback embed.FS
|
||||
|
||||
const HTTPServerTimeout = time.Minute * 5
|
||||
|
||||
// CreateClient creates a new client (normally used for media fetch requests).
|
||||
|
@ -158,6 +161,14 @@ func SetupAndServeHTTP(
|
|||
_, _ = w.Write(landingPage.Bytes())
|
||||
})
|
||||
|
||||
// We only need the files beneath the static/client/login folder.
|
||||
sub, err := fs.Sub(loginFallback, "static/client/login")
|
||||
if err != nil {
|
||||
logrus.Panicf("unable to read embedded files, this should never happen: %s", err)
|
||||
}
|
||||
// Serve a static page for login fallback
|
||||
routers.Static.PathPrefix("/client/login/").Handler(http.StripPrefix("/_matrix/static/client/login/", http.FileServer(http.FS(sub))))
|
||||
|
||||
var clientHandler http.Handler
|
||||
clientHandler = routers.Client
|
||||
if cfg.Global.Sentry.Enabled {
|
||||
|
|
47
setup/base/static/client/login/index.html
Normal file
47
setup/base/static/client/login/index.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title> Login </title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="js/login.js"></script>
|
||||
</head>
|
||||
<body onload="matrixLogin.onLoad()">
|
||||
<div id="container">
|
||||
<h1 id="title"></h1>
|
||||
|
||||
<span id="feedback"></span>
|
||||
|
||||
<div id="loading">
|
||||
<img src="spinner.gif" />
|
||||
</div>
|
||||
|
||||
<div id="sso_flow" class="login_flow" style="display: none;">
|
||||
Single-sign on:
|
||||
<form id="sso_form" action="/_matrix/client/v3/login/sso/redirect" method="get">
|
||||
<input id="sso_redirect_url" type="hidden" name="redirectUrl" value=""/>
|
||||
<input type="submit" value="Log in"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="password_flow" class="login_flow" style="display: none;">
|
||||
Password Authentication:
|
||||
<form onsubmit="matrixLogin.passwordLogin(); return false;">
|
||||
<input id="user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" />
|
||||
<br/>
|
||||
<input id="password" size="32" type="password" placeholder="Password"/>
|
||||
<br/>
|
||||
|
||||
<input type="submit" value="Log in"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="no_login_types" type="button" class="login_flow" style="display: none;">
|
||||
Log in currently unavailable.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
2
setup/base/static/client/login/js/jquery-3.4.1.min.js
vendored
Normal file
2
setup/base/static/client/login/js/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
291
setup/base/static/client/login/js/login.js
Normal file
291
setup/base/static/client/login/js/login.js
Normal file
|
@ -0,0 +1,291 @@
|
|||
window.matrixLogin = {
|
||||
endpoint: location.origin + "/_matrix/client/v3/login",
|
||||
serverAcceptsPassword: false,
|
||||
serverAcceptsSso: false,
|
||||
};
|
||||
|
||||
// Titles get updated through the process to give users feedback.
|
||||
const TITLE_PRE_AUTH = "Log in with one of the following methods";
|
||||
const TITLE_POST_AUTH = "Logging in...";
|
||||
|
||||
// The cookie used to store the original query parameters when using SSO.
|
||||
const COOKIE_KEY = "dendrite_login_fallback_qs";
|
||||
|
||||
/*
|
||||
* Submit a login request.
|
||||
*
|
||||
* type: The login type as a string (e.g. "m.login.foo").
|
||||
* data: An object of data specific to the login type.
|
||||
* extra: (Optional) An object to search for extra information to send with the
|
||||
* login request, e.g. device_id.
|
||||
* callback: (Optional) Function to call on successful login.
|
||||
*/
|
||||
function submitLogin(type, data, extra, callback) {
|
||||
console.log("Logging in with " + type);
|
||||
setTitle(TITLE_POST_AUTH);
|
||||
|
||||
// Add the login type.
|
||||
data.type = type;
|
||||
|
||||
// Add the device information, if it was provided.
|
||||
if (extra.device_id) {
|
||||
data.device_id = extra.device_id;
|
||||
}
|
||||
if (extra.initial_device_display_name) {
|
||||
data.initial_device_display_name = extra.initial_device_display_name;
|
||||
}
|
||||
|
||||
$.post(matrixLogin.endpoint, JSON.stringify(data), function(response) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
matrixLogin.onLogin(response);
|
||||
}).fail(errorFunc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Display an error to the user and show the login form again.
|
||||
*/
|
||||
function errorFunc(err) {
|
||||
// We want to show the error to the user rather than redirecting immediately to the
|
||||
// SSO portal (if SSO is the only login option), so we inhibit the redirect.
|
||||
showLogin(true);
|
||||
|
||||
if (err.responseJSON && err.responseJSON.error) {
|
||||
setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")");
|
||||
}
|
||||
else {
|
||||
setFeedbackString("Request failed: " + err.status);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Display an error to the user.
|
||||
*/
|
||||
function setFeedbackString(text) {
|
||||
$("#feedback").text(text);
|
||||
}
|
||||
|
||||
/*
|
||||
* (Maybe) Show the login forms.
|
||||
*
|
||||
* This actually does a few unrelated functions:
|
||||
*
|
||||
* * Configures the SSO redirect URL to come back to this page.
|
||||
* * Configures and shows the SSO form, if the server supports SSO.
|
||||
* * Otherwise, shows the password form.
|
||||
*/
|
||||
function showLogin(inhibitRedirect) {
|
||||
setTitle(TITLE_PRE_AUTH);
|
||||
|
||||
// If inhibitRedirect is false, and SSO is the only supported login method,
|
||||
// we can redirect straight to the SSO page.
|
||||
if (matrixLogin.serverAcceptsSso) {
|
||||
// Set the redirect to come back to this page, a login token will get
|
||||
// added as a query parameter and handled after the redirect.
|
||||
$("#sso_redirect_url").val(window.location.origin + window.location.pathname);
|
||||
|
||||
// Before submitting SSO, set the current query parameters into a cookie
|
||||
// for retrieval later.
|
||||
var qs = parseQsFromUrl();
|
||||
setCookie(COOKIE_KEY, JSON.stringify(qs));
|
||||
|
||||
// If password is not supported and redirects are allowed, then submit
|
||||
// the form (redirecting to the SSO provider).
|
||||
if (!inhibitRedirect && !matrixLogin.serverAcceptsPassword) {
|
||||
$("#sso_form").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, show the SSO form
|
||||
$("#sso_flow").show();
|
||||
}
|
||||
|
||||
if (matrixLogin.serverAcceptsPassword) {
|
||||
$("#password_flow").show();
|
||||
}
|
||||
|
||||
// If neither password or SSO are supported, show an error to the user.
|
||||
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsSso) {
|
||||
$("#no_login_types").show();
|
||||
}
|
||||
|
||||
$("#loading").hide();
|
||||
}
|
||||
|
||||
/*
|
||||
* Hides the forms and shows a loading throbber.
|
||||
*/
|
||||
function showSpinner() {
|
||||
$("#password_flow").hide();
|
||||
$("#sso_flow").hide();
|
||||
$("#no_login_types").hide();
|
||||
$("#loading").show();
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper to show the page's main title.
|
||||
*/
|
||||
function setTitle(title) {
|
||||
$("#title").text(title);
|
||||
}
|
||||
|
||||
/*
|
||||
* Query the login endpoint for the homeserver's supported flows.
|
||||
*
|
||||
* This populates matrixLogin.serverAccepts* variables.
|
||||
*/
|
||||
function fetchLoginFlows(cb) {
|
||||
$.get(matrixLogin.endpoint, function(response) {
|
||||
for (var i = 0; i < response.flows.length; i++) {
|
||||
var flow = response.flows[i];
|
||||
if ("m.login.sso" === flow.type) {
|
||||
matrixLogin.serverAcceptsSso = true;
|
||||
console.log("Server accepts SSO");
|
||||
}
|
||||
if ("m.login.password" === flow.type) {
|
||||
matrixLogin.serverAcceptsPassword = true;
|
||||
console.log("Server accepts password");
|
||||
}
|
||||
}
|
||||
|
||||
cb();
|
||||
}).fail(errorFunc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Called on load to fetch login flows and attempt SSO login (if a token is available).
|
||||
*/
|
||||
matrixLogin.onLoad = function() {
|
||||
fetchLoginFlows(function() {
|
||||
// (Maybe) attempt logging in via SSO if a token is available.
|
||||
if (!tryTokenLogin()) {
|
||||
showLogin(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Submit simple user & password login.
|
||||
*/
|
||||
matrixLogin.passwordLogin = function() {
|
||||
var user = $("#user_id").val();
|
||||
var pwd = $("#password").val();
|
||||
|
||||
setFeedbackString("");
|
||||
|
||||
showSpinner();
|
||||
submitLogin(
|
||||
"m.login.password",
|
||||
{user: user, password: pwd},
|
||||
parseQsFromUrl());
|
||||
};
|
||||
|
||||
/*
|
||||
* The onLogin function gets called after a successful login.
|
||||
*
|
||||
* It is expected that implementations override this to be notified when the
|
||||
* login is complete. The response to the login call is provided as the single
|
||||
* parameter.
|
||||
*/
|
||||
matrixLogin.onLogin = function(response) {
|
||||
// clobber this function
|
||||
console.warn("onLogin - This function should be replaced to proceed.");
|
||||
};
|
||||
|
||||
/*
|
||||
* Process the query parameters from the current URL into an object.
|
||||
*/
|
||||
function parseQsFromUrl() {
|
||||
var pos = window.location.href.indexOf("?");
|
||||
if (pos == -1) {
|
||||
return {};
|
||||
}
|
||||
var query = window.location.href.substr(pos + 1);
|
||||
|
||||
var result = {};
|
||||
query.split("&").forEach(function(part) {
|
||||
var item = part.split("=");
|
||||
var key = item[0];
|
||||
var val = item[1];
|
||||
|
||||
if (val) {
|
||||
val = decodeURIComponent(val);
|
||||
}
|
||||
result[key] = val;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process the cookies and return an object.
|
||||
*/
|
||||
function parseCookies() {
|
||||
var allCookies = document.cookie;
|
||||
var result = {};
|
||||
allCookies.split(";").forEach(function(part) {
|
||||
var item = part.split("=");
|
||||
// Cookies might have arbitrary whitespace between them.
|
||||
var key = item[0].trim();
|
||||
// You can end up with a broken cookie that doesn't have an equals sign
|
||||
// in it. Set to an empty value.
|
||||
var val = (item[1] || "").trim();
|
||||
// Values might be URI encoded.
|
||||
if (val) {
|
||||
val = decodeURIComponent(val);
|
||||
}
|
||||
result[key] = val;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set a cookie that is valid for 1 hour.
|
||||
*/
|
||||
function setCookie(key, value) {
|
||||
// The maximum age is set in seconds.
|
||||
var maxAge = 60 * 60;
|
||||
// Set the cookie, this defaults to the current domain and path.
|
||||
document.cookie = key + "=" + encodeURIComponent(value) + ";max-age=" + maxAge + ";sameSite=lax";
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes a cookie by key.
|
||||
*/
|
||||
function deleteCookie(key) {
|
||||
// Delete a cookie by setting the expiration to 0. (Note that the value
|
||||
// doesn't matter.)
|
||||
document.cookie = key + "=deleted;expires=0";
|
||||
}
|
||||
|
||||
/*
|
||||
* Submits the login token if one is found in the query parameters. Returns a
|
||||
* boolean of whether the login token was found or not.
|
||||
*/
|
||||
function tryTokenLogin() {
|
||||
// Check if the login token is in the query parameters.
|
||||
var qs = parseQsFromUrl();
|
||||
|
||||
var loginToken = qs.loginToken;
|
||||
if (!loginToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve the original query parameters (from before the SSO redirect).
|
||||
// They are stored as JSON in a cookie.
|
||||
var cookies = parseCookies();
|
||||
var originalQueryParams = JSON.parse(cookies[COOKIE_KEY] || "{}")
|
||||
|
||||
// If the login is successful, delete the cookie.
|
||||
function callback() {
|
||||
deleteCookie(COOKIE_KEY);
|
||||
}
|
||||
|
||||
submitLogin(
|
||||
"m.login.token",
|
||||
{token: loginToken},
|
||||
originalQueryParams,
|
||||
callback);
|
||||
|
||||
return true;
|
||||
}
|
BIN
setup/base/static/client/login/spinner.gif
Normal file
BIN
setup/base/static/client/login/spinner.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
79
setup/base/static/client/login/style.css
Normal file
79
setup/base/static/client/login/style.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
a:link { color: #666; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #000; }
|
||||
a:active { color: #000; }
|
||||
|
||||
input {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
textbox, input[type="text"], input[type="password"] {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
form {
|
||||
text-align: center;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
ul.radiobuttons {
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add some padding to the viewport.
|
||||
*/
|
||||
#container {
|
||||
padding: 10px;
|
||||
}
|
||||
/*
|
||||
* Center all direct children of the main form.
|
||||
*/
|
||||
#container > * {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* A wrapper around each login flow.
|
||||
*/
|
||||
.login_flow {
|
||||
width: 300px;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||
|
||||
background-color: #f8f8f8;
|
||||
border: 1px #ccc solid;
|
||||
}
|
||||
|
||||
/*
|
||||
* Used to show error content.
|
||||
*/
|
||||
#feedback {
|
||||
/* Red text. */
|
||||
color: #ff0000;
|
||||
/* A little space to not overlap the box-shadow. */
|
||||
margin-bottom: 20px;
|
||||
}
|
Loading…
Reference in a new issue