Skip to content
Open
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 priv/mod_invites/base_min.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ <h1 class="card-header">{%block h1 %}{% blocktrans %}Invite to {{ site_name }}{%
{% block qr_code %}{% endblock %}
{% block extra_scripts %}{% endblock %}
<script src="{{ static }}/bootstrap/js/bootstrap.min.js" integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"></script>
<script src="{{ static }}/invite.js" integrity="sha384-9dl7uTP5+QJJfidqZFZB530NVQnp58oBvUdbj6mjFPKPNUWhl85g4kP9BStp0bpv"></script>
<script src="{{ static }}/invite.js" integrity="sha384-DELy2ti586LKOGn5HDTdjg73HEkjZwynBNP7gBW55xktoykPMw3EWbw0SWZLl9w1"></script>
</body>
</html>
113 changes: 113 additions & 0 deletions priv/mod_invites/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{% extends "base_min.html" %}

{% block title %}{% if error %}{% trans "Form Error" %}{% else %}{% trans "Startpage" %}{% endif %} | {{ site_name }}{% endblock %}
{% block h1 %}{% if error %}{% trans "Form Error" %}{% else %}{% blocktrans %}Welcome to {{ site_name }}{% endblocktrans %}{% endif %}{% endblock %}

{% 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 %}

{% block content %}
<h2 class="h3">Create Account Invite</h2>
{% if token %}
<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>
<div class="container-sm-fluid">
<div class="text-center">
<strong>{% trans "Your Invite-Link:" %}</strong> <a href="{{ landing_page }}" target="_blank">{{ token }}</a>
<br/>
<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 }}'>
<span>{% trans "Copy to clipboard" %}</span>
<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" %}'>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="currentColor" fill-rule="nonzero">
<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">
</path>
</g>
</g>
</svg>
</button>
</div>
<div class="bd-callout bd-callout-info">
<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>
{% trans "You can transfer this invite to a mobile device by scanning the QR code below with its camera." %}
</div>
<div id="qr-info-url" class="tab-pane show active">
<div id="qr-invite-page" class="bg-light mx-auto" data-link="{{ landing_page }}"></div>
</div>
</div>

<nav aria-label="{% trans 'Page navigation' %}">
<ul class="pagination mt-3 mb-0">
<li class="page-item"><a tabindex="4" class="page-link" href="/{{ base }}/" aria-label="{% trans 'Previous' %}">
<span aria-hidden="true">&laquo;</span>
<span>{% trans "Previous" %}</span>
</a></li>
</ul>
</nav>
{% else %}
<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>
<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 {% 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 %}>
<div class="input-group-append">
<span class="input-group-text">@{{ domain }}</span>
</div>
<div class="invalid-feedback">
{% if error.class == 'username' %}{{ error.text }}{% else %}
{% blocktrans %}Please provide a valid username!{% endblocktrans %}{% endif %}
</div>
</div>
<small id="usernameHelp" class="d-block form-text text-muted">{% trans "Enter the username you use to log into this service." %}</small>
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 form-label col-form-label fw-bold">{% trans "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="2" required>
<div class="invalid-feedback">
{% if error.class == 'password' %}{{ error.text }}{% else %}
{% blocktrans %}Please provide a password!{% endblocktrans %}{% endif %}
</div>
<small id="passwordHelp" class="form-text text-muted">{% trans "Enter the corresponding password." %}</small>
</div>
</div>
<div class="border border-secondary-light rounded my-3">
<div class="text-bg-light rounded-top p-2 mb-3 h6">{% trans "Invite Options" %}</div>
<div class="p-2">
<div class="row mb-3">
<label for="account_name" class="col-xl-4 form-label fw-bold">{% trans "Suggested Name" %}:</label>
<div class="col-12 col-xl-8">
<div class="input-group">
<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">
<div class="input-group-append">
<span class="input-group-text">@{{ domain }}</span>
</div>
<div class="invalid-feedback">
{% if error.class == 'account_name' %}{{ error.text }}{% endif %}
</div>
<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>
</div>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="subscribe" name="subscribe" value="yes" tabindex="4">
<label class="form-check-label" for="subscribe">
{% trans "Add to contact list" %}
</label>
<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>
</div>
</div>
</div>
<div>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" tabindex="5" class="btn btn-primary float-end">{% trans "Submit" %}</button>
</div>
</form>
{% endif %}
{% endblock %}

{% block extra_scripts %}
<script src="{{ static }}/qrcode.min.js" integrity="sha384-XfbBihCQqSDyejklP5yun2CbVxqR+2eNfx0Fhx5pQAfN5ypWGhSBjXaXr5g6X4DE"></script>
{% endblock %}
16 changes: 8 additions & 8 deletions priv/mod_invites/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
<h2 class="card-title h5">{% trans "Create an account" %}</h2>
{%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="input-group mb-3">
<label for="user" class="col-md-4 col-lg-12 form-label fw-bold">{% trans "Username" %}:</label>
<div class="col-md-8 col-lg-12">
<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" class="form-control {% if error.class == 'username' %}is-invalid{% endif %}" aria-describedby="usernameHelp" tabindex="1"
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 %}>
<div class="input-group-append">
<span class="input-group-text">@{{ domain }}</span>
Expand All @@ -42,10 +42,10 @@ <h2 class="card-title h5">{% trans "Create an account" %}</h2>
<small id="usernameHelp" class="d-block form-text text-muted">{% trans "Choose a username, this will become the first part of your new chat address." %}</small>
</div>
</div>
<div class="input-group mb-3">
<label for="password" class="col-md-4 col-lg-12 form-label col-form-label fw-bold">{% trans "Password" %}:</label>
<div class="col-md-8 col-lg-12">
<input type="password" name="password" class="form-control {% if error.class == 'password' %}is-invalid{% endif %}" aria-describedby="passwordHelp" tabindex="2"
<div class="row mb-3">
<label for="password" class="col-md-4 form-label col-form-label fw-bold">{% trans "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="2"
autocomplete="new-password" required minlength="{{ password_min_length }}">
<div class="invalid-feedback">
{% if error.class == 'password' %}{{ error.text }}{% else %}
Expand Down
42 changes: 36 additions & 6 deletions priv/mod_invites/static/invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));

// If QR lib loaded ok, show QR button on desktop devices
if (window.QRCode) {
const qr_el = document.getElementById("qr-invite-page");
if (window.QRCode && qr_el) {
let link = document.location.href;
const data_link = qr_el.getAttribute('data-link');
if (data_link && data_link != '')
link = data_link;
const qrcode_opts = {
text: document.location.href,
text: link,
addQuietZone: true
};
new QRCode(document.getElementById("qr-invite-page"), qrcode_opts);
document.getElementById('qr-button-container').classList.add("d-md-block");
new QRCode(qr_el, qrcode_opts);
const qr_button = document.getElementById('qr-button-container');
if (qr_button)
qr_button.classList.add("d-md-block");
}

const toggle_pw_button = document.getElementById('toggle-pw-button');
Expand Down Expand Up @@ -129,9 +136,9 @@ function toggle_password(e) {
'use strict';
window.addEventListener('load', function () {
// Fetch all the forms we want to apply custom Bootstrap validation styles to
var forms = document.getElementsByClassName('needs-validation');
const forms = document.getElementsByClassName('needs-validation');
// Loop over them and prevent submission
var validation = Array.prototype.filter.call(forms, function (form) {
const validation = Array.prototype.filter.call(forms, function (form) {
form.addEventListener('submit', function (event) {
if (form.checkValidity() === false) {
event.preventDefault();
Expand All @@ -140,5 +147,28 @@ function toggle_password(e) {
form.classList.add('was-validated');
}, false);
});

const clipboard_btns = document.getElementsByClassName('clipboard');

Array.prototype.forEach.call(clipboard_btns, function(btn) {
btn.addEventListener('click', (e) => {
const span = btn.children[0];
const oldVal = span.innerText;
Promise.resolve().then(() => {
return navigator.clipboard.writeText(btn.getAttribute('data-copy'));
}).then(
() => {
span.innerText = btn.getAttribute('data-text-copied');
},
() => {
span.innerText = btn.getAttribute('data-text-copy-failed');
}
).finally(() => {
window.setTimeout(() => {
span.innerText = oldVal;
}, 1000);
});
});
});
}, false);
})();
19 changes: 14 additions & 5 deletions src/mod_invites.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@
-export([cleanup_expired/0, expire_tokens/2, generate_invite/1, generate_invite/2, list_invites/1]).

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

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

-ifdef(TEST).
-export([create_roster_invite/2, create_account_invite/4, find_invites_tree_root_t/4, gen_invite/1,
-export([create_roster_invite/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 @@ -132,7 +133,15 @@ mod_doc() ->
?T("In order to use the included landing page feature, you have to"
" set `landing_page` to either `auto` or an URL template like "
"`https://{{ host }}/invites/{{ invite.token }}` "
" if your server setup includes a so called reverse proxy."),
" if your server setup includes a so called reverse proxy. "
"Furthermore you need to connect this module as a handler of an `ejabberd_http` "
"listener as shown at the example below."),
"",
?T("For convenience there's now also a start page included at wherever you 'mount' "
"`mod_invites` at the `ejabberd_http` handler. This start page will allow people to "
"generate invites by giving their username and password. As such the URL should be "
"protected by HTTPS. The main use-case is when people have only clients, that don't "
"support generating invites natively."),
"",
?T("If you'd rather want to use an external service, set `landing_page` "
"to something like "
Expand Down
Loading
Loading