Mercurial > prosody-modules
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 |
| parents | |
| children | 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); |
