File

mod_invites_api/mod_invites_api.lua @ 5660:8a607be2cdcc

mod_invites_register_web: change config option from `invites_html_template` to `invites_template_html`.
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Tue, 29 Aug 2023 22:05:46 +0700
parent 5639:1664bd4c274b
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;
-- 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 url_base = module:context(module.host):http_url(module.name, "/"..module.name);
	return url_base.."?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("Adjust:");
		print("");
		print("> prosodyctl mod_"..module.name.." renew JID NAME");
		print("> prosodyctl mod_"..module.name.." rename JID NAME NEW_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 == "rename" then
			print("========================================================================");
			print("");
			print("Re-name a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." rename JID NAME NEW_NAME");
			print("");
			print("------------------------------------------------------------------------");
			print("");
			print("Usage:");
			print("");
			print(" `JID` can only be a valid host.");
			print("");
			print(" You need to supply a `NEW_NAME` to replace « `NAME` » the old one.");
			print("");
			print("========================================================================");
		elseif help_command == "renew" then
			print("========================================================================");
			print("");
			print("Re-new a key:");
			print("");
			print("> prosodyctl mod_"..module.name.." renew 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:depends("http");
	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");

	local found = false;
	local function error_found(username, name)
		if name then
			print("Error: Could not find "..name.." in "..domain);
			print("");
			print("To make this API key, run:");
			print("> prosodyctl "..module.name.." create "..domain.." "..name);
			return 1;
		end
		if username then
			print("Error: Could not find "..username.."@"..domain);
			print("");
			print("To make this API key, run:");
			print("> prosodyctl "..module.name.." create "..username.."@"..domain);
			return 1;
		end
	end

	if command == "create" then
		local util_id = require "util.id".short();
		local util_token = require "util.id".long();
		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
			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 == "rename" then
		local util_token = require "util.id".long();
		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
						print("Found:");
						print(date, id.."/"..util_token, info.jid);
						found = true;
					end
				end);
				if not found then
					error_found(username);
					return 1;
				end
				print("Error: not allow to rename user's account.");
				return 1;
			else
				print("Error: "..username.."@"..domain.." doesn't exists!");
				return 1;
			end
		elseif domain then
			local arg_name_orig = table.remove(arg, 1);
			if not arg_name_orig or arg_name_orig == "" then
				print("Error: key for host needs a `NAME`.");
				return 1;
			end
			local arg_name_new = table.remove(arg, 1);
			if not arg_name_new or arg_name_new == "" then
				print("Error: need a `NEW_NAME` to replace "..arg_name_orig);
				return 1;
			end
			get_value(function (id, info)
				if domain == info.jid and arg_name_orig == info.name then
					api_key_store:set(nil, id, {
						id = id;
						token = info.token;
						name = arg_name_new;
						jid = domain;
						created_at = os_time;
						allowed_methods = { GET = true, POST = true };
					});
					date = datetime.datetime(os_time);
					print("Re-named:");
					print(date, id.."/"..info.token, info.jid, arg_name_new);
					found = true;
				end
			end);
			if not found then
				error_found(nil, arg_name_orig);
				return 1;
			end
			return;
		end
	elseif command == "renew" then
		local util_token = require "util.id".long();
		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
						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
					error_found(username);
					return 1;
				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
			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
				error_found(nil, arg_name);
				return 1;
			end
			return;
		end
	elseif command == "get" 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, id, get_url(id, info.token));
					found = true;
				end
			end);
			if found == false then
				error_found(nil, 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
				error_found(username);
			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);
					found = true;
				end
			end);
			if not found then
				error_found(nil, name);
				return 1;
			end
			return;
		elseif username then
			get_value(function (id, info)
				local j = jid.prepped_split(info.jid);
				if j == username then
					print(id);
					found = true
				end
			end);
			if not found then
				error_found(username);
				return 1;
			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);
					found = true;
				end
			end);
			if not found then
				error_found(nil, name);
				return 1;
			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);
					found = true;
				end
			end);
			if not found then
				error_found(username);
				return 1;
			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));
					found = true;
				end
			end);
			if not found then
				error_found(nil, name);
				return 1;
			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));
					found = true;
				end
			end);
			if not found then
				error_found(username);
				return 1;
			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