File

mod_invites_api/mod_invites_api.lua @ 5595:f7410850941f

mod_invites_api: Change and add new commands for `module.command` to improve UX.
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Wed, 26 Jul 2023 18:43:45 +0700
parent 5143:1cae382e88a1
child 5628:a74b07764d3f
line wrap: on
line source

local http_formdecode = require "net.http".formdecode;
local jid = require "util.jid";
local usermanager = require "core.usermanager";
local datetime = require "util.datetime";

-- Whether local users can invite other users to create an account on this server
local allow_user_invites = module:get_option_boolean("allow_user_invites", false);
-- Who can see and use the contact invite command. It is strongly recommened to
-- keep this available to all local users. To allow/disallow invite-registration
-- on the server, use the option above instead.
local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true);

local api_key_store, api_key_store_kv, invites, date, config, path;
-- COMPAT: workaround to avoid executing inside prosodyctl
if prosody.shutdown then
	module:depends("http");
	api_key_store = module:open_store("invite_api_keys", "map");
	invites = module:depends("invites");
	api_key_store_kv = module:open_store("invite_api_keys");
end

local function get_api_user(request, params)
	local combined_key;

	local auth_header = request.headers.authorization;

	if not auth_header then
		params = params or http_formdecode(request.url.query or "=");
		combined_key = params.key;
	else
		local auth_type, value = auth_header:match("^(%S+)%s(%S+)$");
		if auth_type ~= "Bearer" then
			return;
		end
		combined_key = value;
	end

	if not combined_key then
		return;
	end

	local key_id, key_token = combined_key:match("^([^/]+)/(.+)$");

	if not key_id then
		return;
	end

	local api_user = api_key_store:get(nil, key_id);

	if not api_user or api_user.token ~= key_token then
		return;
	end

	-- TODO: key expiry, rate limiting, etc.
	return api_user;
end

function handle_request(event)
	local query_params = http_formdecode(event.request.url.query);

	local api_user = get_api_user(event.request, query_params);

	if not api_user then
		return 403;
	end

	if api_user.allowed_methods and not api_user.allowed_methods[event.request.method] then
		return 405;
	end

	local invite;
	local username, domain = jid.prepped_split(api_user.jid);
	if username and usermanager.user_exists(username, domain) then
		local ttl = module:get_option_number("invite_expiry", 86400 * 7);
		invite = invites.create_contact(username,
			allow_user_invites, -- allow_registration
			{ source = "api/token/"..api_user.id }, -- additional_data
			ttl
			);
	else
		invite = invites.create_account(nil, { source = "api/token/"..api_user.id });
	end
	if not invite then
		return 500;
	end

	event.response.headers.Location = invite.landing_page or invite.uri;

	if query_params.redirect then
		return 303;
	end
	return 201;
end

if invites then
	module:provides("http", {
		route = {
			["GET"] = handle_request;
		};
	});
end

function get_value(callback)
	for key_id, key_info in pairs(api_key_store_kv:get(nil) or {}) do
		date = datetime.datetime(key_info.created_at);
		callback(key_id, key_info);
	end
end

local function get_url(id, token)
	local config, path = module:get_option("http_paths");
	if config then
		for k, v in pairs(config) do
			if k == module.name then 
				path = v;
				break;
			else 
				path = "/"..module.name;
			end
		end
	else path = "/"..module.name ; end

	local url_base = module:get_option_string("http_external_link", module.host);
	return url_base..path.."?key="..id.."/"..token.."&redirect=true";
end

function module.command(arg)
	if #arg < 2 then
		print("========================================================================");
		print("");
		print("Create:");
		print("");
		print("> prosodyctl mod_"..module.name.." create JID NAME");
		print("");
		print("Query:");
		print("");
		print("> prosodyctl mod_"..module.name.." get JID NAME");
		print("> prosodyctl mod_"..module.name.." date JID NAME");
		print("> prosodyctl mod_"..module.name.." id JID NAME");
		print("> prosodyctl mod_"..module.name.." key JID NAME");
		print("> prosodyctl mod_"..module.name.." url JID NAME");
		print("");
		print("Revoke:");
		print("");
		print("> prosodyctl mod_"..module.name.." delete JID NAME");
		print("> prosodyctl mod_"..module.name.." delete-id HOST ID");
		print("");
		print("Renew:");
		print("");
		print("> prosodyctl mod_"..module.name.." renew JID NAME");
		print("");
		print("Help:");
		print("");
		print("> prosodyctl mod_"..module.name.." help COMMAND");
		print("");
		print("========================================================================");
		return;
	end

	local command = table.remove(arg, 1);
	if command == "help" then
		help_command = table.remove(arg, 1);
		if help_command == "create" then
			print("========================================================================");
			print("");
			print("Create a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." create JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" `JID` can either be a user's account or a host.");
			print(" When `JID` is a host, you need to supply a `NAME`.");
			print("");
			print(" Each user's account can only have 1 API key but hosts are unlimited.");
			print("");
			print("========================================================================");
		elseif help_command == "renew" then
			print("========================================================================");
			print("");
			print("Re-new a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." create JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" `JID` can either be a user's account or a host.");
			print(" When `JID` is a host, you need to supply a `NAME`.");
			print("");
			print(" The old `ID` will be kept and a new token will be generated for the API");
			print(" key you specified.");
			print("");
			print("========================================================================");
		elseif help_command == "get" then
			print("========================================================================");
			print("");
			print("Get info of a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." get JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" When `JID` is a domain, it will list all the keys under that host.");
			print(" When `JID` is a user's account, it fetches the key for that user.");
			print(" If you supply both a host and a `NAME`, it fetches the key with `NAME`");
			print(" under that host.")
			print("");
			print(" Output for a host is: DATE, ID, JID, NAME.");
			print("");
			print(" Output for a user's account is: DATE, ID, URL.");
			print("");
			print(" Output for a host with a valid `NAME` is: DATE, ID, URL.");
			print("");
			print("========================================================================");
		elseif help_command == "date" then
			print("========================================================================");
			print("");
			print("Get the time stamp of a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." date JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Same as the `get` command but print only the birthday of the key.");
			print("");
			print("========================================================================");
		elseif help_command == "id" then
			print("========================================================================");
			print("");
			print("Print the ID of a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." id JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Same as the `get` command but print only the ID of the key.");
			print("");
			print("========================================================================");
		elseif help_command == "key" then
			print("========================================================================");
			print("");
			print("Print the API key:");
			print("");
			print("> prosodyctl mod_"..module.name.." key JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Same as the `get` command but print only the key.");
			print("");
			print(" The key has the format: ID/TOKEN");
			print("");
			print(" The `renew` command will generate a new token and revoke the old one.");
			print("");
			print("========================================================================");
		elseif help_command == "url" then
			print("========================================================================");
			print("");
			print("Print the URL of a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." url JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Same as the `get` command but print only the URL of the key.");
			print("");
			print("========================================================================");
		elseif help_command == "delete" then
			print("========================================================================");
			print("");
			print("Delete a key by JID and NAME:");
			print("");
			print("> prosodyctl mod_"..module.name.." delete JID NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Same as `create` command but delete the key specified.");
			print("");
			print("========================================================================");
		elseif help_command == "delete-id" then
			print("========================================================================");
			print("");
			print("Delete API key by ID:");
			print("");
			print("> prosodyctl mod_"..module.name.." delete HOST ID");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" Input must be a valid host - cannot be a user's account.");
			print("");
			print(" To get the ID of a key, use the `id` command.");
			print("");
			print("========================================================================");
		end
		return;
	end

	local username, domain = jid.prepped_split(table.remove(arg, 1));
	if not prosody.hosts[domain] then
		print("Error: please supply a valid host.");
		return 1;
	end
	require "core.storagemanager".initialize_host(domain);
	module.host = domain; --luacheck: ignore 122/module
	usermanager.initialize_host(module.host);
	api_key_store = module:open_store("invite_api_keys", "map");
	api_key_store_kv = module:open_store("invite_api_keys");

	if command == "create" then
		local util_id = require "util.id".short();
		local util_token = require "util.id".long();
		local found = false;
		local os_time = os.time();
		if username then
			if usermanager.user_exists(username, module.host) then
				get_value(function (id, info)
					if username.."@"..module.host == info.jid then
						date = datetime.datetime(info.created_at);
						print("Found:");
						print(date, id, info.jid);
						util_id = id;
						util_token = info.token;
						found = true;
					end
				end);
				if found == false then
					api_key_store:set(nil, util_id, {
						id = util_id;
						token = util_token;
						name = nil;
						jid = username.."@"..domain;
						created_at = os_time;
						allowed_methods = { GET = true, POST = true };
					});
					date = datetime.datetime(os_time);
					print("Created:");
					print(date, util_id.."/"..util_token, username.."@"..domain);
				end
				return;
			else
				print("Error: "..username.."@"..domain.." does not exists.");
				return 1;
			end
		elseif domain then
			local arg_name = table.remove(arg, 1);
			if not arg_name or arg_name == "" then
				print("Error: key for host needs a `NAME`.");
				return;
			end
			found = false;
			get_value(function (id, info)
				if domain == info.jid and arg_name == info.name then
					date = datetime.datetime(info.created_at);
					print("Found:");
					print(date, id, info.jid, info.name);
					util_id = id;
					util_token = info.token;
					found = true;
				end
			end);
			if found == false then
				api_key_store:set(nil, util_id, {
					id = util_id;
					token = util_token;
					name = arg_name;
					jid = domain;
					created_at = os_time;
					allowed_methods = { GET = true, POST = true };
				});
				date = datetime.datetime(os_time);
				print("Created:");
				print(date, util_id.."/"..util_token, domain, arg_name);
			end
			return;
		end
	elseif command == "renew" then
		local util_token = require "util.id".long();
		local os_time = os.time();
		if username then
			local found;
			if usermanager.user_exists(username, module.host) then
				get_value(function (id, info)
					if username.."@"..module.host == info.jid then
						api_key_store:set(nil, id, {
							id = id;
							token = util_token;
							name = arg[1];
							jid = username.."@"..domain;
							created_at = os_time;
							allowed_methods = { GET = true, POST = true };
						});
						found = true;
						date = datetime.datetime(os_time);
						print("Re-newed:");
						print(date, id.."/"..util_token, info.jid);
					end
				end);
				if not found then
					print("Error: Could not find the key for "..username.."@"..domain);
					print("");
					print("To make this API key, run:");
					print("prosodyctl "..module.name.." create "..username.."@"..domain);
					return;
				end
				return;
			else
				print("Error: "..username.."@"..domain.." does not exists.");
				return 1;
			end
		elseif domain then
			local arg_name = table.remove(arg, 1);
			if not arg_name or arg_name == "" then
				print("Error: key for host needs a `NAME`.");
				return 1;
			end
			found = false;
			get_value(function (id, info)
				if domain == info.jid and arg_name == info.name then
					api_key_store:set(nil, id, {
						id = id;
						token = util_token;
						name = arg_name;
						jid = domain;
						created_at = os_time;
						allowed_methods = { GET = true, POST = true };
					});
					date = datetime.datetime(os_time);
					print("Re-newed:");
					print(date, id.."/"..util_token, info.jid, info.name);
					found = true;
				end
			end);
			if not found then
				date = datetime.datetime(os_time);
				print("Error: Could not find "..arg_name.." in "..domain);
				print("");
				print("To make this API key, run:");
				print("prosodyctl "..module.name.." create "..domain.." "..arg_name);
				return;
			end
			return;
		end
	elseif command == "get" then
		local name = table.remove(arg, 1);
		local found = false;
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					print(date, id, get_url(id, info.token));
					found = true;
				end
			end);
			if found == false then
				print("Error: could not find "..name.." in "..domain);
				print("");
				print("You can create it with:");
				print("");
				print("> prosodyctl "..module.name.." create "..domain.." "..name);
			end
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(date, id, get_url(id, info.token));
					found = true;
				end
			end);
			if found == false then
				print("Error: could not find the key for "..username.."@"..domain);
				print("");
				print("You can create it with:");
				print("");
				print("> prosodyctl "..module.name.." create "..username.."@"..domain);
			end
			return;
		else
			get_value(function (id, info)
				if info.jid == module.host then
					print(date, id, info.jid, info.name or "<unknown>");
				else
					print(date, id, info.jid, info.name or "");
				end
			end);
			return;
		end
	elseif command == "date" then
		local name = table.remove(arg, 1);
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					print(date);
				end
			end);
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(date);
				end
			end);
			return;
		else
			get_value(function (id, info)
				if info.jid == module.host then
					print(date, info.jid, info.name or "<unknown>");
				else
					print(date, info.jid);
				end
			end);
			return;
		end
	elseif command == "id" then
		local name = table.remove(arg, 1);
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					print(id);
				end
			end);
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(id);
				end
			end);
			return;
		else
			get_value(function (id, info)
				if info.jid == module.host then
					print(id, info.jid, info.name or "<unknown>");
				else
					print(id, info.jid);
				end
			end);
			return;
		end
	elseif command == "key" then
		local name = table.remove(arg, 1);
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					print(id.."/"..info.token);
				end
			end);
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(id.."/"..info.token);
				end
			end);
			return;
		else
			get_value(function (id, info)
				if info.jid == module.host then
					print(id.."/"..info.token, info.jid, info.name or "<unknown>");
				else
					print(id.."/"..info.token, info.jid);
				end
			end);
			return;
		end
	elseif command == "url" then
		local name = table.remove(arg, 1);
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					print(get_url(id, info.token));
				end
			end);
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(get_url(id, info.token));
				end
			end);
			return;
		else
			get_value(function (id, info)
				if info.jid == module.host then
					print(get_url(id, info.token), info.jid, info.name or "<unknown>");
				else
					print(get_url(id, info.token), info.jid);
				end
			end);
			return;
		end
	elseif command == "delete" then
		local name = table.remove(arg, 1);
		if name and not username then
			get_value(function (id, info)
				if info.name == name then
					api_key_store:set(nil, id, nil);
					print("Deleted:");
					print(date, id.."/"..info.token, info.jid, info.name or "");
				end
			end);
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					api_key_store:set(nil, id, nil);
					print("Deleted:");
					print(date, id.."/"..info.token, info.jid, info.name or "");
				end
			end);
			return;
		else
			print("Error: Needs a valid `JID`. Or a host and a `NAME`.");
		end
	elseif command == "delete-id" then
		if username then
			print("Error: Input must be a valid host - cannot be a user's account.");
			return 1;
		end
		local arg_id = table.remove(arg, 1);
		if not api_key_store:get(nil, arg_id) then
			print("Error: key not found");
			return 1;
		else
			get_value(function (id, info)
				if arg_id == id then
					api_key_store:set(nil, id, nil);
					print("Deleted:");
					print(date, id, info.jid, info.name or "");
				end
			end);
			return;
		end
	else
		print("Unknown command - "..command);
	end
end