Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/mod_invites.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
7 changes: 7 additions & 0 deletions priv/mod_invites/reset_error.html
Original file line number Diff line number Diff line change
@@ -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 %}
<h2 class="card-title h5">{% trans "Password reset error" %}</h2>

<p>{% if message %}{{ message }}{% else %}{% trans "Sorry, there was a problem changing your account password." %}{% endif %}</p>
{% endblock%}
41 changes: 41 additions & 0 deletions priv/mod_invites/reset_success.html
Original file line number Diff line number Diff line change
@@ -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 %}
<h2 class="card-title h5">{% trans "Congratulations!" %}</h2>

<p>{% blocktrans %}You have changed your password on <strong>{{ site_name }}</strong>.{% endblocktrans %}</p>

<p>{% trans "To continue chatting, you need to enter your new account credentials into your chosen XMPP software." %}</p>


<p>{% trans "As a final reminder, your account details are shown below:" %}</p>

<form class="account-details col-12 mx-auto">
<div class="input-group">
<label for="user" class="col-md-4 col-form-label fw-bold">{% trans "Chat address (JID)" %}:</label>
<div class="col-md-8 col-12">
<input type="text" class="form-control-plaintext" readonly value="{{ username }}@{{ domain }}">
</div>
</div>
{% if password %}
<div class="input-group">
<label for="password" class="col-md-4 col-12 col-form-label fw-bold">{% trans "Password" %}:</label>
<div class="col-md-8 col-12">
<div class="input-group">
<input type="password" readonly disabled aria-label="{% trans "Password" %}" class="form-control" value="{{ password }}">
<div class="input-group-append">
<button id="toggle-pw-button" class="btn btn-outline-secondary rounded-start-0" type="button"
data-text-show="{% trans "Show" %}" data-text-hide="{% trans "Hide" %}">{% trans "Show" %}</button>
</div>
</div>
</div>
</div>
{% endif %}
</form>

{% if password %}
<p class="mt-3">{% 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." %}</p>
{% endif %}
{% endblock %}
48 changes: 48 additions & 0 deletions priv/mod_invites/reset_token.html
Original file line number Diff line number Diff line change
@@ -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 %}
<p>{% blocktrans %}Hey, <strong class="text-alert">{{ username }}</strong>, if you'd like to change your password, maybe because you forgot your old one, you've come to the right place!{% endblocktrans %}</p>
<p class="alert alert-warning">{% blocktrans %}If this is not you, please disregard!{% endblocktrans %}</p>
<div class="alert alert-info">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!</div>
<div class="text-center">
<a href="{{ uri }}" type="button" class="btn btn-secondary">Change Password using your installed client</a></div>

<h3 class="card-title h4 my-3">{% trans "Reset Password" %}</h3>
{%if error and error.class == 'undefined' %}<div class="alert alert-danger" role="alert">{{ error.text }}</div>{% endif %}
<form method="post" class="needs-validation" novalidate>
<div class="row mb-3">
<label for="user" class="col-md-4 form-label fw-bold">{% trans "Username" %}:</label>
<div class="col-md-8">
<div class="input-group">
<input
type="text" name="user" id="user" class="form-control" readonly minlength="1" maxlength="30" length="30" value="{{ username }}">
<div class="input-group-append">
<span class="input-group-text">@{{ domain }}</span>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 form-label col-form-label fw-bold">{% trans "New Password" %}:</label>
<div class="col-md-8">
<input type="password" name="password" id="password" class="form-control {% if error.class == 'password' %}is-invalid{% endif %}" aria-describedby="passwordHelp" tabindex="1"
autocomplete="new-password" required autofocus minlength="{{ password_min_length }}">
<div class="invalid-feedback">
{% if error.class == 'password' %}{{ error.text }}{% else %}
{% blocktrans %}Please provide a password! Minimum length: {{ password_min_length }}{% endblocktrans %}{% endif %}
</div>
<small id="passwordHelp" class="form-text text-muted">{% trans "Enter a secure password that you do not use anywhere else." %}</small>
</div>
</div>
<div>
<input type="hidden" name="token" value="{{ token }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" tabindex="2" class="btn btn-primary float-end">{% trans "Submit" %}</button>
</div>
</form>
{% endblock %}
81 changes: 63 additions & 18 deletions src/mod_invites.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.

Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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,
[],
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)).

Expand All @@ -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
Expand All @@ -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]),
Expand Down Expand Up @@ -1128,27 +1153,45 @@ 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
<<>> ->
Host;
_ ->
<<AccountName/binary, "@", Host/binary>>
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
Expand Down Expand Up @@ -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) ->
Expand Down
Loading
Loading