mod_invites_register_web: New module to allow web registration with an invite token
--- /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;
+ };
+});