local id = require " prosody.util.id " ;
local it = require " prosody.util.iterators " ;
local url = require " socket.url " ;
local jid_node = require " prosody.util.jid " . node ;
local jid_split = require " prosody.util.jid " . split ;
local argparse = require " prosody.util.argparse " ;
local default_ttl = module : get_option_period ( " invite_expiry " , " 1 week " ) ;
local token_storage ;
if prosody.process_type == " prosody " or prosody.shutdown then
token_storage = module : open_store ( " invite_token " , " map " ) ;
end
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 , additional_data , ttl , reusable )
local token = id.medium ( ) ;
local created_at = os.time ( ) ;
local expires = created_at + ( ttl or default_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 ;
additional_data = additional_data ;
uri = get_uri ( invite_action , invite_jid , token , invite_params ) ;
created_at = created_at ;
expires = expires ;
reusable = reusable ;
} ;
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 , additional_data , ttl ) --luacheck: ignore 131/create_account
local jid = account_username and ( account_username .. " @ " .. module.host ) or module.host ;
return create_invite ( " register " , jid , true , additional_data , ttl ) ;
end
-- Create invitation to reset the password for an account
function create_account_reset ( account_username , ttl ) --luacheck: ignore 131/create_account_reset
return create_account ( account_username , { allow_reset = account_username } , ttl or 86400 ) ;
end
-- Create invitation to become a contact of a local user
function create_contact ( username , allow_registration , additional_data , ttl ) --luacheck: ignore 131/create_contact
return create_invite ( " roster " , username .. " @ " .. module.host , allow_registration , additional_data , ttl ) ;
end
-- Create invitation to register an account and join a user group
-- If explicit ttl is passed, invite is valid for multiple signups
-- during that time period
function create_group ( group_ids , additional_data , ttl ) --luacheck: ignore 131/create_group
local merged_additional_data = {
groups = group_ids ;
} ;
if additional_data then
for k , v in pairs ( additional_data ) do
merged_additional_data [ k ] = v ;
end
end
return create_invite ( " register " , module.host , true , merged_additional_data , ttl , not not ttl ) ;
end
-- Iterates pending (non-expired, unused) invites that allow registration
function pending_account_invites ( ) --luacheck: ignore 131/pending_account_invites
local store = module : open_store ( " invite_token " ) ;
local now = os.time ( ) ;
local function is_valid_invite ( _ , invite )
return invite.expires > now ;
end
return it.filter ( is_valid_invite , pairs ( store : get ( nil ) or { } ) ) ;
end
function get_account_invite_info ( token ) --luacheck: ignore 131/get_account_invite_info
if not token then
return nil , " no-token " ;
end
-- Fetch from host store (account invite)
local token_info = token_storage : get ( nil , token ) ;
if not token_info then
return nil , " token-invalid " ;
elseif os.time ( ) > token_info.expires then
return nil , " token-expired " ;
end
return token_info ;
end
function delete_account_invite ( token ) --luacheck: ignore 131/delete_account_invite
if not token then
return nil , " no-token " ;
end
return token_storage : set ( nil , token , nil ) ;
end
local valid_invite_methods = { } ;
local valid_invite_mt = { __index = valid_invite_methods } ;
function valid_invite_methods : use ( )
if self.reusable then
return true ;
end
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 ;
-- Fetch from host store (account invite)
local token_info = token_storage : get ( nil , token ) ;
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
valid_until = token_info and token_info.expires ;
if token_info and 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 ;
type = token_info and token_info.type or " roster " ;
uri = token_info and token_info.uri or get_uri ( " roster " , username .. " @ " .. module.host , token ) ;
additional_data = token_info and token_info.additional_data or nil ;
reusable = token_info.reusable ;
} , valid_invite_mt ) ;
end
function use ( token ) --luacheck: ignore 131/use
local invite = get ( token ) ;
return invite and invite : use ( ) ;
end
--- shell command
module : add_item ( " shell-command " , {
section = " invite " ;
section_desc = " Create and manage invitations " ;
name = " create_account " ;
desc = " Create an invitation to make an account on this server with the specified JID (supply only a hostname to allow any username) " ;
args = { { name = " user_jid " , type = " string " } } ;
host_selector = " user_jid " ;
handler = function ( self , user_jid ) --luacheck: ignore 212/self
local username = jid_split ( user_jid ) ;
local invite , err = create_account ( username ) ;
if not invite then return nil , err ; end
return true , invite.landing_page or invite.uri ;
end ;
} ) ;
module : add_item ( " shell-command " , {
section = " invite " ;
section_desc = " Create and manage invitations " ;
name = " create_contact " ;
desc = " Create an invitation to become contacts with the specified user " ;
args = { { name = " user_jid " , type = " string " } , { name = " allow_registration " } } ;
host_selector = " user_jid " ;
handler = function ( self , user_jid , allow_registration ) --luacheck: ignore 212/self
local username = jid_split ( user_jid ) ;
local invite , err = create_contact ( username , allow_registration ) ;
if not invite then return nil , err ; end
return true , invite.landing_page or invite.uri ;
end ;
} ) ;
local subcommands = { } ;
--- prosodyctl command
function module . command ( arg )
local opts = argparse.parse ( arg , { short_params = { h = " help " ; [ " ? " ] = " help " } } ) ;
local cmd = table.remove ( arg , 1 ) ; -- pop command
if opts.help or not cmd or not subcommands [ cmd ] then
print ( " usage: prosodyctl mod_ " .. module.name .. " generate example.com " ) ;
return 2 ;
end
return subcommands [ cmd ] ( arg ) ;
end
function subcommands . generate ( arg )
local sm = require " prosody.core.storagemanager " ;
local mm = require " prosody.core.modulemanager " ;
local host = table.remove ( arg , 1 ) ; -- pop host
assert ( prosody.hosts [ host ] , " Host " .. tostring ( host ) .. " does not exist " ) ;
sm.initialize_host ( host ) ;
module.host = host ; --luacheck: ignore 122/module
token_storage = module : open_store ( " invite_token " , " map " ) ;
local opts = argparse.parse ( arg , {
short_params = { h = " help " ; [ " ? " ] = " help " ; g = " group " } ;
value_params = { group = true ; reset = true ; role = true } ;
array_params = { group = true ; role = true } ;
} ) ;
if opts.help then
print ( " usage: prosodyctl mod_ " .. module.name .. " generate DOMAIN --reset USERNAME " )
print ( " usage: prosodyctl mod_ " .. module.name .. " generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]... " )
print ( )
print ( " This command has two modes: password reset and new account. " )
print ( " If --reset is given, the command operates in password reset mode and in new account mode otherwise. " )
print ( )
print ( " required arguments in password reset mode: " )
print ( )
print ( " --reset USERNAME Generate a password reset link for the given USERNAME. " )
print ( )
print ( " optional arguments in new account mode: " )
print ( )
print ( " --admin Make the new user privileged " )
print ( " Equivalent to --role prosody:admin " )
print ( " --role ROLE Grant the given ROLE to the new user " )
print ( " --group GROUPID Add the user to the group with the given ID " )
print ( " Can be specified multiple times " )
print ( )
print ( " --group can be specified multiple times; the user will be added to all groups. " )
print ( )
print ( " --reset and the other options cannot be mixed. " )
return 2
end
-- Load mod_invites
local invites = module : depends ( " invites " ) ;
-- Optional community module that if used, needs to be loaded here
local invites_page_module = module : get_option_string ( " invites_page_module " , " invites_page " ) ;
if mm.get_modules_for_host ( host ) : contains ( invites_page_module ) then
module : depends ( invites_page_module ) ;
end
local allow_reset ;
if opts.reset then
local nodeprep = require " prosody.util.encodings " . stringprep.nodeprep ;
local username = nodeprep ( opts.reset )
if not username then
print ( " Please supply a valid username to generate a reset link for " ) ;
return 2 ;
end
allow_reset = username ;
end
local roles = opts.role or { } ;
local groups = opts.groups or { } ;
if opts.admin then
-- Insert it first since we don't get order out of argparse
table.insert ( roles , 1 , " prosody:admin " ) ;
end
local invite ;
if allow_reset then
if roles [ 1 ] then
print ( " --role/--admin and --reset are mutually exclusive " )
return 2 ;
end
if # groups > 0 then
print ( " --group and --reset are mutually exclusive " )
end
invite = assert ( invites.create_account_reset ( allow_reset ) ) ;
else
invite = assert ( invites.create_account ( nil , {
roles = roles ,
groups = groups
} ) ) ;
end
print ( invite.landing_page or invite.uri ) ;
end