Skip to content

Commit e7ea66a

Browse files
committed
mod_invites: add page to create invites
This adds a startpage that allows you to use your account credentials to generate an account creation invite. This is meant for folks that don't have access to a client, that supports that feature.
1 parent 1424c21 commit e7ea66a

5 files changed

Lines changed: 295 additions & 67 deletions

File tree

priv/mod_invites/base_min.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ <h1 class="card-header">{%block h1 %}{% blocktrans %}Invite to {{ site_name }}{%
2424
{% block qr_code %}{% endblock %}
2525
{% block extra_scripts %}{% endblock %}
2626
<script src="{{ static }}/bootstrap/js/bootstrap.min.js" integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"></script>
27-
<script src="{{ static }}/invite.js" integrity="sha384-9dl7uTP5+QJJfidqZFZB530NVQnp58oBvUdbj6mjFPKPNUWhl85g4kP9BStp0bpv"></script>
27+
<script src="{{ static }}/invite.js" integrity="sha384-DELy2ti586LKOGn5HDTdjg73HEkjZwynBNP7gBW55xktoykPMw3EWbw0SWZLl9w1"></script>
2828
</body>
2929
</html>

priv/mod_invites/index.html

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{% extends "base_min.html" %}
2+
3+
{% block title %}{% if error %}{% trans "Form Error" %}{% else %}{% trans "Startpage" %}{% endif %} | {{ site_name }}{% endblock %}
4+
{% block h1 %}{% if error %}{% trans "Form Error" %}{% else %}{% blocktrans %}Welcome to {{ site_name }}{% endblocktrans %}{% endif %}{% endblock %}
5+
6+
{% block form_class %}container col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2 col-lg-6 col-lg-offset-3 my-3 mt-md-5{% endblock %}
7+
8+
{% block content %}
9+
<h2 class="h3">Create Account Invite</h2>
10+
{% if token %}
11+
<div class="alert alert-success">Congratulations, a new invite has been created! Forward this link to someone you want to invite to this service.</div>
12+
<div class="container-sm-fluid">
13+
<div class="text-center">
14+
<strong>{% trans "Your Invite-Link:" %}</strong> <a href="{{ landing_page }}" target="_blank">{{ token }}</a>
15+
<br/>
16+
<button type="button" class="clipboard btn btn-sm btn-outline-secondary mt-1" data-text-copied='{% trans "Copied!" %}' data-text-copy-failed='{% trans "Failed to copy data" %}' data-copy='{{ landing_page }}'>
17+
<span>{% trans "Copy to clipboard" %}</span>
18+
<svg class="svg-inline--fa fa-w-16" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" title='{% trans "Copy to clipboard" %}'>
19+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
20+
<g fill="currentColor" fill-rule="nonzero">
21+
<path d="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z">
22+
</path>
23+
</g>
24+
</g>
25+
</svg>
26+
</button>
27+
</div>
28+
<div class="bd-callout bd-callout-info">
29+
<svg class="svg-inline--fa fa-lightbulb fa-w-11 text-warning" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="lightbulb" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512" data-fa-i2svg=""><path fill="currentColor" d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z"></path></svg>
30+
{% trans "You can transfer this invite to a mobile device by scanning the QR code below with its camera." %}
31+
</div>
32+
<div id="qr-info-url" class="tab-pane show active">
33+
<div id="qr-invite-page" class="bg-light mx-auto" data-link="{{ landing_page }}"></div>
34+
</div>
35+
</div>
36+
37+
<nav aria-label="{% trans 'Page navigation' %}">
38+
<ul class="pagination mt-3 mb-0">
39+
<li class="page-item"><a tabindex="4" class="page-link" href="/{{ base }}/" aria-label="{% trans 'Previous' %}">
40+
<span aria-hidden="true">&laquo;</span>
41+
<span>{% trans "Previous" %}</span>
42+
</a></li>
43+
</ul>
44+
</nav>
45+
{% else %}
46+
<p>{% blocktrans %}This form allows you to create an invite that you can send to someone else, so they can create a new account at this service. Be aware that you need to have the <strong>necessary rights</strong> to do so, otherwise this command will fail.{% endblocktrans %}</p>
47+
<form method="post" class="needs-validation" novalidate>
48+
<div class="row mb-3">
49+
<label for="user" class="col-md-4 form-label fw-bold">{% trans "Username" %}:</label>
50+
<div class="col-md-8">
51+
<div class="input-group">
52+
<input
53+
type="text" name="user" id="user" class="form-control {% if error.class == 'username' %}is-invalid{% endif %}" aria-describedby="usernameHelp" tabindex="1" required autofocus minlength="1" maxlength="30" length="30"{% if username %} value="{{ username }}"{% endif %}>
54+
<div class="input-group-append">
55+
<span class="input-group-text">@{{ domain }}</span>
56+
</div>
57+
<div class="invalid-feedback">
58+
{% if error.class == 'username' %}{{ error.text }}{% else %}
59+
{% blocktrans %}Please provide a valid username!{% endblocktrans %}{% endif %}
60+
</div>
61+
</div>
62+
<small id="usernameHelp" class="d-block form-text text-muted">{% trans "Enter the username you use to log into this service." %}</small>
63+
</div>
64+
</div>
65+
<div class="row mb-3">
66+
<label for="password" class="col-md-4 form-label col-form-label fw-bold">{% trans "Password" %}:</label>
67+
<div class="col-md-8">
68+
<input type="password" name="password" id="password" class="form-control {% if error.class == 'password' %}is-invalid{% endif %}" aria-describedby="passwordHelp" tabindex="2" required>
69+
<div class="invalid-feedback">
70+
{% if error.class == 'password' %}{{ error.text }}{% else %}
71+
{% blocktrans %}Please provide a password!{% endblocktrans %}{% endif %}
72+
</div>
73+
<small id="passwordHelp" class="form-text text-muted">{% trans "Enter the corresponding password." %}</small>
74+
</div>
75+
</div>
76+
<div class="border border-secondary-light rounded my-3">
77+
<div class="text-bg-light rounded-top p-2 mb-3 h6">{% trans "Invite Options" %}</div>
78+
<div class="p-2">
79+
<div class="row mb-3">
80+
<label for="account_name" class="col-xl-4 form-label fw-bold">{% trans "Suggested Name" %}:</label>
81+
<div class="col-12 col-xl-8">
82+
<div class="input-group">
83+
<input type="text" name="account_name" id="account_name" class="form-control {% if error.class == 'account_name' %}is-invalid{% endif %}" aria-describedby="accountnameHelp" tabindex="3" minlength="1" maxlength="30" length="30">
84+
<div class="input-group-append">
85+
<span class="input-group-text">@{{ domain }}</span>
86+
</div>
87+
<div class="invalid-feedback">
88+
{% if error.class == 'account_name' %}{{ error.text }}{% endif %}
89+
</div>
90+
<small id="accountnameHelp" class="d-block form-text text-muted">{% trans "You can suggest a username for the account to be created with this invite." %}</small>
91+
</div>
92+
</div>
93+
</div>
94+
<div class="form-check">
95+
<input class="form-check-input" type="checkbox" id="subscribe" name="subscribe" value="yes" tabindex="4">
96+
<label class="form-check-label" for="subscribe">
97+
{% trans "Add to contact list" %}
98+
</label>
99+
<small id="subscribeHelp" class="d-block form-text text-muted">{% trans "If checked once an account is created using this invite, it will automatically be added to your contact list." %}</small>
100+
</div>
101+
</div>
102+
</div>
103+
<div>
104+
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
105+
<button type="submit" tabindex="5" class="btn btn-primary float-end">{% trans "Submit" %}</button>
106+
</div>
107+
</form>
108+
{% endif %}
109+
{% endblock %}
110+
111+
{% block extra_scripts %}
112+
<script src="{{ static }}/qrcode.min.js" integrity="sha384-XfbBihCQqSDyejklP5yun2CbVxqR+2eNfx0Fhx5pQAfN5ypWGhSBjXaXr5g6X4DE"></script>
113+
{% endblock %}

priv/mod_invites/static/invite.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
33

44
// If QR lib loaded ok, show QR button on desktop devices
5-
if (window.QRCode) {
5+
const qr_el = document.getElementById("qr-invite-page");
6+
if (window.QRCode && qr_el) {
7+
let link = document.location.href;
8+
const data_link = qr_el.getAttribute('data-link');
9+
if (data_link && data_link != '')
10+
link = data_link;
611
const qrcode_opts = {
7-
text: document.location.href,
12+
text: link,
813
addQuietZone: true
914
};
10-
new QRCode(document.getElementById("qr-invite-page"), qrcode_opts);
11-
document.getElementById('qr-button-container').classList.add("d-md-block");
15+
new QRCode(qr_el, qrcode_opts);
16+
const qr_button = document.getElementById('qr-button-container');
17+
if (qr_button)
18+
qr_button.classList.add("d-md-block");
1219
}
1320

1421
const toggle_pw_button = document.getElementById('toggle-pw-button');
@@ -129,9 +136,9 @@ function toggle_password(e) {
129136
'use strict';
130137
window.addEventListener('load', function () {
131138
// Fetch all the forms we want to apply custom Bootstrap validation styles to
132-
var forms = document.getElementsByClassName('needs-validation');
139+
const forms = document.getElementsByClassName('needs-validation');
133140
// Loop over them and prevent submission
134-
var validation = Array.prototype.filter.call(forms, function (form) {
141+
const validation = Array.prototype.filter.call(forms, function (form) {
135142
form.addEventListener('submit', function (event) {
136143
if (form.checkValidity() === false) {
137144
event.preventDefault();
@@ -140,5 +147,28 @@ function toggle_password(e) {
140147
form.classList.add('was-validated');
141148
}, false);
142149
});
150+
151+
const clipboard_btns = document.getElementsByClassName('clipboard');
152+
153+
Array.prototype.forEach.call(clipboard_btns, function(btn) {
154+
btn.addEventListener('click', (e) => {
155+
const span = btn.children[0];
156+
const oldVal = span.innerText;
157+
Promise.resolve().then(() => {
158+
return navigator.clipboard.writeText(btn.getAttribute('data-copy'));
159+
}).then(
160+
() => {
161+
span.innerText = btn.getAttribute('data-text-copied');
162+
},
163+
() => {
164+
span.innerText = btn.getAttribute('data-text-copy-failed');
165+
}
166+
).finally(() => {
167+
window.setTimeout(() => {
168+
span.innerText = oldVal;
169+
}, 1000);
170+
});
171+
});
172+
});
143173
}, false);
144174
})();

src/mod_invites.erl

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,16 @@
4646
-export([cleanup_expired/0, expire_tokens/2, generate_invite/1, generate_invite/2, list_invites/1]).
4747

4848
%% helpers
49-
-export([create_account_allowed/2, get_invite/2, get_invites_tree_t/2, get_max_invites/2,
50-
is_create_allowed/2, is_expired/1, is_reserved/3, is_token_valid/2, roster_add/2,
51-
send_presence/3, set_invitee/3, set_invitee/5, token_uri/1, transaction/2, xdata_field/3]).
49+
-export([create_account_allowed/2, create_account_invite/4, get_invite/2, get_invites_tree_t/2,
50+
get_max_invites/2, is_create_allowed/2, is_expired/1, is_reserved/3, is_token_valid/2,
51+
roster_add/2, send_presence/3, set_invitee/3, set_invitee/5, token_uri/1, transaction/2,
52+
xdata_field/3]).
5253

5354
%% ejabberd_http
5455
-export([process/2]).
5556

5657
-ifdef(TEST).
57-
-export([create_roster_invite/2, create_account_invite/4, find_invites_tree_root_t/4, gen_invite/1,
58+
-export([create_roster_invite/2, find_invites_tree_root_t/4, gen_invite/1,
5859
gen_invite/2, get_invites/2, get_invites_tree_as_root_t/2, is_token_valid/3]).
5960
-endif.
6061

@@ -132,7 +133,15 @@ mod_doc() ->
132133
?T("In order to use the included landing page feature, you have to"
133134
" set `landing_page` to either `auto` or an URL template like "
134135
"`https://{{ host }}/invites/{{ invite.token }}` "
135-
" if your server setup includes a so called reverse proxy."),
136+
" if your server setup includes a so called reverse proxy. "
137+
"Furthermore you need to connect this module as a handler of an `ejabberd_http` "
138+
"listener as shown at the example below."),
139+
"",
140+
?T("For convenience there's now also a start page included at wherever you 'mount' "
141+
"`mod_invites` at the `ejabberd_http` handler. This start page will allow people to "
142+
"generate invites by giving their username and password. As such the URL should be "
143+
"protected by HTTPS. The main use-case is when people have only clients, that don't "
144+
"support generating invites natively."),
136145
"",
137146
?T("If you'd rather want to use an external service, set `landing_page` "
138147
"to something like "

0 commit comments

Comments
 (0)