prosody: Broad update to upstream (prosody-modules) where appropriate
This commit is contained in:
parent
2207199a60
commit
1bff00eba7
|
@ -84,14 +84,25 @@ modules_enabled = {
|
|||
"welcome"; -- Welcome users who register accounts
|
||||
"http_files"; -- Serve static files from a directory over HTTP
|
||||
"reload_modules";
|
||||
"landing_page";
|
||||
|
||||
-- Invites
|
||||
"groups_migration";
|
||||
"invites";
|
||||
"invites_adhoc";
|
||||
"invites_api";
|
||||
"invites_groups";
|
||||
"invites_page";
|
||||
"invites_register";
|
||||
"invites_register_api";
|
||||
"invites_tracking";
|
||||
|
||||
"invites_api";
|
||||
"easy_invite";
|
||||
"watchregistrations";
|
||||
"firewall";
|
||||
|
||||
-- Circles
|
||||
"groups_migration";
|
||||
|
||||
-- For the web portal
|
||||
"http_oauth2";
|
||||
"http_admin_api";
|
||||
|
@ -113,7 +124,7 @@ legacy_ssl_ports = { 5223 }
|
|||
allow_registration = true
|
||||
registration_invite_only = true
|
||||
|
||||
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/_/invite?{token}");
|
||||
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/invite?{token}");
|
||||
|
||||
c2s_require_encryption = true
|
||||
s2s_require_encryption = true
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
- mod_cloud_notify_encrypted
|
||||
- mod_cloud_notify_priority_tag
|
||||
- mod_cloud_notify_filters
|
||||
- mod_invite
|
||||
- mod_block_registrations
|
||||
- mod_compact_resource
|
||||
- mod_conversejs
|
||||
|
@ -86,7 +85,6 @@
|
|||
- mod_lastlog
|
||||
- mod_limit_auth
|
||||
- mod_password_policy
|
||||
- mod_password_reset
|
||||
- mod_roster_allinall
|
||||
- mod_strict_https
|
||||
- mod_vcard_muc
|
||||
|
@ -95,21 +93,21 @@
|
|||
- mod_http_altconnect
|
||||
- mod_bookmarks
|
||||
- mod_default_bookmarks
|
||||
- mod_muc_defaults
|
||||
- mod_muc_local_only
|
||||
- mod_firewall
|
||||
- mod_turncredentials
|
||||
- mod_admin_notify
|
||||
- mod_http_oauth2
|
||||
- mod_http_admin_api
|
||||
- mod_rest
|
||||
|
||||
- name: Install Bootstrap and JS libs
|
||||
apt:
|
||||
name:
|
||||
- libjs-bootstrap4
|
||||
- libjs-jquery
|
||||
install_recommends: no
|
||||
- mod_groups_migration
|
||||
- mod_invites
|
||||
- mod_invites_adhoc
|
||||
- mod_invites_api
|
||||
- mod_invites_groups
|
||||
- mod_invites_page
|
||||
- mod_invites_register
|
||||
- mod_invites_register_api
|
||||
- mod_invites_tracking
|
||||
|
||||
- name: Enable wanted modules
|
||||
file:
|
||||
|
@ -117,12 +115,6 @@
|
|||
src: "/usr/local/lib/snikket-modules/{{item}}"
|
||||
dest: "/etc/prosody/modules/{{item}}"
|
||||
loop:
|
||||
- mod_landing_page
|
||||
- mod_invites
|
||||
- mod_invites_page
|
||||
- mod_invites_register
|
||||
- mod_invites_api
|
||||
- mod_easy_invite
|
||||
- mod_update_check
|
||||
- mod_update_notify
|
||||
- mod_authz_internal
|
||||
|
|
|
@ -1,244 +0,0 @@
|
|||
-- XEP-0401: Easy User Onboarding
|
||||
local dataforms = require "util.dataforms";
|
||||
local datetime = require "util.datetime";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local split_jid = require "util.jid".split;
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local st = require "util.stanza";
|
||||
|
||||
local invite_only = module:get_option_boolean("registration_invite_only", true);
|
||||
local require_encryption = module:get_option_boolean("c2s_require_encryption",
|
||||
module:get_option_boolean("require_encryption", false));
|
||||
|
||||
local new_adhoc = module:require("adhoc").new;
|
||||
|
||||
-- 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", true);
|
||||
|
||||
local invites;
|
||||
if prosody.shutdown then -- COMPAT hack to detect prosodyctl
|
||||
invites = module:depends("invites");
|
||||
end
|
||||
|
||||
local invite_result_form = dataforms.new({
|
||||
title = "Your Invite",
|
||||
-- TODO instructions = something helpful
|
||||
{
|
||||
name = "uri";
|
||||
label = "Invite URI";
|
||||
-- TODO desc = something helpful
|
||||
},
|
||||
{
|
||||
name = "url" ;
|
||||
var = "landing-url";
|
||||
label = "Invite landing URL";
|
||||
},
|
||||
{
|
||||
name = "expire";
|
||||
label = "Token valid until";
|
||||
},
|
||||
});
|
||||
|
||||
module:depends("adhoc");
|
||||
module:provides("adhoc", new_adhoc("New Invite", "urn:xmpp:invite#invite",
|
||||
function (_, data)
|
||||
local username = split_jid(data.from);
|
||||
local invite = invites.create_contact(username, allow_user_invites);
|
||||
--TODO: check errors
|
||||
return {
|
||||
status = "completed";
|
||||
form = {
|
||||
layout = invite_result_form;
|
||||
values = {
|
||||
uri = invite.uri;
|
||||
url = invite.landing_page;
|
||||
expire = datetime.datetime(invite.expires);
|
||||
};
|
||||
};
|
||||
};
|
||||
end, "local_user"));
|
||||
|
||||
|
||||
-- TODO
|
||||
-- module:provides("adhoc", new_adhoc("Create account", "urn:xmpp:invite#create-account", function () end, "admin"));
|
||||
|
||||
-- XEP-0379: Pre-Authenticated Roster Subscription
|
||||
module:hook("presence/bare", function (event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.type ~= "subscribe" then return end
|
||||
|
||||
local preauth = stanza:get_child("preauth", "urn:xmpp:pars:0");
|
||||
if not preauth then return end
|
||||
local token = preauth.attr.token;
|
||||
if not token then return end
|
||||
|
||||
local username, host = jid_split(stanza.attr.to);
|
||||
|
||||
local invite, err = invites.get(token, username);
|
||||
|
||||
if not invite then
|
||||
module:log("debug", "Got invalid token, error: %s", err);
|
||||
return;
|
||||
end
|
||||
|
||||
local contact = jid_bare(stanza.attr.from);
|
||||
|
||||
module:log("debug", "Approving inbound subscription to %s from %s", username, contact);
|
||||
if rostermanager.set_contact_pending_in(username, host, contact, stanza) then
|
||||
if rostermanager.subscribed(username, host, contact) then
|
||||
invite:use();
|
||||
rostermanager.roster_push(username, host, contact);
|
||||
|
||||
-- Send back a subscription request (goal is mutual subscription)
|
||||
if not rostermanager.is_user_subscribed(username, host, contact)
|
||||
and not rostermanager.is_contact_pending_out(username, host, contact) then
|
||||
module:log("debug", "Sending automatic subscription request to %s from %s", contact, username);
|
||||
if rostermanager.set_contact_pending_out(username, host, contact) then
|
||||
rostermanager.roster_push(username, host, contact);
|
||||
module:send(st.presence({type = "subscribe", to = contact }));
|
||||
else
|
||||
module:log("warn", "Failed to set contact pending out for %s", username);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end, 1);
|
||||
|
||||
-- TODO sender side, magic automatic mutual subscription
|
||||
|
||||
local invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:invite" }):up();
|
||||
module:hook("stream-features", function(event)
|
||||
local session, features = event.origin, event.features;
|
||||
|
||||
-- Advertise to unauthorized clients only.
|
||||
if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then
|
||||
return
|
||||
end
|
||||
|
||||
features:add_child(invite_stream_feature);
|
||||
end);
|
||||
|
||||
-- Client is submitting a preauth token to allow registration
|
||||
module:hook("stanza/iq/urn:xmpp:pars:0:preauth", function(event)
|
||||
local preauth = event.stanza.tags[1];
|
||||
local token = preauth.attr.token;
|
||||
local validated_invite = invites.get(token);
|
||||
if not validated_invite then
|
||||
local reply = st.error_reply(event.stanza, "cancel", "forbidden", "The invite token is invalid or expired");
|
||||
event.origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
event.origin.validated_invite = validated_invite;
|
||||
local reply = st.reply(event.stanza);
|
||||
event.origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- Registration attempt - ensure a valid preauth token has been supplied
|
||||
module:hook("user-registering", function (event)
|
||||
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
|
||||
if invite_only and not validated_invite then
|
||||
event.allowed = false;
|
||||
event.reason = "Registration on this server is through invitation only";
|
||||
return;
|
||||
end
|
||||
if validated_invite.additional_data and validated_invite.additional_data.allow_reset then
|
||||
event.allow_reset = validated_invite.additional_data.allow_reset;
|
||||
end
|
||||
end);
|
||||
|
||||
-- Make a *one-way* subscription. User will see when contact is online,
|
||||
-- contact will not see when user is online.
|
||||
function subscribe(host, user_username, contact_username)
|
||||
local user_jid = user_username.."@"..host;
|
||||
local contact_jid = contact_username.."@"..host;
|
||||
-- Update user's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_out(user_username, host, contact_jid);
|
||||
-- Update contact's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_in(contact_username, host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, host, user_jid);
|
||||
-- Update user's roster to say subscription request approved...
|
||||
rostermanager.process_inbound_subscription_approval(user_username, host, contact_jid);
|
||||
end
|
||||
|
||||
-- Make a mutual subscription between jid1 and jid2. Each JID will see
|
||||
-- when the other one is online.
|
||||
function subscribe_both(host, user1, user2)
|
||||
subscribe(host, user1, user2);
|
||||
subscribe(host, user2, user1);
|
||||
end
|
||||
|
||||
-- Registration successful, if there was a preauth token, mark it as used
|
||||
module:hook("user-registered", function (event)
|
||||
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
|
||||
if not validated_invite then
|
||||
return;
|
||||
end
|
||||
local inviter_username = validated_invite.inviter;
|
||||
local contact_username = event.username;
|
||||
validated_invite:use();
|
||||
|
||||
if inviter_username then
|
||||
module:log("debug", "Creating mutual subscription between %s and %s", inviter_username, contact_username);
|
||||
subscribe_both(module.host, inviter_username, contact_username);
|
||||
end
|
||||
|
||||
if validated_invite.additional_data then
|
||||
module:log("debug", "Importing roles from invite");
|
||||
local roles = validated_invite.additional_data.roles;
|
||||
if roles then
|
||||
module:open_store("roles"):set(contact_username, roles);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
-- Equivalent of user-registered but for when the account already existed
|
||||
-- (i.e. password reset)
|
||||
module:hook("user-password-reset", function (event)
|
||||
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
|
||||
if not validated_invite then
|
||||
return;
|
||||
end
|
||||
validated_invite:use();
|
||||
end);
|
||||
|
||||
local sm = require "core.storagemanager";
|
||||
function module.command(arg)
|
||||
if #arg < 2 or arg[2] ~= "generate" then
|
||||
print("usage: prosodyctl mod_easy_invite example.net generate");
|
||||
return;
|
||||
end
|
||||
|
||||
local host = arg[1];
|
||||
assert(hosts[host], "Host "..tostring(host).." does not exist");
|
||||
sm.initialize_host(host);
|
||||
|
||||
-- Load mod_invites
|
||||
invites = module:context(host):depends("invites");
|
||||
module:context(host):depends("invites_page");
|
||||
|
||||
table.remove(arg, 1);
|
||||
table.remove(arg, 1);
|
||||
|
||||
local invite, roles;
|
||||
if arg[1] == "--reset" then
|
||||
local nodeprep = require "util.encodings".stringprep.nodeprep;
|
||||
local username = nodeprep(arg[2]);
|
||||
if not username then
|
||||
print("Please supply a valid username to generate a reset link for");
|
||||
return;
|
||||
end
|
||||
invite = invites.create_account_reset(username);
|
||||
else
|
||||
if arg[1] == "--admin" then
|
||||
roles = { ["prosody:admin"] = true };
|
||||
elseif arg[1] == "--role" then
|
||||
roles = { [arg[2]] = true };
|
||||
end
|
||||
invite = invites.create_account(nil, { roles = roles });
|
||||
end
|
||||
|
||||
print(invite.landing_page or invite.uri);
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
local serve = require "net.http.files".serve;
|
||||
|
||||
module:set_global();
|
||||
|
||||
local path = module:get_option_string("acme_challenge_path", "/var/www/.well-known/acme-challenge");
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/.well-known/acme-challenge";
|
||||
route = {
|
||||
["GET /*"] = serve({ path = path });
|
||||
}
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
local mime_map = module:shared("/*/http_files/mime").types or {
|
||||
css = "text/css",
|
||||
js = "application/javascript",
|
||||
};
|
||||
|
||||
local libjs_path = module:get_option_string("libjs_path", "/usr/share/javascript");
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/share";
|
||||
route = {
|
||||
["GET /*"] = require "net.http.files".serve({ path = libjs_path, mime_map = mime_map });
|
||||
}
|
||||
});
|
|
@ -1,140 +0,0 @@
|
|||
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, additional_data)
|
||||
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;
|
||||
additional_data = additional_data;
|
||||
|
||||
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, additional_data) --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);
|
||||
end
|
||||
|
||||
-- Create invitation to reset the password for an account
|
||||
function create_account_reset(account_username) --luacheck: ignore 131/create_account_reset
|
||||
return create_account(account_username, { allow_reset = account_username });
|
||||
end
|
||||
|
||||
-- Create invitation to become a contact of a local user
|
||||
function create_contact(username, allow_registration, additional_data) --luacheck: ignore 131/create_contact
|
||||
return create_invite("roster", username.."@"..module.host, allow_registration, additional_data);
|
||||
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;
|
||||
|
||||
-- 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;
|
||||
}, valid_invite_mt);
|
||||
end
|
||||
|
||||
function use(token) --luacheck: ignore 131/use
|
||||
local invite = get(token);
|
||||
return invite and invite:use();
|
||||
end
|
|
@ -1,112 +0,0 @@
|
|||
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)
|
||||
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");
|
||||
|
||||
local command = table.remove(arg, 1);
|
||||
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 "<unknown>");
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,131 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
<style>
|
||||
#install-buttons-container {
|
||||
text-align: center;
|
||||
}
|
||||
#install-buttons-container img {
|
||||
height: 3.5em;
|
||||
margin: 0 auto 0.5em auto;
|
||||
}
|
||||
button {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
|
||||
<div class="card-body" >
|
||||
{inviter?<p>You have been invited to chat on {site_name} using Snikket,
|
||||
a secure, privacy-friendly chat app.</p>}
|
||||
|
||||
{inviter&<p>You have been invited to chat with {inviter} using Snikket,
|
||||
a secure, privacy-friendly chat app on {site_name}.</p>}
|
||||
|
||||
<h5 class="card-title">Get started</h5>
|
||||
|
||||
<p>Install the Snikket app on your Android device (iOS <a href="https://snikket.org/faq/#is-there-an-ios-app">coming soon!</a>)</p>
|
||||
|
||||
<div id="install-buttons-container" class="container">
|
||||
<a href='https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'>
|
||||
<img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png'/>
|
||||
</a>
|
||||
<a href="#qr-modal" class="d-none" id="qr-modal-show">
|
||||
<button class="btn btn-info" title="Send this invite to your device"
|
||||
data-toggle="modal" data-target="#qr-modal">Not on mobile?</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>After installation the app should automatically open and prompt you to
|
||||
create an account. If not, simply click the button below.</p>
|
||||
|
||||
<h6 class="text-center">App already installed?</h6>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{uri}" id="uri-cta"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
|
||||
<small class="text-muted">This button works only if you have the app installed already!</small>
|
||||
</div>
|
||||
<br/>
|
||||
<h5>Alternatives</h5>
|
||||
<p>You can connect to Snikket using any XMPP-compatible software. If the button above does not
|
||||
work with your app, you may need to <a href="register?{token}">register an account manually</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="qr-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Scan invite code</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You can transfer this invite to your mobile device by scanning a code with your camera. You can use
|
||||
either a QR scanner app or the Snikket app itself.</p>
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="qr-tab-scanner" data-toggle="tab" href="#qr-info-url" role="tab" aria-controls="qr-info-url" aria-selected="true">Using a QR code scanner</a>
|
||||
<a class="nav-item nav-link" id="qr-tab-app" data-toggle="tab" href="#qr-info-uri" role="tab" aria-controls="qr-info-uri" aria-selected="false">Using the Snikket app</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content">
|
||||
<div id="qr-info-url" class="tab-pane show active">
|
||||
<p>Use a <em>QR code</em> scanner on your mobile device to scan the code below:</p>
|
||||
<div id="qr-invite-page" class="w-50 p-1 mx-auto"></div>
|
||||
</div>
|
||||
<div id="qr-info-uri" class="tab-pane">
|
||||
<div>
|
||||
<img src="/img/snikket-scan-button-shdw.png" class="d-block w-25 p-1 float-right">
|
||||
<p>Install the Snikket app on your mobile device, open it, and
|
||||
tap the 'Scan' button at the top.</p>
|
||||
<p>Your camera will turn on. Point it at the square code below until it is
|
||||
within the highlighted square on your screen, and wait until the app
|
||||
recognises it.</p>
|
||||
</div>
|
||||
<div id="qr-uri" class="w-50 p-1 mx-auto clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
<script src="/js/qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$('#qr-modal').one('show.bs.modal', function (e) {
|
||||
new QRCode(document.getElementById("qr-uri"), document.getElementById("uri-cta").getAttribute("href"));
|
||||
new QRCode(document.getElementById("qr-invite-page"), document.location.href);
|
||||
});
|
||||
$('#qr-modal-show').addClass("d-md-block");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Invite expired</h5>
|
||||
|
||||
<p>Sorry, it looks like this invite code has expired!</p>
|
||||
|
||||
<img class="w-100" src="/img/illus-empty.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,55 +0,0 @@
|
|||
local st = require "util.stanza";
|
||||
local url_escape = require "util.http".urlencode;
|
||||
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
|
||||
urlescape = url_escape;
|
||||
});
|
||||
local render_url = require "util.interpolation".new("%b{}", url_escape, {
|
||||
urlescape = url_escape;
|
||||
noscheme = function (url)
|
||||
return (url:gsub("^[^:]+:", ""));
|
||||
end;
|
||||
});
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
if prosody.shutdown then
|
||||
module:depends("http");
|
||||
end
|
||||
local invites = module:depends("invites");
|
||||
|
||||
-- Point at eg https://github.com/ge0rg/easy-xmpp-invitation
|
||||
local base_url = module:get_option_string("invites_page", (module.http_url and module:http_url().."?{token}") or nil);
|
||||
|
||||
local function add_landing_url(invite)
|
||||
if not base_url then return; end
|
||||
invite.landing_page = render_url(base_url, invite);
|
||||
end
|
||||
|
||||
module:hook("invite-created", add_landing_url);
|
||||
|
||||
|
||||
function serve_invite_page(event)
|
||||
local invite_page_template = assert(module:load_resource("html/invite.html")):read("*a");
|
||||
local invalid_invite_page_template = assert(module:load_resource("html/invite_invalid.html")):read("*a");
|
||||
|
||||
local invite = invites.get(event.request.url.query);
|
||||
if not invite then
|
||||
return render_html_template(invalid_invite_page_template, { site_name = site_name });
|
||||
end
|
||||
|
||||
local invite_page = render_html_template(invite_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
});
|
||||
return invite_page;
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = serve_invite_page;
|
||||
};
|
||||
});
|
|
@ -1,87 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Secure communication on {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
|
||||
<div class="card-body" >
|
||||
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
|
||||
|
||||
<h5 class="card-title">Create an account</h5>
|
||||
|
||||
<p>Creating an account will allow to communicate with other people using
|
||||
the Snikket app or compatible software. If you already have the app installed,
|
||||
we recommend that you continue the account creation process inside the app
|
||||
by clicking on the button below:</p>
|
||||
|
||||
<h6 class="text-center">App already installed?</h6>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{uri}"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
|
||||
<small class="text-muted">This button works only if you have the app installed already!</small>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h5 class="card-title">Create an account online</h5>
|
||||
<p>If you plan to use a legacy XMPP client, you can register an account online and enter your
|
||||
credentials into any XMPP-compatible software.</p>
|
||||
|
||||
{message&<div class="alert {msg_class?alert-info}" role="alert">
|
||||
{message}
|
||||
</div>}
|
||||
|
||||
<form method="post">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Username:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text" name="user" class="form-control" aria-describedby="usernameHelp"
|
||||
required autofocus minlength="1" maxlength="30" length="30"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">@{domain}</span>
|
||||
</div>
|
||||
</div>
|
||||
<small id="usernameHelp" class="d-block form-text text-muted">Choose a username, this will become the first part of your new chat address.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<input type="password" name="password" class="form-control" aria-describedby="passwordHelp"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<small id="passwordHelp" class="form-text text-muted">Enter a secure password that you do not use anywhere else.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<input type="hidden" name="token" value="{token}">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Registration error</h5>
|
||||
|
||||
<p>{message?Sorry, there was a problem registering your account.}</p>
|
||||
|
||||
<img class="w-100" src="/img/illus-bug.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
|
||||
<script>
|
||||
function toggle_password(e) {
|
||||
var button = e.target;
|
||||
var input = button.parentNode.parentNode.querySelector("input");
|
||||
switch(input.attributes.type.value) {
|
||||
case "password":
|
||||
input.attributes.type.value = "text";
|
||||
button.innerText = "Hide";
|
||||
break;
|
||||
case "text":
|
||||
input.attributes.type.value = "password";
|
||||
button.innerText = "Show";
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
{site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Congratulations!</h5>
|
||||
|
||||
<p>You have created an account on {site_name}.</p>
|
||||
|
||||
<p>To start chatting, you need to enter your new account
|
||||
credentials into your chosen XMPP software.</p>
|
||||
|
||||
<p>As a final reminder, your account details are shown below:</p>
|
||||
|
||||
<form class="account-details col-12 col-lg-6 mx-auto">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Chat address (JID):</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<input type="text" class="form-control-plaintext" readonly value="{username}@{domain}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input type="password" readonly class="form-control" value="{password}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="toggle_password(event)">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>Your password is stored encrypted on the server and will not be accessible after you close this page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,158 +0,0 @@
|
|||
local id = require "util.id";
|
||||
local http_formdecode = require "net.http".formdecode;
|
||||
local usermanager = require "core.usermanager";
|
||||
local nodeprep = require "util.encodings".stringprep.nodeprep;
|
||||
local st = require "util.stanza";
|
||||
local url_escape = require "util.http".urlencode;
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
|
||||
urlescape = url_escape;
|
||||
});
|
||||
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
module:depends("http");
|
||||
module:depends("easy_invite");
|
||||
local invites = module:depends("invites");
|
||||
local invites_page = module:depends("invites_page");
|
||||
|
||||
function serve_register_page(event)
|
||||
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
|
||||
|
||||
local invite = invites.get(event.request.url.query);
|
||||
if not invite then
|
||||
return {
|
||||
status_code = 303;
|
||||
headers = {
|
||||
["Location"] = invites.module:http_url().."?"..event.request.url.query;
|
||||
};
|
||||
};
|
||||
end
|
||||
|
||||
local invite_page = render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
});
|
||||
return invite_page;
|
||||
end
|
||||
|
||||
function handle_register_form(event)
|
||||
local request, response = event.request, event.response;
|
||||
local form_data = http_formdecode(request.body);
|
||||
local user, password, token = form_data["user"], form_data["password"], form_data["token"];
|
||||
|
||||
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
|
||||
local error_template = assert(module:load_resource("html/register_error.html")):read("*a");
|
||||
local success_template = assert(module:load_resource("html/register_success.html")):read("*a");
|
||||
|
||||
local invite = invites.get(token);
|
||||
if not invite then
|
||||
return {
|
||||
status_code = 303;
|
||||
headers = {
|
||||
["Location"] = invites_page.module:http_url().."?"..event.request.url.query;
|
||||
};
|
||||
};
|
||||
end
|
||||
|
||||
response.headers.content_type = "text/html; charset=utf-8";
|
||||
|
||||
if not user or #user == 0 or not password or #password == 0 or not token then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "Please fill in all fields.";
|
||||
});
|
||||
end
|
||||
|
||||
-- Shamelessly copied from mod_register_web.
|
||||
local prepped_username = nodeprep(user);
|
||||
|
||||
if not prepped_username or #prepped_username == 0 then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "This username contains invalid characters.";
|
||||
});
|
||||
end
|
||||
|
||||
if usermanager.user_exists(prepped_username, module.host) then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "This username is already in use.";
|
||||
});
|
||||
end
|
||||
|
||||
local registering = {
|
||||
validated_invite = invite;
|
||||
username = prepped_username;
|
||||
host = module.host;
|
||||
allowed = true;
|
||||
};
|
||||
|
||||
module:fire_event("user-registering", registering);
|
||||
|
||||
if not registering.allowed then
|
||||
return render_html_template(error_template, {
|
||||
site_name = site_name;
|
||||
msg_class = "alert-danger";
|
||||
message = registering.reason or "Registration is not allowed.";
|
||||
});
|
||||
end
|
||||
|
||||
local ok, err = usermanager.create_user(prepped_username, password, module.host);
|
||||
|
||||
if ok then
|
||||
module:fire_event("user-registered", {
|
||||
username = prepped_username;
|
||||
host = module.host;
|
||||
source = "mod_"..module.name;
|
||||
validated_invite = invite;
|
||||
});
|
||||
|
||||
return render_html_template(success_template, {
|
||||
site_name = site_name;
|
||||
username = prepped_username;
|
||||
domain = module.host;
|
||||
password = password;
|
||||
});
|
||||
else
|
||||
local err_id = id.short();
|
||||
module:log("warn", "Registration failed (%s): %s", err_id, tostring(err));
|
||||
return render_html_template(error_template, {
|
||||
site_name = site_name;
|
||||
msg_class = "alert-danger";
|
||||
message = ("An unknown error has occurred (%s)"):format(err_id);
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = serve_register_page;
|
||||
["POST"] = handle_register_form;
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
TODO!
|
||||
|
||||
Hello from <strong>{site_name}!</strong>
|
||||
|
||||
You have been invited to join {site_name}. Simply click here to get started: <a href="{invite_page}">view invite</a>
|
||||
|
||||
If you already have a compatible app installed, you can click here instead: <a href="{invite_uri}">open in app</a>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
TODO!
|
||||
|
||||
Hello from {site_name}!
|
||||
|
||||
You have been invited to join {site_name}. Simply click here to get started: {invite_page}
|
||||
|
||||
If you already have a compatible app installed, you can click here instead: {invite_uri}
|
|
@ -1,63 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Secure communication on {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by"><a href="https://snikket.org/">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></a></div>
|
||||
<div class="card-body" >
|
||||
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
|
||||
|
||||
{allow_email&
|
||||
<h5 class="card-title">Request invitation</h5>
|
||||
|
||||
<p>You may join the Snikket network by creating an account
|
||||
on {site_name}. Registration is by invitation only, enter
|
||||
your email address to request an invitation.</p>
|
||||
|
||||
<form action="/_/invite-request" method="post">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Email:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email" name="email" class="form-control" aria-describedby="emailHelp"
|
||||
required autofocus minlength="3" length="30"
|
||||
>
|
||||
</div>
|
||||
<small id="emailHelp" class="d-block form-text text-muted">
|
||||
Enter the email address we should deliver your invitation to.
|
||||
<br/>
|
||||
Your email address will be kept private as per our <a href="privacy/">Privacy Policy</a>.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row float-right">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,94 +0,0 @@
|
|||
local http_formdecode = require "net.http".formdecode;
|
||||
local st = require "util.stanza";
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape);
|
||||
local render_text_template = require"util.interpolation".new("%b{}", function (s) return s; end);
|
||||
local mime = require "mime";
|
||||
local ltn12 = require "ltn12";
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
-- Email templates
|
||||
local email_template_preamble = "Problems viewing this email? View it online at {invite_page}";
|
||||
local email_template_text = assert(module:load_resource("email_templates/invite_email.txt")):read("*a");
|
||||
local email_template_html = assert(module:load_resource("email_templates/invite_email.html")):read("*a");
|
||||
|
||||
module:depends("http");
|
||||
module:depends("email");
|
||||
local invites = module:depends("invites");
|
||||
|
||||
local landing_page_template = assert(module:load_resource("html/index.html")):read("*a");
|
||||
|
||||
local landing_page = render_html_template(landing_page_template, {
|
||||
site_name = site_name;
|
||||
});
|
||||
|
||||
local function handle_form(event)
|
||||
local request, response = event.request, event.response;
|
||||
local form_data = http_formdecode(request.body);
|
||||
|
||||
local email = form_data["email"];
|
||||
|
||||
response.headers.content_type = "";
|
||||
|
||||
local invite = invites.create_account();
|
||||
|
||||
local email_template_params = {
|
||||
site_name = site_name;
|
||||
invite_token = invite.token;
|
||||
invite_uri = invite.uri;
|
||||
invite_page = invite.landing_page;
|
||||
};
|
||||
|
||||
local email_headers = {
|
||||
Subject = "Your Snikket invitation";
|
||||
["Content-Type"] = "multipart/mixed";
|
||||
};
|
||||
|
||||
local email_body = {
|
||||
-- Optional text content prefixed to the entire email (visible even in
|
||||
-- non-MIME clients)
|
||||
preamble = render_text_template(email_template_preamble, email_template_params);
|
||||
|
||||
-- Plain text version
|
||||
[1] = {
|
||||
headers = {
|
||||
["Content-Type"] = 'text/plain; charset="utf-8"';
|
||||
};
|
||||
body = mime.eol(0, render_text_template(email_template_text, email_template_params));
|
||||
};
|
||||
|
||||
-- HTML version
|
||||
[2] = {
|
||||
headers = {
|
||||
["content-type"] = 'text/html;charset="utf-8"',
|
||||
["content-transfer-encoding"] = "quoted-printable"
|
||||
},
|
||||
body = ltn12.source.chain(
|
||||
ltn12.source.string(render_html_template(email_template_html, email_template_params)),
|
||||
ltn12.filter.chain(
|
||||
mime.encode("quoted-printable", "text"),
|
||||
mime.wrap()
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module:send_email({ --luacheck: ignore 143/module
|
||||
to = email;
|
||||
headers = email_headers;
|
||||
body = email_body;
|
||||
});
|
||||
|
||||
return render_html_template(landing_page_template, {
|
||||
site_name = site_name;
|
||||
message = "Ok! Check your inbox :)";
|
||||
});
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET /"] = landing_page;
|
||||
["POST /invite-request"] = handle_form;
|
||||
};
|
||||
});
|
Loading…
Reference in New Issue