Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Wild
6407eb90db Explicitly set cookie SameSite attribute to Lax
With 'Secure' set, it may default to 'None', which we don't need or want.

'Strict' is not suitable for session cookies - the user would see the login
screen when navigating from another site (e.g. hosting dashboard) and we
already have CSRF protection on forms.
2024-04-29 11:18:55 +01:00
14 changed files with 88 additions and 199 deletions

View File

@@ -213,6 +213,7 @@ def create_app() -> quart.Quart:
app.config["ABUSE_EMAIL"] = config.abuse_email
app.config["SECURITY_EMAIL"] = config.security_email
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.context_processor(proc)
app.register_error_handler(

View File

@@ -301,10 +301,6 @@ class InvitePost(BaseForm):
default="prosody:registered",
)
note = wtforms.StringField(
_l("Comment (optional)"),
)
action_create_invite = wtforms.SubmitField(
_l("New invitation link")
)
@@ -386,14 +382,12 @@ async def create_invite() -> typing.Union[str, werkzeug.Response]:
group_ids=form.circles.data,
role_names=[form.role.data],
ttl=form.lifetime.data,
note=form.note.data,
)
else:
invite = await client.create_account_invite(
group_ids=form.circles.data,
role_names=[form.role.data],
ttl=form.lifetime.data,
note=form.note.data,
)
await flash(
_("Invitation created"),

View File

@@ -162,7 +162,6 @@ class AdminInviteInfo:
group_ids: typing.Collection[str]
role_names: typing.Collection[str]
is_reset: bool
note: typing.Optional[str]
@classmethod
def from_api_response(
@@ -182,7 +181,6 @@ class AdminInviteInfo:
role_names=data.get("roles", []),
reusable=data["reusable"],
is_reset=data.get("reset", False),
note=data.get("note"),
)
@@ -1093,7 +1091,6 @@ class ProsodyClient:
role_names: typing.Collection[str] = [],
restrict_username: typing.Optional[str] = None,
ttl: typing.Optional[int] = None,
note: typing.Optional[str] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {}
@@ -1103,8 +1100,6 @@ class ProsodyClient:
payload["username"] = restrict_username
if ttl is not None:
payload["ttl"] = ttl
if note is not None:
payload["note"] = note
async with session.post(
self._admin_v1_endpoint("/invites/account"),
@@ -1119,7 +1114,6 @@ class ProsodyClient:
group_ids: typing.Collection[str] = [],
role_names: typing.Collection[str] = [],
ttl: typing.Optional[int] = None,
note: typing.Optional[str] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {
@@ -1128,8 +1122,6 @@ class ProsodyClient:
}
if ttl is not None:
payload["ttl"] = ttl
if note is not None:
payload["note"] = note
async with session.post(
self._admin_v1_endpoint("/invites/group"),

View File

@@ -992,18 +992,19 @@ div.profile-card {
}
}
/* clipboard and share buttons */
/* clipboard button */
.copy-to-clipboard, .share-button {
.copy-to-clipboard {
cursor: pointer;
font-style: normal;
text-decoration: none;
}
body.no-copy .copy-to-clipboard, body.no-share .share-button {
body.no-copy .copy-to-clipboard {
display: none !important;
}
/* magic */
pre.guru-meditation {

View File

@@ -193,9 +193,4 @@ licensed under the terms of the Apache 2.0 License -->
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
</symbol>
<!-- from: social/share/materialiconsround/24px.svg -->
<symbol id="icon-share" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z" />
</symbol>
</defs></svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -66,12 +66,6 @@
{%- call render_errors(invite_form.circles) -%}{%- endcall -%}
</div>
<!-- Comment -->
<div class="f-ebox">
{{ invite_form.note.label }}
{{ invite_form.note }}
</div>
<div class="f-bbox">
{%- call form_button("create_link", invite_form.action_create_invite, class="primary") %}{% endcall -%}
</div>

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_name, invite_type_description %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -13,10 +13,9 @@
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ invite.expires | format_date }}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% trans %}Invitation to Snikket{% endtrans %}{% endcall %}</dd>
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% endcall %}</dd>
<dt>{% trans %}Invitation type{% endtrans %}</dt>
{% set invite_type = invite.reusable and "group" or "account" %}
<dd><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></dd>
<dd>{% call invite_type_description(invite) %}{% endcall %}</dd>
{%- set ngroups = invite.group_ids | length -%}
{%- if ngroups > 1 -%}
{#- not supported via the web UI, but we should still display it properly -#}

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, icon, clipboard_button, share_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
{% from "library.j2" import action_button, icon, clipboard_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -18,18 +18,17 @@
<col/>
<thead>
<tr>
<th>{% trans %}Expires{% endtrans %}</th>
<th class="collapsible">{% trans %}Type{% endtrans %}</th>
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
<th>{% trans %}Expires{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for invite in invites %}
{% set invite_type = invite.reusable and "group" or "account" %}
<tr>
<td class="collapsible"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></td>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
<td class="collapsible"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite) %}{% endcall %}">{% call invite_type_name(invite) %}{% endcall %}</span></td>
<td class="collapsible">
{#- -#}
<ul class="inline">
@@ -39,8 +38,6 @@
</ul>
{#- -#}
</td>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
<td>{% if invite.note is not none %}{{ invite.note }}{% endif %}</td>
<td class="nowrap">
{%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%}
{% trans %}Show invite details{% endtrans %}
@@ -48,9 +45,6 @@
{%- call clipboard_button(invite.landing_page, class="primary") -%}
{% trans %}Copy invite link to clipboard{% endtrans %}
{%- endcall -%}
{%- call share_button("Invitation to Snikket", invite.landing_page, class="primary") -%}
{% trans %}Share invitation link{% endtrans %}
{%- endcall -%}
{%- call custom_form_button("remove_link", form.action_revoke.name, invite.id_, class="secondary danger", slim=True) -%}
{% trans %}Delete invitation{% endtrans %}
{%- endcall -%}

View File

@@ -15,7 +15,7 @@
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ reset_link.expires | format_date }}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}Reset your Snikket password{% endcall %}</dd>
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}{% endcall %}</dd>
</dd>
<div class="f-bbox">
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}

View File

@@ -16,5 +16,5 @@
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %} no-copy no-share"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %}"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
</html>

View File

@@ -115,63 +115,8 @@ var copy_to_clipboard_btn = function(el) {
});
};
var copy_to_clipboard_btn = function(el) {
var text = el.dataset.cliptext;
if (!text) {
console.error('copy_to_clipboard used on element without text to copy');
}
copyTextToClipboard(text, el, function(success) {
var existing_result_el = document.getElementById("clipboard-result");
if (existing_result_el !== null) {
existing_result_el.parentNode.removeChild(existing_result_el);
}
var icon = "done";
if (!success) {
icon = "cancel";
}
var icon_bak = get_current_icon(el.firstChild);
change_icon(el.firstChild, icon);
setTimeout(function() {
change_icon(el.firstChild, icon_bak);
el.blur();
}, 1500);
});
};
var share_url_btn = function(el) {
let data = {
"title": el.dataset.shareTitle,
"url": el.dataset.shareUrl,
}
let icon_bak = get_current_icon(el.firstChild);
new Promise(function (resolve, reject) {
if(!navigator.canShare || !navigator.canShare(data)) {
return reject();
}
return resolve(navigator.share(data));
}).then(function () {
// Success
change_icon(el.firstChild, "done");
}, function () {
// Failure
change_icon(el.firstChild, "cancel");
}).finally(function () {
// Either way, clear status icon after 1.5s
setTimeout(function() {
change_icon(el.firstChild, icon_bak);
el.blur();
}, 1500);
});
}
window.addEventListener('load', function() {
document.body.classList.remove("no-copy");
if(navigator.share) {
document.body.classList.remove("no-share");
}
});
</script>

View File

@@ -38,10 +38,7 @@
<em>—</em>
{%- else -%}
<div><input type="text" {% if id_ %}id="{{ id_ }}" {% endif %}readonly="readonly" value="{{ uri }}"></div>
<div>
{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}
{% call share_button(caller() if caller is not none else None, uri, show_label=True) %}{% trans %}Share{% endtrans %}{% endcall %}
</div>
<div>{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}</div>
{%- endif -%}
{% endmacro %}
@@ -85,7 +82,7 @@
{% macro clipboard_button(data, show_label=False, caller=None, class=None) -%}
{%- set label = caller() -%}
<a class="button copy-to-clipboard{% if class %} {{ class }}{% endif %}"
<a class="button{% if class %} {{ class }}{% endif %}"
href="#"
{% if not show_label %}
aria-label="{{ label }}"
@@ -100,24 +97,6 @@
</a>
{%- endmacro %}
{% macro share_button(title, url, show_label=False, caller=None, class=None) -%}
{%- set label = caller() -%}
<a class="button share-button{% if class %} {{ class }}{% endif %}"
href="#"
{% if not show_label %}
aria-label="{{ label }}"
title="{{ label }}"
{% endif %}
data-share-title="{{ title }}"
data-share-url="{{ url }}"
onclick="share_url_btn(this); return false;">
{%- call icon("share") %}{% endcall -%}
{%- if show_label %}
<span>{{ label }}</span>
{% endif -%}
</a>
{%- endmacro %}
{% macro render_errors(field, caller=None) -%}
{%- set error_list = field.errors if field.errors is not mapping else (field.errors.values() | flatten | list) -%}
{%- if error_list -%}
@@ -153,11 +132,19 @@
{%- endif -%}
{% endmacro %}
{%- macro invite_type_name(invite_type, caller=None) -%}
{%- if invite_type == "account" -%}
{% trans %}Individual{% endtrans %}
{%- else -%}
{%- macro invite_type_name(invite_info, caller=None) -%}
{%- if invite_info.reusable -%}
{% trans %}Group{% endtrans %}
{%- else -%}
{% trans %}Individual{% endtrans %}
{%- endif -%}
{%- endmacro -%}
{%- macro invite_type_description(invite_info, caller=None) -%}
{%- if invite_info.reusable -%}
{% trans %}Can be used multiple times to create accounts on this Snikket service.{% endtrans %}
{%- else -%}
{% trans %}Can be used once to create an account on this Snikket service.{% endtrans %}
{%- endif -%}
{%- endmacro -%}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-04-30 10:52+0100\n"
"POT-Creation-Date: 2024-04-27 14:22+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -136,121 +136,117 @@ msgstr ""
msgid "Invitation type"
msgstr ""
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:158
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:139
msgid "Individual"
msgstr ""
#: snikket_web/admin.py:289 snikket_web/templates/library.j2:160
#: snikket_web/admin.py:289 snikket_web/templates/library.j2:137
msgid "Group"
msgstr ""
#: snikket_web/admin.py:305
msgid "Comment (optional)"
msgstr ""
#: snikket_web/admin.py:309
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:371
#: snikket_web/admin.py:367
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:399
#: snikket_web/admin.py:393
msgid "Invitation created"
msgstr ""
#: snikket_web/admin.py:415
#: snikket_web/admin.py:409
msgid "No such invitation exists"
msgstr ""
#: snikket_web/admin.py:430
#: snikket_web/admin.py:424
msgid "Invitation revoked"
msgstr ""
#: snikket_web/admin.py:447 snikket_web/admin.py:495
#: snikket_web/admin.py:441 snikket_web/admin.py:489
#: snikket_web/templates/admin_delete_circle.html:10
#: snikket_web/templates/admin_edit_circle.html:44
msgid "Name"
msgstr ""
#: snikket_web/admin.py:452 snikket_web/templates/admin_circles.html:47
#: snikket_web/admin.py:446 snikket_web/templates/admin_circles.html:47
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:482
#: snikket_web/admin.py:476
msgid "Circle created"
msgstr ""
#: snikket_web/admin.py:500
#: snikket_web/admin.py:494
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:505
#: snikket_web/admin.py:499
msgid "Update circle"
msgstr ""
#: snikket_web/admin.py:511
#: snikket_web/admin.py:505
msgid "Add user"
msgstr ""
#: snikket_web/admin.py:529 snikket_web/admin.py:628 snikket_web/admin.py:676
#: snikket_web/admin.py:523 snikket_web/admin.py:622 snikket_web/admin.py:670
msgid "No such circle exists"
msgstr ""
#: snikket_web/admin.py:566
#: snikket_web/admin.py:560
msgid "Circle data updated"
msgstr ""
#: snikket_web/admin.py:576
#: snikket_web/admin.py:570
msgid "User added to circle"
msgstr ""
#: snikket_web/admin.py:585
#: snikket_web/admin.py:579
msgid "User removed from circle"
msgstr ""
#: snikket_web/admin.py:594
#: snikket_web/admin.py:588
msgid "Chat removed from circle"
msgstr ""
#: snikket_web/admin.py:612
#: snikket_web/admin.py:606
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:639
#: snikket_web/admin.py:633
msgid "Circle deleted"
msgstr ""
#: snikket_web/admin.py:653
#: snikket_web/admin.py:647
msgid "Group chat name"
msgstr ""
#: snikket_web/admin.py:658
#: snikket_web/admin.py:652
msgid "Create group chat"
msgstr ""
#: snikket_web/admin.py:688
#: snikket_web/admin.py:682
msgid "New group chat added to circle"
msgstr ""
#: snikket_web/admin.py:755
#: snikket_web/admin.py:749
msgid "Message contents"
msgstr ""
#: snikket_web/admin.py:761
#: snikket_web/admin.py:755
msgid "Only send to online users"
msgstr ""
#: snikket_web/admin.py:765
#: snikket_web/admin.py:759
msgid "Post to all users"
msgstr ""
#: snikket_web/admin.py:769
#: snikket_web/admin.py:763
msgid "Send preview to yourself"
msgstr ""
#: snikket_web/admin.py:791
#: snikket_web/admin.py:785
msgid "Announcement sent!"
msgstr ""
@@ -550,7 +546,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_edit_circle.html:45
#: snikket_web/templates/admin_edit_circle.html:74
#: snikket_web/templates/admin_invites.html:25
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:10
msgid "Actions"
msgstr ""
@@ -778,7 +774,7 @@ msgid "The user has been deleted from the server."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:84
#: snikket_web/templates/library.j2:152
#: snikket_web/templates/library.j2:131
msgid "deleted"
msgstr ""
@@ -821,42 +817,38 @@ msgstr ""
msgid "Link"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:16
msgid "Invitation to Snikket"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:23
#: snikket_web/templates/admin_edit_invite.html:22
#: snikket_web/templates/admin_home.html:19
msgid "Circles"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:24
#: snikket_web/templates/admin_edit_invite.html:23
msgid "Users joining via this invitation will be added to the following circles:"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:30
#: snikket_web/templates/admin_invites.html:22
#: snikket_web/templates/admin_edit_invite.html:29
#: snikket_web/templates/admin_invites.html:23
msgid "Circle"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:36
#: snikket_web/templates/admin_edit_invite.html:35
msgid "The user will not be added to any circle and will have no contacts."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:41
#: snikket_web/templates/admin_edit_invite.html:40
msgid "Contact"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:42
#: snikket_web/templates/admin_edit_invite.html:41
#, python-format
msgid "The user will get added as contact of %(peer_jid)s."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:44
#: snikket_web/templates/admin_edit_invite.html:43
msgid "Created"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:49
#: snikket_web/templates/admin_edit_invite.html:48
msgid "Return to invitation list"
msgstr ""
@@ -1016,34 +1008,26 @@ msgid "Pending invitations"
msgstr ""
#: snikket_web/templates/admin_invites.html:21
msgid "Type"
msgstr ""
#: snikket_web/templates/admin_invites.html:23
msgid "Expires"
msgstr ""
#: snikket_web/templates/admin_invites.html:24
msgid "Comment"
#: snikket_web/templates/admin_invites.html:22
msgid "Type"
msgstr ""
#: snikket_web/templates/admin_invites.html:46
#: snikket_web/templates/admin_invites.html:43
msgid "Show invite details"
msgstr ""
#: snikket_web/templates/admin_invites.html:49
#: snikket_web/templates/admin_invites.html:46
msgid "Copy invite link to clipboard"
msgstr ""
#: snikket_web/templates/admin_invites.html:52
msgid "Share invitation link"
msgstr ""
#: snikket_web/templates/admin_invites.html:55
#: snikket_web/templates/admin_invites.html:49
msgid "Delete invitation"
msgstr ""
#: snikket_web/templates/admin_invites.html:63
#: snikket_web/templates/admin_invites.html:57
msgid "Currently, there are no pending invitations."
msgstr ""
@@ -1581,39 +1565,43 @@ msgstr ""
msgid " (Restricted)"
msgstr ""
#: snikket_web/templates/library.j2:42
#: snikket_web/templates/library.j2:41
msgid "Copy link"
msgstr ""
#: snikket_web/templates/library.j2:43
msgid "Share"
msgstr ""
#: snikket_web/templates/library.j2:125
#: snikket_web/templates/library.j2:104
msgid "Invalid input"
msgstr ""
#: snikket_web/templates/library.j2:166
#: snikket_web/templates/library.j2:145
msgid "Can be used multiple times to create accounts on this Snikket service."
msgstr ""
#: snikket_web/templates/library.j2:147
msgid "Can be used once to create an account on this Snikket service."
msgstr ""
#: snikket_web/templates/library.j2:153
msgid ""
"Limited users can interact with users on the same Snikket service and be "
"members of circles."
msgstr ""
#: snikket_web/templates/library.j2:168
#: snikket_web/templates/library.j2:155
msgid ""
"Like limited users and can also interact with users on other Snikket "
"services."
msgstr ""
#: snikket_web/templates/library.j2:170
#: snikket_web/templates/library.j2:157
msgid "Like normal users and can access the admin panel in the web portal."
msgstr ""
#: snikket_web/templates/library.j2:184
#: snikket_web/templates/library.j2:171
msgid "Invite a single person (invitation link can only be used once)."
msgstr ""
#: snikket_web/templates/library.j2:186
#: snikket_web/templates/library.j2:173
msgid "Invite a group of people (invitation link can be used multiple times)."
msgstr ""

View File

@@ -36,4 +36,3 @@ image/edit:edit
action/admin_panel_settings:admin
content/link:link
content/insights:insights
social/share:share