Comparison

mod_password_reset/mod_password_reset.lua @ 3344:0ce475235ae1

mod_password_reset: New module for self-service password resets via a web page
author Matthew Wild <mwild1@gmail.com>
date Tue, 02 Oct 2018 16:09:17 +0100
child 3352:f7668aee968a
comparison
equal deleted inserted replaced
3343:2e65160187a4 3344:0ce475235ae1
1 local adhoc_new = module:require "adhoc".new;
2 local adhoc_simple_form = require "util.adhoc".new_simple_form;
3 local new_token = require "util.id".long;
4 local jid_prepped_split = require "util.jid".prepped_split;
5 local http_formdecode = require "net.http".formdecode;
6 local usermanager = require "core.usermanager";
7 local dataforms_new = require "util.dataforms".new;
8 local tohtml = require "util.stanza".xml_escape
9 local tostring = tostring;
10
11 local reset_tokens = module:open_store();
12
13 local max_token_age = module:get_option_number("password_reset_validity", 86400);
14
15 local serve = module:depends"http_files".serve;
16
17 module:depends"adhoc";
18 module:depends"http";
19
20 local function apply_template(template, args)
21 return
22 template:gsub("{{([^}]*)}}", function (k)
23 if args[k] then
24 return tohtml(args[k])
25 else
26 return k
27 end
28 end)
29 end
30
31 function generate_page(event)
32 local request, response = event.request, event.response;
33
34 local token = request.url.query;
35 local reset_info = token and reset_tokens:get(token);
36
37 response.headers.content_type = "text/html; charset=utf-8";
38
39 if not reset_info or os.difftime(os.time(), reset_info.generated_at) > max_token_age then
40 module:log("warn", "Expired token: %s", token or "<none>");
41 local template = assert(module:load_resource("password_reset/password_result.html")):read("*a");
42
43 return apply_template(template, { classes = "alert-danger", message = "This link has expired." })
44 end
45
46 local template = assert(module:load_resource("password_reset/password_reset.html")):read("*a");
47
48 return apply_template(template, { jid = reset_info.user.."@"..module.host, token = token });
49 end
50
51 function handle_form(event)
52 local request, response = event.request, event.response;
53 local form_data = http_formdecode(request.body);
54 local password, token = form_data["password"], form_data["token"];
55
56 local reset_info = reset_tokens:get(token);
57
58 local template = assert(module:load_resource("password_reset/password_result.html")):read("*a");
59
60 response.headers.content_type = "text/html; charset=utf-8";
61
62 if not reset_info or os.difftime(os.time(), reset_info.generated_at) > max_token_age then
63 return apply_template(template, { classes = "alert-danger", message = "This link has expired." })
64 end
65
66 local ok, err = usermanager.set_password(reset_info.user, password, module.host);
67
68 if ok then
69 reset_tokens:set(token, nil);
70
71 return apply_template(template, { classes = "alert-success",
72 message = "Your password has been updated! Happy chatting :)" })
73 else
74 module:log("debug", "Resetting password failed: " .. tostring(err));
75
76 return apply_template(template, { classes = "alert-danger", message = "An unknown error has occurred." })
77 end
78 end
79
80 module:provides("http", {
81 route = {
82 ["GET /bootstrap.min.css"] = serve(module:get_directory() .. "/password_reset/bootstrap.min.css");
83 ["GET /reset"] = generate_page;
84 ["POST /reset"] = handle_form;
85 };
86 });
87
88 -- Changing a user's password
89 local reset_password_layout = dataforms_new{
90 title = "Generate password reset link";
91 instructions = "Please enter the details of the user who needs a reset link.";
92
93 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/adhoc/mod_password_reset" };
94 { name = "accountjid", type = "jid-single", required = true, label = "JID" };
95 };
96
97 local reset_command_handler = adhoc_simple_form(reset_password_layout, function (data, errors)
98 if errors then
99 local errmsg = {};
100 for name, text in pairs(errors) do
101 errmsg[#errmsg + 1] = name .. ": " .. text;
102 end
103 return { status = "completed", error = { message = table.concat(errmsg, "\n") } };
104 end
105
106 local jid = data.accountjid;
107 local user, host = jid_prepped_split(jid);
108
109 if host ~= module.host then
110 return {
111 status = "completed";
112 error = { message = "You may only generate password reset links for users on "..module.host.."." };
113 };
114 end
115
116 local token = new_token();
117 reset_tokens:set(token, {
118 generated_at = os.time();
119 user = user;
120 });
121
122 return { info = module:http_url() .. "/reset?" .. token, status = "completed" };
123 end);
124
125 local adhoc_reset = adhoc_new(
126 "Generate password reset link",
127 "password_reset",
128 reset_command_handler,
129 "admin"
130 );
131
132 module:add_item("adhoc", adhoc_reset);