Skip to content
Draft
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
29 changes: 29 additions & 0 deletions include/mod_invites.hrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-define(DEFAULT_MAX_INVITES, infinity).
-define(DEFAULT_TOKEN_EXPIRE_SECONDS, 5*86400).
-define(DEFAULT_TOKEN_LENGTH, 24).

-define(NS_FEATURE_IBR_TOKEN, <<"urn:xmpp:ibr-token:0">>).
-define(NS_INVITE_CREATE_ACCOUNT, <<"urn:xmpp:invite#create-account">>).
-define(NS_INVITE_INVITE, <<"urn:xmpp:invite#invite">>).
-define(NS_PARS, <<"urn:xmpp:pars:0">>).

-define(OVERUSE_LIMIT, 1000).

-define(SPEEDY_GOAT_LEVELS, 2).
-define(SPEEDY_GOAT_SECONDS, 300).

-record(invite_token, {token :: binary(),
inviter :: {binary(), binary()},
%% A non-empty value if `invitee` indicates the invite has been used.
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())) + ?DEFAULT_TOKEN_EXPIRE_SECONDS) :: calendar:datetime(),
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`).
account_name = <<>> :: binary()
}).

%%% FIXME - just being lazy here, replacing ejabberd's lazy T with BIN
-define(BIN(S), <<S>>).
4 changes: 4 additions & 0 deletions priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type AdminQuery{
broadcast: BroadcastAdminQuery
"Blocklist management"
blocklist: BlocklistAdminQuery
"Invites management"
invites: InvitesAdminQuery
}

"""
Expand Down Expand Up @@ -92,6 +94,8 @@ type AdminMutation @protected{
broadcast: BroadcastAdminMutation
"Blocklist management"
blocklist: BlocklistAdminMutation
"Invites management"
invites: InvitesAdminMutation
}

type AdminSubscription {
Expand Down
36 changes: 36 additions & 0 deletions priv/graphql/schemas/admin/invites.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Allow admin to manage invites.
"""
type InvitesAdminMutation @use(modules: ["mod_invites"]) @protected{
"Generate Account Creation Invite for given XMPP hostname"
generateInvite(host: DomainName!, username: String): Invite
@protected(type: DOMAIN, args: ["host"]) @use(args: ["host"])

"Cleanup all expired tokens accross all hosts. Be careful since this will affect traceability and reset all acccounts max invites settings"
cleanupExpired: Int
@protected(type: DOMAIN, args: []) @use(args: [])

"Delete an invite using the associated token."
deleteInviteByToken(host: DomainName!, token: String!): String
@protected(type: DOMAIN, args: ["host", "token"]) @use(args: ["host", "token"])

"Expire all tokens for a given user, this way they can't be used anymore."
expireInvites(host: DomainName!, username: String!): Int
@protected(type: DOMAIN, args: ["host", "username"]) @use(args: ["host", "username"])

"Expire a specific invite by associated token."
expireInviteByToken(host: DomainName!, token: String!): String
@protected(type: DOMAIN, args: ["host", "token"]) @use(args: ["host", "token"])

"Generate a reset token for a useraccount, it can be used to reset a lost password."
generateResetToken(host: DomainName!, username: String!): Invite
@protected(type: Domain, args: ["host", "username"]) @use(args: ["host", "username"])
}

"""
Allow admin to retrieve information about invites.
"""
type InvitesAdminQuery @protected @use(modules: ["mod_invites"]){
listInvites(host: DomainName!): [Invite!]
@protected(type: DOMAIN, args: ["host"]) @use(args: ["host"])
}
12 changes: 12 additions & 0 deletions priv/graphql/schemas/global/invites.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type Invite{
account_name: String
created_at: String
expires: String
invitee: JID
inviter: JID!
landing_page: String
token: String!
token_uri: String
type: String
valid: Boolean
}
14 changes: 14 additions & 0 deletions priv/migrations/mysql_6.6.0_6.7.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE invites (
token text NOT NULL,
username text NOT NULL,
host varchar(250) NOT NULL,
invitee varchar(191) NOT NULL DEFAULT '',
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
type character(1) NOT NULL,
account_name text NOT NULL,
PRIMARY KEY (token(191))
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE INDEX i_invite_token_username USING BTREE ON invites(username(191), server_host(191));
CREATE INDEX i_invite_token_invitee USING BTREE ON invites(invitee(191));
13 changes: 13 additions & 0 deletions priv/migrations/pgsql_6.6.0_6.7.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE invites (
token text NOT NULL,
username text NOT NULL,
host text NOT NULL,
invitee text NOT NULL DEFAULT '',
created_at timestamp NOT NULL DEFAULT now(),
expires timestamp NOT NULL DEFAULT now(),
"type" character(1) NOT NULL,
account_name text NOT NULL,
PRIMARY KEY (token)
);
CREATE INDEX i_invite_token_username_server_host ON invites USING btree (username, server_host);
CREATE INDEX i_invite_token_invitee ON invites USING btree (invitee);
15 changes: 15 additions & 0 deletions priv/mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -673,3 +673,18 @@ CREATE TABLE blocklist (
reason TEXT,
PRIMARY KEY (luser, lserver)
);

CREATE TABLE invites (
token text NOT NULL,
username text NOT NULL,
host varchar(191) NOT NULL,
invitee varchar(191) NOT NULL DEFAULT '',
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
type character(1) NOT NULL,
account_name text NOT NULL,
PRIMARY KEY (token(191))
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE INDEX i_invite_token_username USING BTREE ON invites(username(191), server_host(191));
CREATE INDEX i_invite_token_invitee USING BTREE ON invites(invitee(191));
14 changes: 14 additions & 0 deletions priv/pg.sql
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,17 @@ CREATE TABLE blocklist (
reason TEXT,
PRIMARY KEY (luser, lserver)
);

CREATE TABLE invites (
token text NOT NULL,
username text NOT NULL,
host text NOT NULL,
invitee text NOT NULL DEFAULT '',
created_at timestamp NOT NULL DEFAULT now(),
expires timestamp NOT NULL DEFAULT now(),
"type" character(1) NOT NULL,
account_name text NOT NULL,
PRIMARY KEY (token)
);
CREATE INDEX i_invite_token_username_server_host ON invites USING btree (username, server_host);
CREATE INDEX i_invite_token_invitee ON invites USING btree (invitee);
1 change: 1 addition & 0 deletions src/config/mongoose_config_spec.erl
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ configurable_modules() ->
mod_global_distrib,
mod_http_upload,
mod_inbox,
mod_invites,
mod_keystore,
mod_last,
mod_mam,
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/admin/mongoose_graphql_admin_mutation.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ execute(_Ctx, _Obj, <<"externalServices">>, _Args) ->
{ok, externalServices};
execute(_Ctx, _Obj, <<"inbox">>, _Args) ->
{ok, inbox};
execute(_Ctx, _Obj, <<"invites">>, _Args) ->
{ok, invites};
execute(_Ctx, _Obj, <<"last">>, _Args) ->
{ok, last};
execute(_Ctx, _Obj, <<"muc">>, _Args) ->
Expand Down
4 changes: 3 additions & 1 deletion src/graphql/admin/mongoose_graphql_admin_query.erl
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ execute(_Ctx, _Obj, <<"stanza">>, _Args) ->
execute(_Ctx, _Obj, <<"stat">>, _Args) ->
{ok, stats};
execute(_Ctx, _Obj, <<"vcard">>, _Args) ->
{ok, vcard}.
{ok, vcard};
execute(_Ctx, _Obj, <<"invites">>, _Args) ->
{ok, invites}.
57 changes: 57 additions & 0 deletions src/graphql/admin/mongoose_graphql_invites_admin_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-module(mongoose_graphql_invites_admin_mutation).

-behaviour(mongoose_graphql).

-export([execute/4]).

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").

-import(mongoose_graphql_helper, [make_error/2, format_result/2]).

execute(_Ctx, _Obj, <<"generateInvite">>, Args) ->
generate_invite(Args);
execute(_Ctx, _Obj, <<"cleanupExpired">>, Args) ->
cleanup_expired(Args);
execute(_Ctx, _Obj, <<"deleteInviteByToken">>, Args) ->
delete_invite_by_token(Args);
execute(_Ctx, _Obj, <<"expireInvites">>, Args) ->
expire_invites(Args);
execute(_Ctx, _Obj, <<"expireInviteByToken">>, Args) ->
expire_invite_by_token(Args);
execute(_Ctx, _Obj, <<"generateResetToken">>, Args) ->
generate_reset_token(Args).

cleanup_expired(_) ->
{ok, mod_invites:cleanup_expired()}.

delete_invite_by_token(#{<<"host">> := Host, <<"token">> := Token}) ->
handle_cmd_result(mod_invites:delete_invite_by_token(Host, Token), Host).

expire_invites(#{<<"host">> := Host, <<"username">> := Username}) ->
handle_cmd_result(mod_invites:expire_invites(Host, Username), Host).

expire_invite_by_token(#{<<"host">> := Host, <<"token">> := Token}) ->
handle_cmd_result(mod_invites:expire_invite_by_token(Host, Token), Host).

-spec generate_invite(map()) -> {ok, map()} | {error, resolver_error()}.
generate_invite(#{<<"host">> := Host, <<"username">> := Username0}) ->
Username = null_to_bin(Username0),
case mod_invites:generate_invite(Host, Username) of
{error, _} = Error ->
make_error(Error, #{host => Host});
Invite ->
{ok, mod_invites:format_invite(Host, Invite)}
end.

generate_reset_token(#{<<"host">> := Host, <<"username">> := Username}) ->
handle_cmd_result(mod_invites:generate_reset_token(Host, Username), Host).

null_to_bin(null) -> <<>>;
null_to_bin(Bin) when is_binary(Bin) -> Bin.

handle_cmd_result({error, _} = Error, Host) ->
make_error(Error, #{host => Host});
handle_cmd_result(Result, _) ->
{ok, Result}.
25 changes: 25 additions & 0 deletions src/graphql/admin/mongoose_graphql_invites_admin_query.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-module(mongoose_graphql_invites_admin_query).

-export([execute/4]).

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").
-include("mod_invites.hrl").

-import(mongoose_graphql_helper, [make_error/2, format_result/2]).

execute(_Ctx, _Obj, <<"listInvites">>, Args) ->
list_invites(Args).

-spec list_invites(map()) -> {ok, [map()]} | {error, resolver_error()}.
list_invites(#{<<"host">> := Host}) ->
case mod_invites:pretty_format_command_result(mod_invites:list_invites(Host)) of
{error, _} = Error ->
make_error(Error, #{host => Host});
Invites ->
{ok, [{ok, mod_invites:format_invite(Host, Invite)} || Invite <- sort(Invites)]}
end.

sort(Invites) ->
lists:sort(fun(#invite_token{created_at = A}, #invite_token{created_at = B}) -> A < B end, Invites).
2 changes: 2 additions & 0 deletions src/graphql/mongoose_graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ admin_mapping_rules() ->
'Domain' => mongoose_graphql_domain,
'DomainWithType' => mongoose_graphql_domain,
'MetricAdminQuery' => mongoose_graphql_metric_admin_query,
'InvitesAdminMutation' => mongoose_graphql_invites_admin_mutation,
'InvitesAdminQuery' => mongoose_graphql_invites_admin_query,
default => mongoose_graphql_default},
interfaces => #{default => mongoose_graphql_default},
scalars => #{default => mongoose_graphql_scalar},
Expand Down
Loading