diff --git a/include/mod_invites.hrl b/include/mod_invites.hrl
index 104fd61ae94..8ea7a83f86c 100644
--- a/include/mod_invites.hrl
+++ b/include/mod_invites.hrl
@@ -15,7 +15,7 @@
invitee = <<>> :: binary(),
created_at = calendar:now_to_datetime(erlang:timestamp()) :: calendar:datetime(),
expires = calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(erlang:timestamp())) + ?INVITE_TOKEN_EXPIRE_SECONDS_DEFAULT) :: calendar:datetime(),
- type = roster_only :: roster_only | account_only | account_subscription,
+ type = roster_only :: roster_only | account_only | account_subscription | reset_token,
%% If type is 'roster_only' then we indicate a token has been used to create
%% an account (if allowed) by setting `account_name` to the name of the user
%% (which should match `invitee`).
diff --git a/priv/mod_invites/reset_error.html b/priv/mod_invites/reset_error.html
new file mode 100644
index 00000000000..a1137cf83bb
--- /dev/null
+++ b/priv/mod_invites/reset_error.html
@@ -0,0 +1,7 @@
+{% extends "base_min.html" %}
+{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %}
+{% block content %}
+
{% trans "Password reset error" %}
+
+ {% if message %}{{ message }}{% else %}{% trans "Sorry, there was a problem changing your account password." %}{% endif %}
+{% endblock%}
diff --git a/priv/mod_invites/reset_success.html b/priv/mod_invites/reset_success.html
new file mode 100644
index 00000000000..e9fce15073c
--- /dev/null
+++ b/priv/mod_invites/reset_success.html
@@ -0,0 +1,41 @@
+{% extends "base_min.html" %}
+{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %}
+{% block title %}{% trans "Password Change Success" %} | {{site_name}}{% endblock %}
+{% block h1 %}{{site_name}}{% endblock %}
+{% block content %}
+ {% trans "Congratulations!" %}
+
+ {% blocktrans %}You have changed your password on {{ site_name }}.{% endblocktrans %}
+
+ {% trans "To continue chatting, you need to enter your new account credentials into your chosen XMPP software." %}
+
+
+ {% trans "As a final reminder, your account details are shown below:" %}
+
+
+
+ {% if password %}
+ {% trans "Your password is stored encrypted on the server and will not be accessible after you close this page. Keep it safe and never share it with anyone." %}
+ {% endif %}
+{% endblock %}
diff --git a/priv/mod_invites/reset_token.html b/priv/mod_invites/reset_token.html
new file mode 100644
index 00000000000..db00c8f079b
--- /dev/null
+++ b/priv/mod_invites/reset_token.html
@@ -0,0 +1,48 @@
+{% extends "base_min.html" %}
+
+{% block title %}{% if error %}{% trans "Form Error" %}{% else %}{% trans "Reset Password" %}{% endif %} | {{ site_name }}{% endblock %}
+{% block h1 %}{% if error %}{% trans "Form Error" %}{% else %}{% blocktrans %}Reset Password on {{ site_name }}{% endblocktrans %}{% endif %}{% endblock %}
+
+{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %}
+
+{% block content %}
+ {% blocktrans %}Hey, {{ username }}, if you'd like to change your password, maybe because you forgot your old one, you've come to the right place!{% endblocktrans %}
+ {% blocktrans %}If this is not you, please disregard!{% endblocktrans %}
+ If you have a compatible XMPP/Jabber client installed on your system you can try using this button to reset your password instead. This way you don't need to copy your password. Otherwise proceed with the form below!
+
+
+ {% trans "Reset Password" %}
+ {%if error and error.class == 'undefined' %}{{ error.text }}
{% endif %}
+
+{% endblock %}
diff --git a/src/mod_invites.erl b/src/mod_invites.erl
index 51e72817c88..a0495c36097 100644
--- a/src/mod_invites.erl
+++ b/src/mod_invites.erl
@@ -44,7 +44,7 @@
%% commands
-export([cleanup_expired/0, delete_invite_by_token/2, expire_invites/2, expire_invite_by_token/2, generate_invite/1,
- generate_invite/2, list_invites/1]).
+ generate_invite/2, generate_reset_token/2, list_invites/1]).
%% helpers
-export([create_account_allowed/2, create_account_invite/4, get_invite/2, get_invites_tree_t/2,
@@ -65,7 +65,7 @@
-include("ejabberd_web_admin.hrl").
-ifdef(TEST).
--export([create_roster_invite/2, find_invites_tree_root_t/4, gen_invite/1,
+-export([create_roster_invite/2, create_reset_token/2, find_invites_tree_root_t/4, gen_invite/1,
gen_invite/2, get_invites/2, get_invites_tree_as_root_t/2, is_token_valid/3]).
-endif.
@@ -301,7 +301,7 @@ start(Host, Opts) ->
%% note the sequence below is important
{hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 10},
%% webadmin
- {hook, webadmin_menu_system_post, web_menu_system, 1000-$i, global},
+ {hook, webadmin_menu_system_post, web_menu_system, 1000 - $i, global},
{hook, webadmin_menu_main, webadmin_menu_main, 50, global},
{hook, webadmin_page_main, webadmin_page_main, 50, global},
{hook, webadmin_menu_host, webadmin_menu_host, 50},
@@ -392,6 +392,7 @@ webadmin_page_host(_,
BaseArguments,
[{force_execution, false}]),
make_command(expire_invite_tokens, Request, BaseArguments, []),
+ make_command(generate_reset_token, Request, BaseArguments, [{force_execution, false}]),
make_command(cleanup_expired_invite_tokens,
Request,
[],
@@ -497,6 +498,20 @@ get_commands_spec() ->
{<<"xmpp:juliet@example.com?register;preauth=4bsdpwVrRDQYnF9aQQKXGbF7">>,
<<"https://example.com/invites/4bsdpwVrRDQYnF9aQQKXGbF7">>},
result = {invite, {tuple, [{invite_uri, string}, {landing_page, string}]}}},
+ #ejabberd_commands{name = generate_reset_token,
+ tags = [accounts],
+ desc =
+ "Create a password reset token for user with given name on given host.",
+ module = ?MODULE,
+ function = generate_reset_token,
+ note = "added in 26.05",
+ args = [{username, binary}, {host, binary}],
+ args_desc = ["Username", "Hostname"],
+ args_example = [<<"juliet">>, <<"example.com">>],
+ result_example =
+ {<<"xmpp:juliet@example.com?register;preauth=4bsdpwVrRDQYnF9aQQKXGbF7">>,
+ <<"https://example.com/invites/4bsdpwVrRDQYnF9aQQKXGbF7">>},
+ result = {invite, {tuple, [{invite_uri, string}, {landing_page, string}]}}},
#ejabberd_commands{name = list_invites,
tags = [accounts],
desc = "List invite tokens",
@@ -549,11 +564,11 @@ expire_invites(User0, Server0) ->
expire_invite_by_token(Host, Token) ->
pretty_format_command_result(try_db_call(Host, expire_invite_by_token, [Host, Token])).
--spec generate_invite(binary()) -> binary() | {error, any()}.
+-spec generate_invite(binary()) -> {binary(), binary()} | {error, any()}.
generate_invite(Host) ->
generate_invite(<<>>, Host).
--spec generate_invite(binary(), binary()) -> binary() | {error, any()}.
+-spec generate_invite(binary(), binary()) -> {binary(), binary()} | {error, any()}.
generate_invite(AccountName, Host) ->
pretty_format_command_result(gen_invite(AccountName, Host)).
@@ -565,7 +580,7 @@ gen_invite(Host) ->
-endif.
--spec gen_invite(binary(), binary()) -> binary() | {error, any()}.
+-spec gen_invite(binary(), binary()) -> {binary(), binary()} | {error, any()}.
gen_invite(AccountName, Host0) ->
Host = jid:nameprep(Host0),
case create_account_invite(Host, {<<>>, Host}, AccountName, false) of
@@ -575,6 +590,16 @@ gen_invite(AccountName, Host0) ->
{token_uri(Invite), landing_page(Host, Invite)}
end.
+-spec generate_reset_token(binary(), binary()) -> {binary(), binary()} | {error, any()}.
+generate_reset_token(User, Host) ->
+ Res = case create_reset_token(User, Host) of
+ {error, _Reason} = Error ->
+ Error;
+ Invite ->
+ {token_uri(Invite), landing_page(Host, Invite)}
+ end,
+ pretty_format_command_result(Res).
+
list_invites(Host) ->
Res = maybe
{ok, Invites} ?= try_db_call(Host, list_invites, [Host]),
@@ -1128,11 +1153,37 @@ invite_token_t(Type, Host, Inviter, AccountName0) ->
account_name = AccountName},
mod_invites_opt:token_expire_seconds(Host)).
-token_uri(#invite_token{type = Type,
+-spec create_reset_token(binary(), binary()) -> invite_token() | {error, any()}.
+create_reset_token(User, Host) ->
+ maybe
+ (#invite_token{} = ResetToken) ?= reset_token(User, Host),
+ F = fun() -> db_call(Host, create_invite_t, [ResetToken]) end,
+ transaction(Host, F)
+ end.
+
+reset_token(User, Host) ->
+ maybe
+ true ?= lists:member(Host, ejabberd_option:hosts()) orelse {error, host_unknown},
+ true ?= ejabberd_auth:user_exists(User, Host) orelse {error, user_not_exists},
+ set_token_expires(#invite_token{token =
+ p1_rand:get_alphanum_string(?INVITE_TOKEN_LENGTH_DEFAULT),
+ inviter = {<<>>, Host},
+ type = reset_token,
+ account_name = User},
+ mod_invites_opt:token_expire_seconds(Host))
+ end.
+
+token_uri(#invite_token{type = roster_only,
token = Token,
+ inviter = {User, Host}}) ->
+ IBR = maybe_add_ibr_allowed(User, Host),
+ Inviter =
+ jid:encode(
+ jid:make(User, Host)),
+ <<"xmpp:", Inviter/binary, "?roster;preauth=", Token/binary, IBR/binary>>;
+token_uri(#invite_token{token = Token,
account_name = AccountName,
- inviter = {_User, Host}})
- when Type =:= account_only; Type =:= account_subscription ->
+ inviter = {_User, Host}}) ->
Invitee =
case AccountName of
<<>> ->
@@ -1140,15 +1191,7 @@ token_uri(#invite_token{type = Type,
_ ->
<>
end,
- <<"xmpp:", Invitee/binary, "?register;preauth=", Token/binary>>;
-token_uri(#invite_token{type = roster_only,
- token = Token,
- inviter = {User, Host}}) ->
- IBR = maybe_add_ibr_allowed(User, Host),
- Inviter =
- jid:encode(
- jid:make(User, Host)),
- <<"xmpp:", Inviter/binary, "?roster;preauth=", Token/binary, IBR/binary>>.
+ <<"xmpp:", Invitee/binary, "?register;preauth=", Token/binary>>.
maybe_add_ibr_allowed(User, Host) ->
case create_account_allowed(Host, jid:make(User, Host)) of
@@ -1312,6 +1355,8 @@ pretty_format_command_result({error, host_unknown}) ->
{error, "Virtual host not known"};
pretty_format_command_result({error, user_exists}) ->
{error, "Username already taken"};
+pretty_format_command_result({error, user_not_exists}) ->
+ {error, "User does not exist"};
pretty_format_command_result({ok, Result}) ->
Result;
pretty_format_command_result(Result) ->
diff --git a/src/mod_invites_http.erl b/src/mod_invites_http.erl
index de6f8826ab6..b3f4284e123 100644
--- a/src/mod_invites_http.erl
+++ b/src/mod_invites_http.erl
@@ -121,6 +121,8 @@ process([Token | _] = LocalPath,
try mod_invites:is_token_valid(Host, Token) of
true ->
case mod_invites:get_invite(Host, Token) of
+ #invite_token{type = reset_token} = Invite ->
+ process_reset_token(Request, Invite, LocalPath);
#invite_token{type = roster_only} = Invite ->
process_roster_token(LocalPath, Request, Invite);
Invite ->
@@ -155,7 +157,10 @@ process([] = LocalPath,
{ok, true, ok} ->
AccountName = proplists:get_value(<<"account_name">>, Q, <<>>),
Subscribe = proplists:get_value(<<"subscribe">>, Q, <<"no">>) == <<"yes">>,
- case mod_invites:create_account_invite(Host, {Username, Host}, AccountName, Subscribe)
+ case mod_invites:create_account_invite(Host,
+ {jid:nodeprep(Username), Host},
+ AccountName,
+ Subscribe)
of
#invite_token{} = Invite ->
Ctx = [{uri, mod_invites:token_uri(Invite)},
@@ -233,25 +238,34 @@ process_valid_token([_Token] = LocalPath,
process_valid_token(_, _, _) ->
?NOT_FOUND.
-process_register_form(Invite,
- AppID,
- #request{host = Host, lang = Lang} = Request,
- LocalPath) ->
+process_reset_token(#request{method = 'POST'} = Request, Invite, LocalPath) ->
+ process_post(reset_token, Invite, <<>>, Request, LocalPath);
+process_reset_token(Request, Invite, LocalPath) ->
+ process_form(reset_token, Invite, <<>>, Request, LocalPath).
+
+process_register_form(Invite, AppID, Request, LocalPath) ->
+ process_form(register, Invite, AppID, Request, LocalPath).
+
+process_form(Form,
+ Invite,
+ AppID,
+ #request{host = Host, lang = Lang} = Request,
+ LocalPath) ->
try app_ctx(Host, AppID, Lang, ctx(Invite, Request, LocalPath)) of
AppCtx ->
CSRFCookie = gen_rand_id(),
Ctx = [{csrf_token, csrf_token(CSRFCookie)} | maybe_add_username(AppCtx, Invite)],
- Body = render_register_form(Request, Ctx),
+ Body = render_form(Form, Request, Ctx),
Headers =
add_cookie_header(maybe_hsts_header(is_https_lp(Host, Invite)),
- csrf_cookie_string(<<"register-id">>, CSRFCookie)),
+ csrf_cookie_string(form_id(Form), CSRFCookie)),
?HTTP_OK(Headers, Body)
catch
_:not_found ->
?NOT_FOUND
end.
-render_register_form(#request{host = Host, lang = Lang}, Ctx) ->
+render_form(Form, #request{host = Host, lang = Lang}, Ctx) ->
MinLength =
case mod_register_opt:password_strength(Host) of
0 ->
@@ -259,23 +273,47 @@ render_register_form(#request{host = Host, lang = Lang}, Ctx) ->
_ ->
6
end,
- render(Host, Lang, <<"register.html">>, [{password_min_length, MinLength} | Ctx]).
-
-process_register_post(Invite,
- AppID,
- #request{host = Host,
- q = Q,
- lang = Lang,
- path = Path,
- ip = {Source, _},
- headers = Headers} =
- Request,
- LocalPath) ->
+ render(Host, Lang, form(Form), [{password_min_length, MinLength} | Ctx]).
+
+form(register) ->
+ <<"register.html">>;
+form(reset_token) ->
+ <<"reset_token.html">>.
+
+form_success(register) ->
+ <<"register_success.html">>;
+form_success(reset_token) ->
+ <<"reset_success.html">>.
+
+form_error(register) ->
+ <<"register_error.html">>;
+form_error(reset_token) ->
+ <<"reset_error.html">>.
+
+form_id(register) ->
+ <<"register-id">>;
+form_id(reset_token) ->
+ <<"reset-id">>.
+
+process_register_post(Invite, AppID, Request, LocalPath) ->
+ process_post(register, Invite, AppID, Request, LocalPath).
+
+process_post(Form,
+ Invite,
+ AppID,
+ #request{host = Host,
+ q = Q,
+ lang = Lang,
+ path = Path,
+ ip = {Source, _},
+ headers = Headers} =
+ Request,
+ LocalPath) ->
Username = proplists:get_value(<<"user">>, Q),
Password = proplists:get_value(<<"password">>, Q),
CSRFToken = proplists:get_value(<<"csrf_token">>, Q),
Token = Invite#invite_token.token,
- CSRFCookie = get_csrf_cookie(<<"register-id">>, Headers),
+ CSRFCookie = get_csrf_cookie(form_id(Form), Headers),
try {app_ctx(Host, AppID, Lang, ctx(Invite, Request, LocalPath)),
ensure_same(Token, proplists:get_value(<<"token">>, Q)),
check_csrf(CSRFCookie, CSRFToken)}
@@ -284,10 +322,11 @@ process_register_post(Invite,
case mod_invites_register:try_register(Invite, Username, Host, Password, Source, Lang)
of
{ok, _UpdatedInvite} ->
- Ctx = maybe_add_webchat_url(Host,
+ Ctx = maybe_add_webchat_url(Form,
+ Host,
[{username, Username}, {password, Password}
| AppCtx]),
- render_ok(Host, Invite, Lang, <<"register_success.html">>, Ctx);
+ render_ok(Host, Invite, Lang, form_success(Form), Ctx);
{error,
#stanza_error{text = Text,
type = Type,
@@ -301,12 +340,12 @@ process_register_post(Invite,
{csrf_token, CSRFToken},
{error, [{text, Msg}, {class, error_class(Reason)}]}]
++ AppCtx,
- Body = render_register_form(Request, Ctx),
+ Body = render_form(Form, Request, Ctx),
?BAD_REQUEST(Body);
_ ->
render_bad_request(Host,
is_https_lp(Host, Invite),
- <<"register_error.html">>,
+ form_error(Form),
[{message, Msg} | base_ctx(Host,
Lang,
Path,
@@ -346,7 +385,7 @@ csrf_token(Msg) when Msg /= <<>> ->
crypto:hash(sha256, SecretKey)),
Msg)).
-maybe_add_webchat_url(Host, Ctx) ->
+maybe_add_webchat_url(register, Host, Ctx) ->
case mod_invites_opt:webchat_url(Host) of
none ->
Ctx;
@@ -362,7 +401,9 @@ maybe_add_webchat_url(Host, Ctx) ->
end;
WebchatUrl ->
[{webchat_url, WebchatUrl} | Ctx]
- end.
+ end;
+maybe_add_webchat_url(_, _, Ctx) ->
+ Ctx.
process_roster_token([_Token] = LocalPath,
#request{host = Host, lang = Lang} = Request,
@@ -597,7 +638,7 @@ parse_cookie_header(Headers) ->
M
end,
#{},
- [binary:split(S, <<"=">>) || S <- binary:split(C, <<"; ">>)]).
+ [binary:split(S, <<"=">>) || S <- binary:split(C, <<"; ">>, [global])]).
error_class('jid-malformed') ->
username;
diff --git a/src/mod_invites_register.erl b/src/mod_invites_register.erl
index 42f1e978fb0..a52a78caf46 100644
--- a/src/mod_invites_register.erl
+++ b/src/mod_invites_register.erl
@@ -162,7 +162,7 @@ maybe_create_mutual_subscription(#invite_token{inviter = {User, Server},
process_token(#{server := Host} = State, Token, #iq{lang = Lang} = IQ) ->
?DEBUG("processing token (~s): ~s", [Host, Token]),
- case can_create(Host, Token) of
+ case can_create_account_or_change_pw(Host, Token) of
{true, Invite} ->
NewState = State#{invite => Invite},
{NewState, xmpp:make_iq_result(IQ)};
@@ -170,19 +170,19 @@ process_token(#{server := Host} = State, Token, #iq{lang = Lang} = IQ) ->
{State, preauth_invalid(IQ, Lang)}
end.
-can_create(Host, Token) ->
+can_create_account_or_change_pw(Host, Token) ->
try mod_invites:is_token_valid(Host, Token) of
true ->
case mod_invites:get_invite(Host, Token) of
+ #invite_token{type = reset_token} = Invite ->
+ {true, Invite};
#invite_token{type = roster_only, account_name = AccountName}
when AccountName /= <<>> ->
false;
Invite ->
- case create_account_allowed(Invite) of
- ok ->
- {true, Invite};
- {error, not_allowed} ->
- false
+ maybe
+ true ?= create_account_allowed(Invite),
+ {true, Invite}
end
end;
false ->
@@ -201,35 +201,54 @@ create_account_allowed(#invite_token{type = roster_only} = Invite) ->
fun() ->
mod_invites:get_invites_tree_t(Host, {User, Host})
end)),
- case NumInvites >= ?OVERUSE_LIMIT of
- false ->
- ok;
- true ->
- {error, not_allowed}
- end;
+ NumInvites < ?OVERUSE_LIMIT;
false ->
- {error, not_allowed}
+ false
end;
create_account_allowed(#invite_token{inviter = {<<>>, _Host}}) ->
- ok;
+ true;
create_account_allowed(#invite_token{inviter = {User, Host}}) ->
- mod_invites:create_account_allowed(Host, jid:make(User, Host)).
+ mod_invites:create_account_allowed(Host, jid:make(User, Host)) == ok.
preauth_invalid(IQ, Lang) ->
Text = ?T("The token provided is either invalid or expired."),
make_stripped_error(IQ, #preauth{}, xmpp:err_item_not_found(Text, Lang)).
+-spec try_register(mod_invites:invite_token(),
+ binary(),
+ binary(),
+ binary(),
+ tuple(),
+ binary()) ->
+ {ok, mod_invites:invite_token()} | {error, stanza_error()}.
+try_register(#invite_token{type = reset_token} = Invite,
+ User,
+ Server,
+ Password,
+ _Source,
+ Lang) ->
+ case Invite#invite_token.account_name == User of
+ true ->
+ ChPwF = fun() -> mod_register:try_set_password(User, Server, Password) end,
+ NewInvite =
+ #invite_token{invitee = Invitee} =
+ maybe_set_invitee(Invite, jid:make(User, Server)),
+ case mod_invites:set_invitee(ChPwF, Server, Invite#invite_token.token, Invitee, User) of
+ ok ->
+ {ok, NewInvite};
+ {error, Why} ->
+ {error, to_xmpp_error(Why, Lang)}
+ end;
+ false ->
+ {error, to_xmpp_error(not_allowed, Lang)}
+ end;
try_register(Invite, User, Server, Password, Source, Lang) ->
#invite_token{token = Token} = Invite,
case {jid:nodeprep(User), not mod_invites:is_reserved(Server, Token, User)} of
{error, _} ->
- {error,
- xmpp:err_jid_malformed(
- mod_register:format_error(invalid_jid), Lang)};
+ {error, to_xmpp_error(invalid_jid, Lang)};
{_, false} ->
- {error,
- xmpp:err_not_allowed(
- mod_register:format_error(not_allowed), Lang)};
+ {error, to_xmpp_error(not_allowed, Lang)};
{_, true} ->
RegF =
fun() ->
@@ -244,14 +263,31 @@ try_register(Invite, User, Server, Password, Source, Lang) ->
{ok, NewInvite};
{error, conflict} ->
?LOG_WARNING("Conflict when redeeming invite token: ~p", [NewInvite]),
- {error,
- xmpp:err_conflict(
- mod_register:format_error(not_allowed), Lang)};
- {error, _Reason} = Error ->
- Error
+ {error, to_xmpp_error(conflict, Lang)};
+ {error, Why} ->
+ {error, to_xmpp_error(Why, Lang)}
end
end.
+to_xmpp_error(Why, Lang) when Why == not_allowed; Why == invalid_password ->
+ xmpp:err_not_allowed(
+ mod_register:format_error(Why), Lang);
+to_xmpp_error(weak_password = Why, Lang) ->
+ xmpp:err_not_acceptable(
+ mod_register:format_error(Why), Lang);
+to_xmpp_error(invalid_jid = Why, Lang) ->
+ xmpp:err_jid_malformed(
+ mod_register:format_error(Why), Lang);
+to_xmpp_error(db_failure = Why, Lang) ->
+ xmpp:err_internal_server_error(
+ mod_register:format_error(Why), Lang);
+to_xmpp_error(conflict, Lang) ->
+ xmpp:err_conflict(
+ mod_register:format_error(not_allowed), Lang);
+to_xmpp_error(Unexpected, Lang) ->
+ xmpp:err_internal_server_error(
+ mod_register:format_error(Unexpected), Lang).
+
check_captcha(true, #register{xdata = X}, #iq{lang = Lang} = IQ) ->
XdataC =
xmpp_util:set_xdata_field(#xdata_field{var = <<"FORM_TYPE">>,
diff --git a/src/mod_invites_sql.erl b/src/mod_invites_sql.erl
index 900be6bf3f4..bd72367a6e1 100644
--- a/src/mod_invites_sql.erl
+++ b/src/mod_invites_sql.erl
@@ -268,11 +268,12 @@ is_token_valid(Host, Token, {User, Host}) ->
list_invites(Host) ->
{selected, Rows} =
ejabberd_sql:sql_query(Host,
- ?SQL("SELECT @(token)s, @(username)s, @(type)s, @(account_name)s, "
+ ?SQL("SELECT @(token)s, @(username)s, @(invitee)s, @(type)s, @(account_name)s, "
"@(expires)t, @(created_at)t FROM invite_token WHERE %(Host)H")),
- lists:map(fun({Token, User, Type, AccountName, Expires, CreatedAt}) ->
+ lists:map(fun({Token, User, Invitee, Type, AccountName, Expires, CreatedAt}) ->
#invite_token{token = Token,
inviter = {User, Host},
+ invitee = Invitee,
type = dec_type(Type),
account_name = AccountName,
expires = Expires,
@@ -314,11 +315,15 @@ enc_type(roster_only) ->
enc_type(account_subscription) ->
<<"S">>;
enc_type(account_only) ->
- <<"A">>.
+ <<"A">>;
+enc_type(reset_token) ->
+ <<"T">>.
dec_type(<<"R">>) ->
roster_only;
dec_type(<<"S">>) ->
account_subscription;
dec_type(<<"A">>) ->
- account_only.
+ account_only;
+dec_type(<<"T">>) ->
+ reset_token.
diff --git a/src/mod_register.erl b/src/mod_register.erl
index fb6e289ff57..90baba24358 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -33,7 +33,7 @@
-export([start/2, stop/1, reload/3, stream_feature_register/2,
c2s_unauthenticated_packet/2, try_register/4, try_register/5,
- try_register/6, process_iq/1, send_registration_notifications/3,
+ try_register/6, try_set_password/3, process_iq/1, send_registration_notifications/3,
mod_opt_type/1, mod_options/1, depends/2,
format_error/1, mod_doc/0]).
diff --git a/test/invites_tests.erl b/test/invites_tests.erl
index e035dec422a..0925d072a53 100644
--- a/test/invites_tests.erl
+++ b/test/invites_tests.erl
@@ -148,7 +148,8 @@ single_cases() ->
single_test(ibr_subscription),
single_test(ibr_conflict),
single_test(http),
- single_test(create_invite_page)]}.
+ single_test(create_invite_page),
+ single_test(reset_token)]}.
%%%===================================================================
@@ -412,10 +413,7 @@ max_invites(Config0) ->
Inviter = {User, Server},
OldOpts = gen_mod:get_module_opts(Server, mod_invites),
NewOpts =
- gen_mod_set_opts(OldOpts,
- [{max_invites, 3},
- {access_create_account, account_invite},
- {allow_modules, [mod_invites]}]),
+ gen_mod_set_opts(OldOpts, [{max_invites, 3}, {access_create_account, account_invite}]),
update_module_opts(Server, mod_invites, NewOpts),
Config = reconnect(Config0),
@@ -446,9 +444,7 @@ overuse(Config0) ->
Inviter = {User, Server},
InviteeJID = jid:make(User, Server),
OldOpts = gen_mod:get_module_opts(Server, mod_invites),
- NewOpts =
- gen_mod_set_opts(OldOpts,
- [{access_create_account, account_invite}, {allow_modules, [mod_invites]}]),
+ NewOpts = gen_mod_set_opts(OldOpts, [{access_create_account, account_invite}]),
update_module_opts(Server, mod_invites, NewOpts),
Config1 = reconnect(Config0),
@@ -973,6 +969,57 @@ create_invite_page(Config) ->
update_module_opts(Server, mod_invites, OldOpts),
disconnect(Config).
+reset_token(Config0) ->
+ Server = ?config(server, Config0),
+ User = ?config(user, Config0),
+ Password = ?config(password, Config0),
+
+ httpc:set_options([{cookies, enabled}]),
+
+ OldRegisterOpts = gen_mod:get_module_opts(Server, mod_register),
+ NewRegisterOpts = gen_mod:set_opt(allow_modules, [mod_invites], OldRegisterOpts),
+ update_module_opts(Server, mod_register, NewRegisterOpts),
+
+ Config1 = reconnect(Config0),
+
+ #invite_token{token = Token} = mod_invites:create_reset_token(User, Server),
+
+ BaseURL = mod_invites_http:landing_page(Server, mod_invites:get_invite(Server, Token)),
+ CSRFToken = get_csrf_token(BaseURL),
+
+ ?match(true, ejabberd_auth:check_password(User, <<"plain">>, Server, Password)),
+
+ ?match(#iq{type = error}, send_iq_register(Config1, User, <<"newPassword">>)),
+ ?match(#iq{type = result}, send_pars(Config1, Token)),
+ ?match(#iq{type = error}, send_iq_register(Config1, <<"wrong_user">>, <<"newPassword">>)),
+ ?match(#iq{type = result}, send_iq_register(Config1, User, <<"newPassword">>)),
+
+ ?match(true, ejabberd_auth:check_password(User, <<"plain">>, Server, <<"newPassword">>)),
+ ?match(false, ejabberd_auth:check_password(User, <<"plain">>, Server, Password)),
+
+ ?match(false, mod_invites:is_token_valid(Server, Token)),
+
+ {ok, {{_, 404, _}, _, _}} = post(BaseURL, Token, CSRFToken, User, <<"anotherPassword">>),
+
+ #invite_token{token = Token2} = mod_invites:create_reset_token(User, Server),
+ BaseURL2 = mod_invites_http:landing_page(Server, mod_invites:get_invite(Server, Token2)),
+ CSRFToken2 = get_csrf_token(BaseURL2),
+
+ {ok, {{_, 400, _}, _, _}} =
+ post(BaseURL2, Token2, CSRFToken, User, <<"anotherPassword">>),
+ {ok, {{_, 400, _}, _, _}} =
+ post(BaseURL2, Token2, CSRFToken2, <<"wronguser">>, <<"anotherPassword">>),
+ {ok, {{_, 200, _}, _, _}} =
+ post(BaseURL2, Token2, CSRFToken2, User, <<"anotherPassword">>),
+ ?match(true,
+ ejabberd_auth:check_password(User, <<"plain">>, Server, <<"anotherPassword">>)),
+
+ ok = mod_register:try_set_password(User, Server, Password),
+ update_module_opts(Server, mod_register, OldRegisterOpts),
+ mod_invites:expire_invites(<<>>, Server),
+ ?match(2, mod_invites:cleanup_expired()),
+ disconnect(Config1).
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
@@ -1076,12 +1123,15 @@ send_get_iq_register(Config) ->
to = ServerJID,
sub_els = [#register{}]}).
-send_iq_register(Config, AccountName) ->
+send_iq_register(Config, Username) ->
+ send_iq_register(Config, Username, <<"mySecret">>).
+
+send_iq_register(Config, Username, Password) ->
ServerJID = jid:from_string(?config(server, Config)),
send_recv(Config,
#iq{type = set,
to = ServerJID,
- sub_els = [#register{username = AccountName, password = <<"mySecret">>}]}).
+ sub_els = [#register{username = Username, password = Password}]}).
send_pars(Config, Token) ->
ServerJID = jid:from_string(?config(server, Config)),