# HG changeset patch # User Matthew Wild # Date 1599991519 -3600 # Node ID 165ade4ce97b87578b08caa1cb224405c87aee7e # Parent 4656a64e59bedf365765cd77c53c283b5d8a34fe mod_invites_api: New module to create new invites over HTTP diff -r 4656a64e59be -r 165ade4ce97b mod_invites_api/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_api/README.markdown Sun Sep 13 11:05:19 2020 +0100 @@ -0,0 +1,113 @@ +--- +labels: +- 'Stage-Beta' +summary: 'Authenticated HTTP API to create invites' +rockspec: + dependencies: + - mod_invites +... + +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_api provides an authenticated HTTP API to create invites +using mod_invites. + +You can use the command-line to create and manage API keys. + +Configuration +============= + +There are no specific configuration options for this module. + +All the usual [HTTP configuration options](https://prosody.im/doc/http) +can be used to configure this module. + +API usage +========= + +Step 1: Create an API key, with an optional name to help you remember what +it is for + +``` +$ prosodyctl mod_invites_api create example.com "My test key" +``` + +**Tip:** Remember to put quotes around your key name if it contains spaces. + +The command will print out a key: + +``` +HTwALnKL/73UUylA-2ZJbu9x1XMATuIbjWpip8ow1 +``` + +Step 2: Make a HTTP request to Prosody, containing the key + +``` +$ curl -v https://example.com:5281/invites_api?key=HTwALnKL/73UUylA-2ZJbu9x1XMATuIbjWpip8ow1 +``` + +Prosody will respond with a HTTP status code "201 Created" to indicate +creation of the invite, and per HTTP's usual rules, the URL of the created +invite page will be in the `Location` header: + +``` +< HTTP/1.1 201 Created +< Access-Control-Max-Age: 7200 +< Connection: Keep-Alive +< Access-Control-Allow-Origin: * +< Date: Sun, 13 Sep 2020 09:50:19 GMT +< Access-Control-Allow-Headers: Content-Type +< Access-Control-Allow-Methods: OPTIONS, GET +< Content-Length: 0 +< Location: https://example.com/invite?c-vhJjyB5Pb4HpAf +``` + +Sometimes for convenience, you may want to just visit the URL in the +browser. Append `&redirect=true` to the URL, and instead Prosody will +return a `303 See Other` response code, which will tell the browser to +redirect straight to the newly-created invite. This is super handy in a +bookmark :) + +If using the API programmatically, it is recommended to put the key in +the `Authorization` header if possible. This is quite simple: + +``` +Authorization: Bearer HTwALnKL/73UUylA-2ZJbu9x1XMATuIbjWpip8ow1 +``` + +Key management +============== + +At any time you can view authorized keys using: + +``` +prosodyctl mod_invites_api list example.com +``` + +This will list out the id of each key, and the name if set: + +``` +HTwALnKL My test key +``` + +You can revoke a key by passing this key id to the 'delete` sub-command: + +``` +prosodyctl mod_invites_api delete example.com HTwALnKL +``` \ No newline at end of file diff -r 4656a64e59be -r 165ade4ce97b mod_invites_api/mod_invites_api.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_invites_api/mod_invites_api.lua Sun Sep 13 11:05:19 2020 +0100 @@ -0,0 +1,124 @@ +local http_formdecode = require "net.http".formdecode; + +local api_key_store; +local invites; +-- 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"); +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); + 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 + + local invite = invites.create_account(nil, { source = "api/token/"..api_user.id }); + 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 module.command(arg) + if #arg < 2 then + print("Usage:"); + print(""); + print(" prosodyctl mod_"..module.name.." create NAME"); + print(" prosodyctl mod_"..module.name.." delete KEY_ID"); + print(" prosodyctl mod_"..module.name.." list"); + print(""); + end + + local command = table.remove(arg, 1); + + local host = table.remove(arg, 1); + if not prosody.hosts[host] then + print("Error: please supply a valid host"); + return 1; + end + require "core.storagemanager".initialize_host(host); + module.host = host; --luacheck: ignore 122/module + api_key_store = module:open_store("invite_api_keys", "map"); + + if command == "create" then + local id = require "util.id".short(); + local token = require "util.id".long(); + api_key_store:set(nil, id, { + id = id; + token = token; + name = arg[1]; + created_at = os.time(); + }); + print(id.."/"..token); + elseif command == "delete" then + local id = table.remove(arg, 1); + if not api_key_store:get(nil, id) then + print("Error: key not found"); + return 1; + end + api_key_store:set(nil, id, nil); + elseif command == "list" then + local api_key_store_kv = module:open_store("invite_api_keys"); + for key_id, key_info in pairs(api_key_store_kv:get(nil)) do + print(key_id, key_info.name or ""); + end + else + print("Unknown command - "..command); + end +end