Mercurial > prosody-modules
changeset 4093:a2116f5a7c8f
mod_invites_register_web: New module to allow web registration with an invite token
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Fri, 11 Sep 2020 13:51:54 +0100 |
parents | 2b6918714792 |
children | dd00a2b9927c |
files | mod_invites_register_web/README.markdown mod_invites_register_web/html/register.html mod_invites_register_web/html/register_error.html mod_invites_register_web/html/register_success.html mod_invites_register_web/html/register_success_setup.html mod_invites_register_web/mod_invites_register_web.lua |
diffstat | 6 files changed, 523 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/README.markdown Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,34 @@ +--- +labels: +- 'Stage-Beta' +summary: 'Register accounts via the web using invite tokens' +... + +Introduction +============ + +This module is part of the suite of modules that implement invite-based +account registration for Prosody. The other modules are: + +- mod_invites +- mod_invites_adhoc +- mod_invites_page +- mod_invites_register +- mod_invites_register_web +- mod_register_apps + +For details and a full overview, start with the mod_invites documentation. + +Details +======= + +mod_invites_register_web implements a web-based registration form that +validates invite tokens. It also supports guiding the user through client +download and configuration via mod_register_apps. + +There is no specific configuration for this module (though it uses the +optional `site_name` to override the displayed site name. + +This module depends on mod_invites_page solely for the case where an invalid +invite token is received - it will redirect to mod_invites_page so that an +appropriate error can be served to the user.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/html/register.html Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{site_name}</title> + <link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> + <meta name="msapplication-TileColor" content="#fbd308"> + <meta name="theme-color" content="#fbd308"> +</head> +<body> + <div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div> + <div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5"> + <div class="card rounded-lg shadow"> + <h1 class="card-header rounded-lg rounded-lg"> + Register on {site_name}<br/> + </h1> + <div class="card-body" > + <p>{site_name} is part of XMPP, a secure and decentralized messaging network. To begin + chatting {app&using {app.name} }you need to first register an account.</p> + + <p>Creating an account will allow to communicate with {inviter&{inviter} and }other + people on {site_name} and other services on the XMPP network.</p> + + {app&{app.supports_preauth_uri& + <div class="alert alert-info"> + <p>If you already have {app.name} installed, + we recommend that you continue the account creation process using the app + by clicking on the button below:</p> + + <h6 class="text-center">{app.name} already installed?</h6> + + <div class="text-center"> + <a href="{uri}"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/> + <small class="text-muted">This button works only if you have the app installed already!</small> + </div> + <br/> + </div> + }} + + <h5 class="card-title">Create an account</h5> + + {message&<div class="alert {msg_class?alert-info}" role="alert"> + {message} + </div>} + + <form method="post"> + <div class="form-group form-row"> + <label for="user" class="col-md-4 col-lg-12 col-form-label">Username:</label> + <div class="col-md-8 col-lg-12"> + <div class="input-group"> + <input + type="text" name="user" class="form-control" aria-describedby="usernameHelp" + required autofocus minlength="1" maxlength="30" length="30" + > + <div class="input-group-append"> + <span class="input-group-text">@{domain}</span> + </div> + </div> + <small id="usernameHelp" class="d-block form-text text-muted">Choose a username, this will become the first part of your new chat address.</small> + </div> + </div> + <div class="form-group form-row"> + <label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label> + <div class="col-md-8 col-lg-12"> + <input type="password" name="password" class="form-control" aria-describedby="passwordHelp" + autocomplete="new-password" + > + <small id="passwordHelp" class="form-text text-muted">Enter a secure password that you do not use anywhere else.</small> + </div> + </div> + <div class="form-group form-row"> + <input type="hidden" name="token" value="{token}"> + {app&<input type="hidden" name="app_id" value="{app.id}">} + <button type="submit" class="btn btn-primary btn-lg">Submit</button> + </div> + </form> + </div> + </div> + </div> + <script src="/share/jquery/jquery.min.js"></script> + <script src="/share/bootstrap4/js/bootstrap.min.js"></script> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/html/register_error.html Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Invite to {site_name}</title> + <link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> + <meta name="msapplication-TileColor" content="#fbd308"> + <meta name="theme-color" content="#fbd308"> +</head> +<body> + <div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div> + <div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5"> + <div class="card rounded-lg shadow"> + <h1 class="card-header rounded-lg rounded-lg"> + Invite to {site_name}<br/> + </h1> + <div class="card-body" > + <h5 class="card-title">Registration error</h5> + + <p>{message?Sorry, there was a problem registering your account.}</p> + </div> + </div> + </div> + <script src="/share/jquery/jquery.min.js"></script> + <script src="/share/bootstrap4/js/bootstrap.min.js"></script> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/html/register_success.html Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{site_name}</title> + <link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> + <meta name="msapplication-TileColor" content="#fbd308"> + <meta name="theme-color" content="#fbd308"> + + <script> + function toggle_password(e) { + var button = e.target; + var input = button.parentNode.parentNode.querySelector("input"); + switch(input.attributes.type.value) { + case "password": + input.attributes.type.value = "text"; + button.innerText = "Hide"; + break; + case "text": + input.attributes.type.value = "password"; + button.innerText = "Show"; + break; + } + } + </script> +</head> +<body> + <div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div> + <div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5"> + <div class="card rounded-lg shadow"> + <h1 class="card-header rounded-lg rounded-lg"> + {site_name}<br/> + </h1> + <div class="card-body"> + <h5 class="card-title">Congratulations!</h5> + + <p>You have created an account on {site_name}.</p> + + <p>To start chatting, you need to enter your new account + credentials into your chosen XMPP software.</p> + + <p>As a final reminder, your account details are shown below:</p> + + <form class="account-details col-12 col-lg-6 mx-auto"> + <div class="form-group form-row"> + <label for="user" class="col-md-4 col-lg-12 col-form-label">Chat address (JID):</label> + <div class="col-md-8 col-lg-12"> + <input type="text" class="form-control-plaintext" readonly value="{username}@{domain}"> + </div> + </div> + {password& + <div class="form-group form-row"> + <label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label> + <div class="col-md-8 col-lg-12"> + <div class="input-group"> + <input type="password" readonly class="form-control" value="{password}"> + <div class="input-group-append"> + <button class="btn btn-outline-secondary" type="button" onclick="toggle_password(event)">Show</button> + </div> + </div> + </div> + </div> + } + </form> + + <p>Your password is stored encrypted on the server and will not be accessible after you close + this page. Keep it safe and never share it with anyone.</p> + </div> + </div> + </div> + <script src="/share/jquery/jquery.min.js"></script> + <script src="/share/bootstrap4/js/bootstrap.min.js"></script> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/html/register_success_setup.html Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{site_name}</title> + <link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> + <meta name="msapplication-TileColor" content="#fbd308"> + <meta name="theme-color" content="#fbd308"> + + <script> + function toggle_password(e) { + var button = e.target; + var input = button.parentNode.parentNode.querySelector("input"); + switch(input.attributes.type.value) { + case "password": + input.attributes.type.value = "text"; + button.innerText = "Hide"; + break; + case "text": + input.attributes.type.value = "password"; + button.innerText = "Show"; + break; + } + } + </script> +</head> +<body> + <div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div> + <div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5"> + <div class="card rounded-lg shadow"> + <h1 class="card-header rounded-lg rounded-lg"> + {site_name}<br/> + </h1> + <div class="card-body"> + <h5 class="card-title">Congratulations!</h5> + + <p>You have created an account on {site_name}!</p> + + <p>You can now set up {app.name} and connect it to your new account.</p> + + <h5>Step 1: Download and install {app.name}</h5> + + <p>{app.download.text?Download and install {app.name} below:}</p> + + <div class="ml-5 mb-3"> + {app.download.buttons# + {item.image& + <a href="{item.url}" {item.target&target="{item.target}"} rel="noopener"> + <img src="{item.image}" {item.alttext&alt="{item.alttext}"}> + </a> + } + {item.text& + <a href="{item.url}" {item.target&target="{item.target}"} rel="noopener"> + <button class="btn btn-primary"> + {item.text} + </button> + </a> + } + } + </div> + + <h5>Step 2: Connect {app.name} to your new account</h5> + + <p>{app.setup.text?Launch {app.name} and sign in using your account credentials.}</p> + + <p>As a final reminder, your account details are shown below:</p> + + <form class="account-details col-12 col-lg-6 mx-auto"> + <div class="form-group form-row"> + <label for="user" class="col-md-4 col-lg-12 col-form-label font-weight-bold">Chat address (JID):</label> + <div class="col-md-8 col-lg-12"> + <input type="text" class="form-control-plaintext" readonly value="{username}@{domain}"> + </div> + </div> + {password& + <div class="form-group form-row"> + <label for="password" class="col-md-4 col-lg-12 col-form-label font-weight-bold">Password:</label> + <div class="col-md-8 col-lg-12"> + <div class="input-group"> + <input type="password" readonly class="form-control" value="{password}"> + <div class="input-group-append"> + <button class="btn btn-outline-secondary" type="button" onclick="toggle_password(event)">Show</button> + </div> + </div> + </div> + </div> + } + </form> + + <p>Your password is stored encrypted at {site_name} and will not be accessible after you close + this page. Keep it safe and never share it with anyone.</p> + </div> + </div> + </div> + <script src="/share/jquery/jquery.min.js"></script> + <script src="/share/bootstrap4/js/bootstrap.min.js"></script> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_register_web/mod_invites_register_web.lua Fri Sep 11 13:51:54 2020 +0100 @@ -0,0 +1,183 @@ +local id = require "util.id"; +local http_formdecode = require "net.http".formdecode; +local usermanager = require "core.usermanager"; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local st = require "util.stanza"; +local url_escape = require "util.http".urlencode; +local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, { + urlescape = url_escape; +}); + + +module:depends("register_apps"); + +local site_name = module:get_option_string("site_name", module.host); +local site_apps = module:shared("register_apps/apps"); + +module:depends("http"); +module:depends("easy_invite"); +local invites = module:depends("invites"); +local invites_page = module:depends("invites_page"); + +function serve_register_page(event) + local register_page_template = assert(module:load_resource("html/register.html")):read("*a"); + + local query_params = http_formdecode(event.request.url.query); + + local invite = invites.get(query_params.t); + if not invite then + return { + status_code = 303; + headers = { + ["Location"] = invites.module:http_url().."?"..event.request.url.query; + }; + }; + end + + local invite_page = render_html_template(register_page_template, { + site_name = site_name; + token = invite.token; + domain = module.host; + uri = invite.uri; + type = invite.type; + jid = invite.jid; + inviter = invite.inviter; + app = query_params.c and site_apps[query_params.c]; + }); + return invite_page; +end + +function handle_register_form(event) + local request, response = event.request, event.response; + local form_data = http_formdecode(request.body); + local user, password, token = form_data["user"], form_data["password"], form_data["token"]; + local app_id = form_data["app_id"]; + + local register_page_template = assert(module:load_resource("html/register.html")):read("*a"); + local error_template = assert(module:load_resource("html/register_error.html")):read("*a"); + + local invite = invites.get(token); + if not invite then + return { + status_code = 303; + headers = { + ["Location"] = invites_page.module:http_url().."?"..event.request.url.query; + }; + }; + end + + response.headers.content_type = "text/html; charset=utf-8"; + + if not user or #user == 0 or not password or #password == 0 or not token then + return render_html_template(register_page_template, { + site_name = site_name; + token = invite.token; + domain = module.host; + uri = invite.uri; + type = invite.type; + jid = invite.jid; + + msg_class = "alert-warning"; + message = "Please fill in all fields."; + }); + end + + -- Shamelessly copied from mod_register_web. + local prepped_username = nodeprep(user); + + if not prepped_username or #prepped_username == 0 then + return render_html_template(register_page_template, { + site_name = site_name; + token = invite.token; + domain = module.host; + uri = invite.uri; + type = invite.type; + jid = invite.jid; + + msg_class = "alert-warning"; + message = "This username contains invalid characters."; + }); + end + + if usermanager.user_exists(prepped_username, module.host) then + return render_html_template(register_page_template, { + site_name = site_name; + token = invite.token; + domain = module.host; + uri = invite.uri; + type = invite.type; + jid = invite.jid; + + msg_class = "alert-warning"; + message = "This username is already in use."; + }); + end + + local registering = { + validated_invite = invite; + username = prepped_username; + host = module.host; + allowed = true; + }; + + module:fire_event("user-registering", registering); + + if not registering.allowed then + return render_html_template(error_template, { + site_name = site_name; + msg_class = "alert-danger"; + message = registering.reason or "Registration is not allowed."; + }); + end + + local ok, err = usermanager.create_user(prepped_username, password, module.host); + + if ok then + module:fire_event("user-registered", { + username = prepped_username; + host = module.host; + source = "mod_"..module.name; + validated_invite = invite; + }); + + local app_info = site_apps[app_id]; + + local success_template; + if app_info then + -- If recognised app, we serve a page that includes setup instructions + success_template = assert(module:load_resource("html/register_success_setup.html")):read("*a"); + else + success_template = assert(module:load_resource("html/register_success.html")):read("*a"); + end + + -- Due to the credentials being served here, ensure that + -- the browser or any intermediary does not cache the page + event.response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; + event.response.headers["Pragma"] = "no-cache"; + event.response.headers["Expires"] = "0"; + + return render_html_template(success_template, { + site_name = site_name; + username = prepped_username; + domain = module.host; + password = password; + app = app_info; + }); + else + local err_id = id.short(); + module:log("warn", "Registration failed (%s): %s", err_id, tostring(err)); + return render_html_template(error_template, { + site_name = site_name; + msg_class = "alert-danger"; + message = ("An unknown error has occurred (%s)"):format(err_id); + }); + end +end + +module:provides("http", { + default_path = "register"; + route = { + ["GET"] = serve_register_page; + ["POST"] = handle_register_form; + }; +});