changeset 3776:80830d97da81

mod_invites: New module providing an API to create/manage invite tokens
author Matthew Wild <mwild1@gmail.com>
date Fri, 27 Dec 2019 10:31:33 +0000
parents d60efdb947fd
children 26559776a87e
files mod_invites/README.markdown mod_invites/mod_invites.lua
diffstat 2 files changed, 151 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_invites/README.markdown	Fri Dec 27 10:31:33 2019 +0000
@@ -0,0 +1,22 @@
+
+This module manages the creation and consumption of invite codes for the
+host(s) it is loaded onto. It currently does not expose any admin/user-facing
+functionality (though in the future it will probably gain a way to view/manage
+pending invites).
+
+Other modules can use the API from this module to create invite tokens which
+can be used to e.g. register accounts or create automatic subscription approvals.
+
+# Configuration
+
+`` {.lua}
+-- Configure the number of seconds a token is valid for (default 7 days)
+invite_expiry = 86400 * 7
+```
+
+Note that all modules that use this API will automatically load this module,
+so adding it to modules_enabled is generally not necessary.
+
+# Compatibility
+
+0.11 and later.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_invites/mod_invites.lua	Fri Dec 27 10:31:33 2019 +0000
@@ -0,0 +1,129 @@
+local id = require "util.id";
+local url = require "socket.url";
+local jid_node = require "util.jid".node;
+
+local invite_ttl = module:get_option_number("invite_expiry", 86400 * 7);
+
+local token_storage = module:open_store("invite_token", "map");
+
+local function get_uri(action, jid, token, params) --> string
+	return url.build({
+			scheme = "xmpp",
+			path = jid,
+			query = action..";preauth="..token..(params and (";"..params) or ""),
+		});
+end
+
+local function create_invite(invite_action, invite_jid, allow_registration)
+	local token = id.medium();
+
+	local created_at = os.time();
+	local expires = created_at + invite_ttl;
+
+	local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil;
+
+	local invite = {
+		type = invite_action;
+		jid = invite_jid;
+
+		token = token;
+		allow_registration = allow_registration;
+
+		uri = get_uri(invite_action, invite_jid, token, invite_params);
+
+		created_at = created_at;
+		expires = expires;
+	};
+
+	module:fire_event("invite-created", invite);
+
+	if allow_registration then
+		local ok, err = token_storage:set(nil, token, invite);
+		if not ok then
+			module:log("warn", "Failed to store account invite: %s", err);
+			return nil, "internal-server-error";
+		end
+	end
+
+	if invite_action == "roster" then
+		local username = jid_node(invite_jid);
+		local ok, err = token_storage:set(username, token, expires);
+		if not ok then
+			module:log("warn", "Failed to store subscription invite: %s", err);
+			return nil, "internal-server-error";
+		end
+	end
+
+	return invite;
+end
+
+-- Create invitation to register an account (optionally restricted to the specified username)
+function create_account(account_username) --luacheck: ignore 131/create_account
+	local jid = account_username and (account_username.."@"..module.host) or module.host;
+	return create_invite("register", jid, true);
+end
+
+-- Create invitation to become a contact of a local user
+function create_contact(username, allow_registration) --luacheck: ignore 131/create_contact
+	return create_invite("roster", username.."@"..module.host, allow_registration);
+end
+
+local valid_invite_methods = {};
+local valid_invite_mt = { __index = valid_invite_methods };
+
+function valid_invite_methods:use()
+	if self.username then
+		-- Also remove the contact invite if present, on the
+		-- assumption that they now have a mutual subscription
+		token_storage:set(self.username, self.token, nil);
+	end
+	token_storage:set(nil, self.token, nil);
+	return true;
+end
+
+-- Get a validated invite (or nil, err). Must call :use() on the
+-- returned invite after it is actually successfully used
+-- For "roster" invites, the username of the local user (who issued
+-- the invite) must be passed.
+-- If no username is passed, but the registration is a roster invite
+-- from a local user, the "inviter" field of the returned invite will
+-- be set to their username.
+function get(token, username)
+	if not token then
+		return nil, "no-token";
+	end
+
+	local valid_until, inviter;
+
+	if username then -- token being used for subscription
+		-- Fetch from user store (subscription invite)
+		valid_until = token_storage:get(username, token);
+	else -- token being used for account creation
+		-- Fetch from host store (account invite)
+		local token_info = token_storage:get(nil, token);
+		valid_until = token_info and token_info.expires;
+		if token_info.type == "roster" then
+			username = jid_node(token_info.jid);
+			inviter = username;
+		end
+	end
+
+	if not valid_until then
+		module:log("debug", "Got unknown token: %s", token);
+		return nil, "token-invalid";
+	elseif os.time() > valid_until then
+		module:log("debug", "Got expired token: %s", token);
+		return nil, "token-expired";
+	end
+
+	return setmetatable({
+		token = token;
+		username = username;
+		inviter = inviter;
+	}, valid_invite_mt);
+end
+
+function use(token) --luacheck: ignore 131/use
+	local invite = get(token);
+	return invite and invite:use();
+end