Compare commits

...

36 Commits

Author SHA1 Message Date
Matthew Wild
4de4509fc9 Update __init__.py 2021-02-20 07:07:18 +00:00
misiek
93e3b325b1 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-18 20:01:22 +00:00
Matthew Wild
ceecfc861c docker: allow custom bind interface/port from environment 2021-02-17 13:38:10 +00:00
Link Mauve
2467e73781 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-16 12:02:25 +00:00
Jonas Schäfer
2f34d39a09 Merge pull request #58 from linkmauve/more-translated-titles
Make more titles translatable
2021-02-14 16:09:39 +01:00
Emmanuel Gil Peyrot
de8589923b Make more titles translatable 2021-02-14 13:11:18 +01:00
Tilman Jiménez
db3a1ac22f Translated using Weblate (Spanish (Mexico))
Currently translated at 41.6% (92 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-02-10 17:01:21 +00:00
Jonas Schäfer
b48d130659 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-10 17:01:20 +00:00
Jonas Schäfer
1aed573eb2 Bump version number for pre-release
(Sigh, I need to get better at this… Or automate it.)
2021-02-09 16:46:18 +01:00
Jonas Schäfer
d4707196ec Include Link header and element in invite response
This allows future App versions to also work with the invite page
without having to screen scrape the content.

Fixes #56 (at least for the portal side of things).
2021-02-09 16:44:50 +01:00
Jonas Schäfer
8a8d4c54bd Collapse the logout button text on narrow displays
This prevents ugly line wraps on long site names
2021-02-09 16:44:50 +01:00
Jonas Schäfer
ab534e3a59 Fix strange 308 error code when using slash-less invite
That seems to be some Quart-internal redirect which isn’t executed
correctly (probably due to our makeshift error handlers). So I
make this a proper redirect instead.
2021-02-09 16:44:50 +01:00
Jonas Schäfer
4c128f1af2 Clarify "Not on mobile" button text
Tester feedback has shown that desktop client users will also
click that button because they are, in fact, not on mobile.

This button speaks more to the users intent (sending the
invitation to the mobile device) after having (hopefully) read
the text above.

Fixes #38.
2021-02-09 16:44:50 +01:00
Jonas Schäfer
8b551a8946 Fix invite page layout after adding support for flashboxes 2021-02-09 16:44:50 +01:00
Tilman Jiménez
182d2301be Translated using Weblate (Spanish (Mexico))
Currently translated at 21.7% (48 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-02-06 18:02:14 +00:00
misiek
6dba5e3a65 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-06 18:02:13 +00:00
Jonas Schäfer
713da89445 Add flash message feedback to all relevant user actions
Fixes #40.
2021-02-06 12:00:55 +01:00
Jonas Schäfer
9876e42fb7 Add support for a flash message sidebar 2021-02-06 12:00:45 +01:00
Jonas Schäfer
8b66c5a063 Add alert role to dynamically added message for a11y 2021-02-06 11:31:55 +01:00
Jonas Schäfer
ddf9f89d77 Remove redundant import 2021-02-06 11:31:51 +01:00
Jonas Schäfer
53e023f9ae Protect against invalid domain on the client side
Here we protect the user from themselves if they accidentally
enter their snikket credentials into the wrong instance by
preventing the form from even being submitted and by showing a
nice error message.
2021-02-06 11:20:05 +01:00
Jonas Schäfer
e4d339627e Protect against incorrect domain name on the server side
Instead of processing the input further and forwarding the
credentials to prosody, we catch the error early on to prevent
having to handle the 400 error code specially and to prevent the
password from spilling in other components.

Fixes #55.
2021-02-06 11:20:05 +01:00
Jonas Schäfer
cd3026911b Added translation using Weblate (Spanish (Mexico)) 2021-02-05 14:30:23 +00:00
GodGoldfish
d7da16f780 Translated using Weblate (Russian)
Currently translated at 55.2% (122 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ru/
2021-02-04 19:02:06 +00:00
Jonas Schäfer
8ed0fbec25 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-04 19:02:05 +00:00
Jonas Schäfer
5b812c773d Fix footer on login page 2021-02-04 15:51:43 +01:00
Michał Mazur
fa61ee4e11 Update __init__.py
Wrong Polish language ISO code. That's probably why it doesn't work.
2021-02-04 14:37:51 +01:00
Jonas Schäfer
7402480c62 Allow / suffix on invite URLs
This makes them a bit more clickable in some user agents (think
email, xmpp) which have to rely on parsing to find and highlight
URLs.

Fixes #48.
2021-02-03 19:00:49 +01:00
Jonas Schäfer
a68a469319 Add extended trademark hints to the about page 2021-02-03 18:57:01 +01:00
Jonas Schäfer
961f285fa5 Add trademark info to the footer
Fixes #45.
2021-02-03 18:55:22 +01:00
Jonas Schäfer
7456295cb6 Make title red if running in debug
This (a) helps developers to not accidentally their production
server and (b) deters user from letting it run that way for long.
2021-02-03 18:50:36 +01:00
Jonas Schäfer
96f4b0d4f8 Make version info only available on admin or debug sessions 2021-02-03 18:47:21 +01:00
Jonas Schäfer
245434126e Bump version number for the next release 2021-02-03 18:44:18 +01:00
Jonas Schäfer
725dffc458 Reduce image size by approximately 65% 2021-02-03 18:36:31 +01:00
Jonas Schäfer
22783b837e Update readme screenshot 2021-02-03 18:30:38 +01:00
Jonas Schäfer
ba18fe692f Fix ClientResponseError if a circle has a deleted user
Eventually, we need to clear that on the backend, but for now we
deal with it in the frontend.

Bonus: this also optimises the display of the circle by removing
O(n) backend requests.
2021-02-03 18:25:29 +01:00
27 changed files with 1363 additions and 163 deletions

View File

@@ -3,44 +3,28 @@ FROM debian:buster
ARG BUILD_SERIES=dev
ARG BUILD_ID=0
ENV DEBIAN_FRONTEND noninteractive
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
COPY Makefile /opt/snikket-web-portal/Makefile
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
# This Dockerfile attempts to strike a balance between image size and time it
# takes to do an incremental build on changes.
# Improvements welcome.
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
export DEBIAN_FRONTEND=noninteractive ; \
apt-get update ; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-setuptools python3-wheel \
libpython3-dev \
make build-essential \
; \
apt-get clean ; rm -rf /var/lib/apt/lists
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
pip3 install -r requirements.txt; \
pip3 install -r build-requirements.txt; \
rm -rf /root/.cache;
COPY Makefile /opt/snikket-web-portal/Makefile
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
# NOTE: abusing true(1) as a terrible way to disable a specific command. If
# one merged all the RUN commands into one, one would want to run the
# uninstall/remove commands there, but with the split up RUN commands it is
# rather pointless.
RUN set -eu; \
make; \
true pip3 uninstall -yr build-requirements.txt; \
true apt-get remove -y build-essential make libpython3-dev; \
true apt-get autoremove -y; \
pip3 uninstall -yr build-requirements.txt; \
apt-get remove -y build-essential make libpython3-dev; \
apt-get autoremove -y; \
pip3 install hypercorn; \
rm -rf /root/.cache; \
apt-get clean ; rm -rf /var/lib/apt/lists

View File

@@ -2,4 +2,7 @@
export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN"
exec hypercorn -b "127.0.0.1:5765" 'snikket_web:create_app()'
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}"
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}"
exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" 'snikket_web:create_app()'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -48,6 +48,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
"text_to_css": colour.text_to_css,
"lang": infra.selected_locale(),
"user_info": user_info,
"is_in_debug_mode": current_app.debug,
}
@@ -148,7 +149,8 @@ class AppConfig:
"en",
"fr",
"id",
"po",
"it",
"pl",
], converter=autosplit)
apple_store_url = environ.var("")

View File

@@ -1,4 +1,4 @@
version_info = (0, 1, 0, "a0")
version_info = (0, 1, 2, "a0")
version = (
".".join(map(str, version_info[:3])) +
(f"-{version_info[3]}" if version_info[3] else "")

View File

@@ -1,4 +1,3 @@
import asyncio
import json
import typing
@@ -18,10 +17,11 @@ from quart import (
url_for,
request,
abort,
flash,
)
import flask_wtf
from flask_babel import lazy_gettext as _l
from flask_babel import lazy_gettext as _l, _
from . import prosodyclient
from .infra import client, circle_name
@@ -69,6 +69,10 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
if form.action_delete.data:
await client.delete_user_by_localpart(localpart)
await flash(
_("User deleted"),
"success",
)
return redirect(url_for(".users"))
return await render_template(
@@ -108,8 +112,16 @@ async def create_password_reset_link() -> typing.Union[str, quart.Response]:
localpart=localpart,
ttl=86400,
)
await flash(
_("Password reset link created"),
"success",
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
await flash(
_("Password reset link deleted"),
"success",
)
return redirect(url_for(".users"))
return await render_template(
@@ -243,6 +255,10 @@ async def create_invite() -> typing.Union[str, quart.Response]:
group_ids=form.circles.data,
ttl=form.lifetime.data,
)
await flash(
_("Invitation created"),
"success",
)
return redirect(url_for(".edit_invite", id_=invite.id_))
return await render_template("admin_create_invite.html",
invite_form=form)
@@ -255,7 +271,11 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
invite_info = await client.get_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
abort(404)
await flash(
_("No such invitation exists"),
"alert",
)
return redirect(url_for(".invitations"))
circles = await client.list_groups()
circle_map = {
circle.id_: circle
@@ -266,6 +286,10 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
if form.action_revoke.data:
await client.delete_invite(id_)
await flash(
_("Invitation revoked"),
"success",
)
return redirect(url_for(".invitations"))
return redirect(url_for(".edit_invite", id_=id_))
@@ -314,6 +338,10 @@ async def create_circle() -> typing.Union[str, quart.Response]:
circle = await client.create_group(
name=create_form.name.data,
)
await flash(
_("Circle created"),
"success",
)
return redirect(url_for(".edit_circle", id_=circle.id_))
return await render_template(
@@ -359,28 +387,28 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
circle_members = await asyncio.gather(*(
client.get_user_by_localpart(
localpart,
session=session,
)
for localpart in sorted(circle.members)
))
users = await client.list_users()
users = sorted(
await client.list_users(),
key=lambda x: x.localpart
)
circle_members = [
user for user in users
if user.localpart in circle.members
]
form = EditCircleForm()
form.user_to_add.choices = sorted(
(
(u.localpart, u.localpart)
for u in users
if u.localpart not in circle.members
),
key=lambda x: x[1]
)
form.user_to_add.choices = [
(user.localpart, user.localpart)
for user in users
if user.localpart not in circle.members
]
valid_users = [x[0] for x in form.user_to_add.choices]
invite_form = InvitePost()
@@ -396,21 +424,36 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
id_,
new_name=form.name.data,
)
await flash(
_("Circle data updated"),
"success",
)
elif form.action_delete.data:
await client.delete_group(id_)
await flash(
_("Circle deleted"),
"success",
)
return redirect(url_for(".circles"))
elif form.action_add_user.data:
if form.user_to_add.data in valid_users:
print("is valid")
await client.add_group_member(
id_,
form.user_to_add.data,
)
await flash(
_("User added to circle"),
"success",
)
elif form.action_remove_user.data:
await client.remove_group_member(
id_,
form.action_remove_user.data,
)
await flash(
_("User removed from circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_))
else:

View File

@@ -48,7 +48,12 @@ def context() -> typing.Mapping[str, typing.Any]:
@bp.route("/<id_>")
async def view(id_: str) -> str:
async def view_old(id_: str) -> quart.Response:
return redirect(url_for(".view", id_=id_))
@bp.route("/<id_>/")
async def view(id_: str) -> typing.Union[quart.Response, str]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -79,13 +84,19 @@ async def view(id_: str) -> str:
)
apple_store_url = current_app.config["APPLE_STORE_URL"]
return await render_template(
body = await render_template(
"invite_view.html",
invite=invite,
play_store_url=play_store_url,
apple_store_url=apple_store_url,
invite_id=id_,
)
return quart.Response(
body,
headers={
"Link": "<{}> rel=\"alternate\"".format(invite.xmpp_uri),
}
)
class RegisterForm(flask_wtf.FlaskForm): # type:ignore

View File

@@ -15,6 +15,7 @@ from quart import (
render_template,
request,
Response,
flash,
)
import babel
@@ -52,6 +53,9 @@ async def index() -> quart.Response:
return redirect(url_for("index"))
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
@bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]:
if client.has_session and (await client.test_session()):
@@ -63,24 +67,35 @@ async def login() -> typing.Union[str, quart.Response]:
localpart, domain, resource = xmpputil.split_jid(jid)
if not localpart:
localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"]
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
form.password.errors.append(
_("Invalid username or password.")
)
if domain != current_app.config["SNIKKET_DOMAIN"]:
# (a) prosody throws a 400 at us and I prefer to catch that here
# and (b) I dont want to pass on this obviously not-for-here
# password further than necessary.
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
return redirect(url_for('user.index'))
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
await flash(
_("Login successful!"),
"success"
)
return redirect(url_for('user.index'))
return await render_template("login.html", form=form)
@bp.route("/meta/about.html")
async def about() -> str:
version = None
extra_versions = {}
if current_app.debug:
if current_app.debug or client.is_admin_session:
version = _version.version
extra_versions["Quart"] = quart.__version__
extra_versions["aiohttp"] = aiohttp.__version__
extra_versions["babel"] = babel.__version__
@@ -89,7 +104,7 @@ async def about() -> str:
return await render_template(
"about.html",
version=_version.version,
version=version,
extra_versions=extra_versions,
)

View File

@@ -252,3 +252,4 @@ $h-sizes: [200.0%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 10
$h-small-sizes: [150.0%, 138.31618672%, 127.54245006%, 117.60790225%, 108.44717712%, 100.0%];
$small-screen-threshold: 40rem;
$medium-screen-threshold: 60rem;
$large-screen-threshold: 80rem;

View File

@@ -33,13 +33,35 @@ body {
main {
padding: $w-l1;
margin-left: auto;
max-width: 60rem;
margin-right: auto;
}
#mwrap {
flex: 1;
display: flex;
flex-direction: row-reverse;
> .filler, > .flashbox {
flex: 1 1 1rem;
}
> main {
flex: 0 1 60rem;
}
}
@media screen and (max-width: $large-screen-threshold) {
#mwrap {
display: block;
> main {
margin-left: auto;
margin-right: auto;
}
}
}
.flashbox > div.box > :first-child {
margin-top: 0;
}
/* top bar */
@@ -67,6 +89,10 @@ div#topbar {
font-size: $_top-h-size;
line-height: 1.5;
body.debug & {
color: red;
}
@media screen and (max-width: $small-screen-threshold) {
font-size: $_top-h-small-size;
}
@@ -134,22 +160,20 @@ body > footer {
background-color: $gray-100;
color: $gray-800;
padding: 0 $w-l1;
font-size: 92.21079115%;
ul {
display: block;
padding: 0;
margin: 0;
list-style-type: none;
text-align: center;
line-height: 1.6267076567643135;
}
li {
display: inline-block;
margin: $w-l1 0;
}
li:before {
content: '';
padding-right: $w-s2;
display: block;
margin: $w-s1 0;
}
a, a:visited, a:hover, a:active, a:focus {
@@ -993,6 +1017,23 @@ div.profile-card {
display: none;
}
}
input[type="submit"], button, .button {
&.slimmify {
> svg.icon {
margin-right: 0;
}
> span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
top: -100px;
}
}
}
}
/* clipboard button */

View File

@@ -3,5 +3,7 @@
{#- -#}
<li>{% trans about_url=url_for('main.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li>
{#- -#}
<li>{% trans %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company.{% endtrans %}</li>
{#- -#}
</ul>
</footer>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
<title>About Snikket</title>
<title>{% trans %}About Snikket{% endtrans %}</title>
{% endblock %}
{% block body %}
<main>
@@ -14,10 +14,12 @@
<p>{% trans agpl_url="https://www.gnu.org/licenses/agpl.html" %}The web portal software is licensed under the terms of the <a href="{{ agpl_url }}">Affero GNU General Public License, version 3.0 or later</a>. The full terms of the license can be reviewed using the aforementioned link.{% endtrans %}</p>
<p>{% trans source_url="https://github.com/snikket-im/snikket-web-portal/" %}The source code of the web portal can be downloaded and viewed in <a href="{{ source_url }}">its GitHub repository</a>.{% endtrans %}</p>
<p>{% trans source_url="https://material.io/resources/icons/", apache20_url="https://www.apache.org/licenses/LICENSE-2.0.txt" %}The icons used in the web portal are <a href="{{ source_url }}">Googles Material Icons</a>, made available by Google under the terms of the <a href="{{ apache20_url }}">Apache 2.0 License</a>.{% endtrans %}</p>
<h3>{% trans %}Trademarks{% endtrans %}</h3>
<p>{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
<h3>{% trans %}Software Versions{% endtrans %}</h3>
<pre>Snikket Server
Domain: {{ config["SNIKKET_DOMAIN"] }}
Snikket Web Portal ({{ version }})
Snikket Web Portal{% if version %} ({{ version }}){% endif %}
{%- if extra_versions -%}
{% for name, version in extra_versions.items() %}
{{ name }} ({{ version }}){% endfor %}

View File

@@ -5,5 +5,5 @@
{% endblock %}
{% block topbar_right %}
{{- super() -}}
{% call standard_button("logout", url_for("user.logout"), class="tertiary") %}{% trans %}Log out{% endtrans %}{% endcall %}
{% call standard_button("logout", url_for("user.logout"), class="tertiary slimmify") %}{% trans %}Log out{% endtrans %}{% endcall %}
{%- endblock %}

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 %}{% if body_class | default(False) %} class="{{ body_class }}"{% endif %}{% 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

@@ -5,6 +5,6 @@
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block body %}
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
<div id="mwrap"><div class="filler"></div><main>{% block content %}{% endblock %}</main><div class="filler"></div></div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -6,6 +6,7 @@
<title>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }} | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/invite-magic.js") }}"></script>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
<link rel="alternate" href="{{ invite.xmpp_uri }}">
{% endblock %}
{% block content %}
<div class="elevated box el-3">
@@ -30,7 +31,7 @@
{%- endif -%}
</ul>
{%- call standard_button("qrcode", "#qr-modal", class="primary", onclick="open_modal(this); return false;") -%}
{% trans %}Not on mobile?{% endtrans %}
{% trans %}Send to mobile device{% endtrans %}
{%- endcall -%}
</div>
<p>{% trans %}After installation the app should automatically open and prompt you to create an account. If not, simply click the button below.{% endtrans %}</p>

View File

@@ -9,16 +9,20 @@
{{ super() }}
{% endblock %}
{% block body %}
<div id="mwrap"><main><div class="form layout-expanded">
<div id="mwrap"><div class="filler"></div><main><div class="form layout-expanded">
<h1 class="form-title">{{ config["SITE_NAME"] }}</h1>
<p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p>
<form method="POST" action="{{ url_for('.login') }}" name="login">
<form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}">
{{ form.csrf_token }}
{% if form.errors %}
{% call box("alert", _("Login failed")) %}
<p>{{ form.errors.values() | flatten | join(", ")}}</p>
{% endcall %}
{% endif %}
<div class="box alert" role="alert" style="display: none;" id="id-warning">
<header>{% trans %}Incorrect address{% endtrans %}</header>
<p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.address.label(class="a11y-only") }}
{{ form.address(placeholder=form.address.label.text) }}
@@ -31,8 +35,22 @@
{%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
</div>
</from>
</div></main></div>
<footer>
<ul><li>{% trans about_url=url_for('.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li></ul>
</footer>
<script type="text/javascript">
var domainCheck = function() {
var form = document.getElementById("login-form");
var addressId = form.dataset.addressid;
var addressField = document.getElementById(addressId);
var domain = form.dataset.domain;
var address = addressField.value;
var errorBox = document.getElementById("id-warning");
if (address.includes("@") && !address.endsWith(domain)) {
errorBox.style.display = "block";
return false;
}
errorBox.style.display = "none";
return true;
};
</script>
</div></main><div class="filler"></div></div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -7,6 +7,25 @@
<div class="filler"></div>
{% block topbar_right %}{% endblock %}
</div>
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
<div id="mwrap">
{#- -#}
<div class="flashbox">
{%- for category, message in get_flashed_messages(True) -%}
<div class="box {{ category }} el-5" role="alert">
{% if category == "success" %}
<header>{% trans %}Operation successful{% endtrans %}</header>
{% elif category == "alert" %}
<header>{% trans %}Error{% endtrans %}</header>
{% endif %}
<p>{{ message }}</p>
</div>
{%- endfor -%}
</div>
{#- -#}
<main>{% block content %}{% endblock %}</main>
{#- -#}
<div class="filler"></div>
{#- -#}
</div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -1,8 +1,5 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Sign out of the Snikket Web Portal{% endtrans %}</h2>

View File

@@ -1,8 +1,5 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, custom_form_button, render_errors %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<div class="form layout-expanded"><form method="POST">
<h1 class="form-title">{% trans %}Change your password{% endtrans %}</h1>

View File

@@ -1,8 +1,5 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button, avatar with context %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<h1>{% trans %}Update your profile{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: SnikketWeb 0.1.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
"PO-Revision-Date: 2021-01-31 12:54+0000\n"
"PO-Revision-Date: 2021-02-10 17:01+0000\n"
"Last-Translator: Jonas Schäfer <jonas@zombofant.net>\n"
"Language-Team: German <https://i18n.sotecware.net/projects/snikket/"
"web-portal/de/>\n"
@@ -353,7 +353,7 @@ msgstr "Neue Gemeinschaft"
#: snikket_web/templates/admin_create_invite.html:3
msgid "Create invitation"
msgstr "Gemeinschaft gründen"
msgstr "Einladung erzeugen"
#: snikket_web/templates/admin_create_invite_form.html:5
msgid "Create new invitation"
@@ -653,7 +653,7 @@ msgstr "Debugging-Informationen für %(user_name)s anzeigen"
#: snikket_web/templates/admin_users.html:28
#, python-format
msgid "Create password reset link for %(user_name)s"
msgstr "Benutzer %(user_name)s löschen"
msgstr "Link zum Zurücksetzen des Passwortes von %(user_name)s erzeugen"
#: snikket_web/templates/app.html:4
msgid "Snikket Web Portal"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
"PO-Revision-Date: 2021-02-02 21:01+0000\n"
"PO-Revision-Date: 2021-02-16 12:02+0000\n"
"Last-Translator: Link Mauve <linkmauve@linkmauve.fr>\n"
"Language-Team: French <https://i18n.sotecware.net/projects/snikket/"
"web-portal/fr/>\n"
@@ -46,7 +46,7 @@ msgstr "Douze heures"
#: snikket_web/admin.py:144
msgid "One day"
msgstr "Un jour"
msgstr "Une journée"
#: snikket_web/admin.py:145
msgid "One week"
@@ -382,11 +382,11 @@ msgstr "Le contenu ci-dessous peut contenir des informations sensibles."
#: snikket_web/templates/admin_debug_user.html:15
msgid "Raw debug dump"
msgstr "Journal de débogage brute"
msgstr "Journal de débogage brut"
#: snikket_web/templates/admin_debug_user.html:17
msgid "Copy complete output"
msgstr "Copier le journal complet"
msgstr "Copier le journal entier"
#: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:22
@@ -686,7 +686,7 @@ msgstr "Le portail web a rencontré une erreur interne."
#: snikket_web/templates/invite_view.html:12
#, python-format
msgid "Invite to %(site_name)s"
msgstr "Inviter à %(site_name)s"
msgstr "Invitation à %(site_name)s"
#: snikket_web/templates/invite_invalid.html:6
#: snikket_web/templates/invite_register.html:10
@@ -762,9 +762,8 @@ msgid ""
"If you plan to use a legacy XMPP client, you can register an account online "
"and enter your credentials into any XMPP-compatible software."
msgstr ""
"Si vous prévoyez dutiliser un ancien client XMPP, vous pouvez ouvrir un "
"compte en ligne et saisir vos indentifiants dans tout logiciel compatible "
"XMPP."
"Si vous prévoyez dutiliser un client XMPP, vous pouvez ouvrir un compte en "
"ligne et saisir vos indentifiants dans tout logiciel compatible XMPP."
#: snikket_web/templates/invite_register.html:27
msgid ""
@@ -907,13 +906,13 @@ msgid ""
"You can now set up your legacy XMPP client with the above address and the "
"password you chose during registration."
msgstr ""
"Vous pouvez maintenant configurer votre ancien client XMPP avec ladresse ci-"
"dessus et le mot de passe que vous avez choisi lors de lenregistrement."
"Vous pouvez maintenant configurer votre client XMPP avec ladresse ci-dessus "
"et le mot de passe que vous avez choisi lors de lenregistrement."
#: snikket_web/templates/invite_view.html:6
#, python-format
msgid "Invite to %(site_name)s | Snikket"
msgstr "Inviter à %(site_name)s | Snikket"
msgstr "Invitation à %(site_name)s | Snikket"
#: snikket_web/templates/invite_view.html:15
#, python-format
@@ -921,9 +920,9 @@ msgid ""
"You have been invited to chat with %(inviter_name)s using Snikket, a secure, "
"privacy-friendly chat app on %(site_name)s."
msgstr ""
"Vous avez été invité à converser avec %(inviter_name)s en utilisant Snikket, "
"une application de messagerie sécurisée et respectueuse de la vie privée sur "
"%(site_name)s."
"Vous avez été invité(e) à converser avec %(inviter_name)s en utilisant "
"Snikket, une application de messagerie sécurisée et respectueuse de la vie "
"privée sur %(site_name)s."
#: snikket_web/templates/invite_view.html:17
#, python-format
@@ -1048,7 +1047,7 @@ msgstr "Peut être utilisée pour créer un seul compte sur ce service Snikket."
#: snikket_web/templates/login.html:5
msgid "Snikket Login"
msgstr "Identifiant Snikket"
msgstr "Connexion à Snikket"
#: snikket_web/templates/login.html:14
msgid "Enter your Snikket address and password to manage your account."
@@ -1065,7 +1064,7 @@ msgstr "Bienvenue!"
#: snikket_web/templates/user_home.html:10
#, python-format
msgid "Welcome home, %(user_name)s."
msgstr "Bienvenue à la maison, %(user_name)s."
msgstr "Bienvenue chez vous, %(user_name)s."
#: snikket_web/templates/user_home.html:14
msgid "Your account"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
"PO-Revision-Date: 2021-01-31 12:54+0000\n"
"PO-Revision-Date: 2021-02-18 20:01+0000\n"
"Last-Translator: misiek <migelazur@mailbox.org>\n"
"Language-Team: Polish <https://i18n.sotecware.net/projects/snikket/"
"web-portal/pl/>\n"
@@ -268,8 +268,8 @@ msgid ""
"\"%(apache20_url)s\">Apache 2.0 License</a>."
msgstr ""
"Ikony wykorzystane w portalu to <a href=\"%(source_url)s\">Googles Material "
"Icons</a>, udostępnione przez Google na warunkach licencji<a href="
"\"%(apache20_url)s\">Apache 2.0</a>."
"Icons</a>, udostępnione przez Google na warunkach licencji <a href=\""
"%(apache20_url)s\">Apache 2.0</a>."
#: snikket_web/templates/about.html:17
msgid "Software Versions"
@@ -690,7 +690,7 @@ msgstr "Portal internetowy napotkał błąd wewnętrzny."
#: snikket_web/templates/invite_view.html:12
#, python-format
msgid "Invite to %(site_name)s"
msgstr "Zaproś na %(site_name)s"
msgstr "Zaproszenie na %(site_name)s"
#: snikket_web/templates/invite_invalid.html:6
#: snikket_web/templates/invite_register.html:10
@@ -930,7 +930,7 @@ msgid ""
"You have been invited to chat on %(site_name)s using Snikket, a secure, "
"privacy-friendly chat app."
msgstr ""
"Zostałeś zaproszony do czatu na %(site_name)s za pomocą Snikket - "
"Zostałeś zaproszony do czatowania na %(site_name)s za pomocą Snikket - "
"bezpiecznej i przyjaznej dla prywatności aplikacji do rozmów."
#: snikket_web/templates/invite_view.html:19
@@ -1040,13 +1040,13 @@ msgstr "usunięty"
#: snikket_web/templates/library.j2:122
msgid "Can be used multiple times to create accounts on this Snikket service."
msgstr ""
"Może być wykorzystywany wielokrotnie, by utworzyć konto na tym serwerze "
"Może być wykorzystywane wielokrotnie, by utworzyć konto na tym serwerze "
"Snikket."
#: snikket_web/templates/library.j2:124
msgid "Can be used once to create an account on this Snikket service."
msgstr ""
"Może być wykorzystany jeden raz, by utworzyć konto na tym serwerze Snikket."
"Może być wykorzystane jeden raz, by utworzyć konto na tym serwerze Snikket."
#: snikket_web/templates/login.html:5
msgid "Snikket Login"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
"PO-Revision-Date: 2021-02-02 21:01+0000\n"
"Last-Translator: GodGoldfish <mathis.useless@protonmail.com>\n"
"PO-Revision-Date: 2021-02-04 19:02+0000\n"
"Last-Translator: GodGoldfish <godgoldfish@pm.me>\n"
"Language-Team: Russian <https://i18n.sotecware.net/projects/snikket/"
"web-portal/ru/>\n"
"Language: ru\n"
@@ -130,12 +130,10 @@ msgid "That username is already taken"
msgstr "Это имя пользователя уже используется"
#: snikket_web/invite.py:141 snikket_web/invite.py:205
#, fuzzy
msgid "Registration was declined for unknown reasons"
msgstr "В регистрации отказано по неизвестным причинам"
msgstr "Регистрация была отклонена по неизвестным причинам"
#: snikket_web/invite.py:145
#, fuzzy
msgid "The username is not valid"
msgstr "Имя пользователя недействительно"
@@ -153,7 +151,6 @@ msgid "Sign in"
msgstr "Войти"
#: snikket_web/main.py:72
#, fuzzy
msgid "Invalid username or password."
msgstr "Неверное имя пользователя или пароль."
@@ -392,14 +389,12 @@ msgstr ""
"Приведенное ниже содержание может содержать конфиденциальную информацию."
#: snikket_web/templates/admin_debug_user.html:15
#, fuzzy
msgid "Raw debug dump"
msgstr "Сырой журнал отладки"
msgstr "Исходная отладка переполнения"
#: snikket_web/templates/admin_debug_user.html:17
#, fuzzy
msgid "Copy complete output"
msgstr "Скопируйте полный журнал"
msgstr "Копировать полный вывод"
#: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:22
@@ -412,13 +407,11 @@ msgid "Delete user"
msgstr "Удалить пользователя"
#: snikket_web/templates/admin_delete_user.html:8
#, fuzzy
msgid "Are you sure you want to delete the following user?"
msgstr "Вы уверены, что хотите удалить следующего пользователя?"
#: snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:10
#, fuzzy
msgid "Login name"
msgstr "Логин"
@@ -447,9 +440,8 @@ msgid "Back"
msgstr "Вернуть"
#: snikket_web/templates/admin_edit_circle.html:14
#, fuzzy
msgid "This is your main circle"
msgstr "Это твой главный круг"
msgstr "Это ваш основной круг"
#: snikket_web/templates/admin_edit_circle.html:15
#, fuzzy
@@ -459,27 +451,23 @@ msgstr ""
#: snikket_web/templates/admin_edit_circle.html:17
#: snikket_web/templates/admin_edit_circle.html:33
#, fuzzy
msgid "Group chat address"
msgstr "Адрес групповой беседы"
msgstr "Адрес группового чата"
#: snikket_web/templates/admin_edit_circle.html:20
#: snikket_web/templates/admin_edit_circle.html:36
#: snikket_web/templates/invite_success.html:15
#: snikket_web/templates/user_home.html:21
#, fuzzy
msgid "Copy address"
msgstr "Скопируйте адрес"
msgstr "Копировать адрес"
#: snikket_web/templates/admin_edit_circle.html:26
#, fuzzy
msgid "Circle information"
msgstr "Информация о круге"
#: snikket_web/templates/admin_edit_circle.html:39
#, fuzzy
msgid "This circle has no group chat associated."
msgstr "У этого круга нет группового разговора."
msgstr "С этим кругом не связан ни один групповой чат."
#: snikket_web/templates/admin_edit_circle.html:48
msgid "Delete circle"
@@ -491,49 +479,41 @@ msgid "Deleting a circle does not delete any users in the circle."
msgstr "Удаление круга не приводит к удалению пользователей из круга."
#: snikket_web/templates/admin_edit_circle.html:55
#, fuzzy
msgid "Circle members"
msgstr "Члены Круга"
msgstr "Участники круга"
#: snikket_web/templates/admin_edit_circle.html:70
#, fuzzy, python-format
#, python-format
msgid "Remove user %(username)s from circle"
msgstr "Удалить пользователя %(username)s из круга"
#: snikket_web/templates/admin_edit_circle.html:78
#, fuzzy
msgid "This circle currently has no members."
msgstr "У этого круга пока нет членов."
msgstr "У этого круга пока нет участников."
#: snikket_web/templates/admin_edit_circle.html:80
#, fuzzy
msgid "Invite more members"
msgstr "Приглашать других членов"
msgstr "Пригласить других участников"
#: snikket_web/templates/admin_edit_circle.html:83
#, fuzzy
msgid "Add existing user"
msgstr "Добавить существующего пользователя"
#: snikket_web/templates/admin_edit_circle.html:94
#, fuzzy
msgid "All users added"
msgstr "Все пользователи добавлены"
#: snikket_web/templates/admin_edit_circle.html:95
#, fuzzy
msgid "All users on this service are already in this circle."
msgstr "Все пользователи этого сервиса уже находятся в этом кругу."
#: snikket_web/templates/admin_edit_invite.html:8
#, fuzzy
msgid "View invitation"
msgstr "Информация о приглашении"
#: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:21
#: snikket_web/templates/admin_reset_user_password.html:15
#, fuzzy
msgid "Valid until"
msgstr "Действительно до"
@@ -571,7 +551,7 @@ msgid "Contact"
msgstr "Контакт"
#: snikket_web/templates/admin_edit_invite.html:41
#, fuzzy, python-format
#, python-format
msgid "The user will get added as contact of %(peer_jid)s."
msgstr "Пользователь будет добавлен как контакт %(peer_jid)s."
@@ -580,9 +560,8 @@ msgid "Created"
msgstr "Создано"
#: snikket_web/templates/admin_home.html:4
#, fuzzy
msgid "Welcome to the admin panel!"
msgstr "Добро пожаловать в панель администрирования!"
msgstr "Добро пожаловать в административную панель!"
#: snikket_web/templates/admin_home.html:5
#, python-format
@@ -594,18 +573,15 @@ msgid "Users"
msgstr "Пользователи"
#: snikket_web/templates/admin_home.html:11
#, fuzzy
msgid "Create password reset links or delete users."
msgstr "Создайте ссылки для сброса пароля или удалите пользователей."
#: snikket_web/templates/admin_home.html:15
#: snikket_web/templates/admin_users.html:4
#, fuzzy
msgid "Manage users"
msgstr "Управление пользователями"
msgstr "Управлять пользователями"
#: snikket_web/templates/admin_home.html:21
#, fuzzy
msgid "Create and manage social circles represented on your service."
msgstr ""
"Создавайте и управляйте социальными кругами, представленными на вашем "
@@ -616,7 +592,6 @@ msgid "Invitations"
msgstr "Приглашения"
#: snikket_web/templates/admin_home.html:29
#, fuzzy
msgid "Create, revoke or copy invitations."
msgstr "Создавайте, отзывайте или копируйте приглашения."

View File

@@ -2,7 +2,14 @@ import asyncio
import typing
import quart.flask_patch
from quart import Blueprint, render_template, request, redirect, url_for
from quart import (
Blueprint,
render_template,
request,
redirect,
url_for,
flash,
)
import quart.exceptions
import wtforms
@@ -93,6 +100,10 @@ async def change_pw() -> typing.Union[str, quart.Response]:
_("Incorrect password"),
)
else:
await flash(
_("Password changed"),
"success",
)
return redirect(url_for("user.change_pw"))
return await render_template("user_passwd.html", form=form)
@@ -131,6 +142,10 @@ async def profile() -> typing.Union[str, quart.Response]:
client.set_nickname_access_model(access_model),
)
await flash(
_("Profile updated"),
"success",
)
return redirect(url_for(".profile"))
return await render_template("user_profile.html", form=form)
@@ -142,6 +157,12 @@ async def logout() -> typing.Union[quart.Response, str]:
form = LogoutForm()
if form.validate_on_submit():
await client.logout()
# No flashing here because we dont collect flashes in the login page
# and itd be weird.
# await flash(
# _("Logged out"),
# "success",
# )
return redirect(url_for("main.index"))
return await render_template("user_logout.html", form=form)