Compare commits

...

79 Commits

Author SHA1 Message Date
Matthew Wild
e56c0f9029 github: Strip Generated-By in both places 2024-01-09 15:07:21 +00:00
Weblate
794b48a50b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-01-09 14:52:58 +00:00
Matthew Wild
393b30cf5c Remove collapsible flag from column to fix mobile display 2024-01-09 14:52:43 +00:00
Matthew Wild
97198a1da4 Ignore generator name/version when checking if translations are up to date 2024-01-09 14:51:34 +00:00
Weblate
3ba1195fbe Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-01-09 10:09:07 +00:00
Matthew Wild
121f3eddb5 Merge pull request #174 from snikket-im/fix/password-validation
Fix password validation
2024-01-09 10:08:58 +00:00
Matthew Wild
38ad81b0e2 Validate passwords as early as possible
Prosody now enforces some password policies, including a minimum length of 10
characters. If this fails, we currently show a rather unfriendly error to the
user. By adding this validation, the user should get nicer feedback and never
see that error.

There is a known issue that we don't currently validate all the policies that
Prosody does - for example, Prosody won't accept a password that contains the
username.

Ultimately we should fix the error handling anyway.
2024-01-08 22:58:46 +00:00
Matthew Wild
ec94c64dbc Handle errors on password change 2024-01-08 22:50:36 +00:00
Andrey
28a9a33aa1 Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-01-07 16:01:03 +00:00
Kim Alvefur
97eeb85032 Merge pull request #173 from Zash/fix-scope-restricted
Fix login for restricted users
2024-01-07 12:40:26 +01:00
Kim Alvefur
ceef9f024c Fix login for restricted users
Previously this led to an OAuth grant with an empty scope, which could
not be used for anything.
2024-01-07 00:03:38 +01:00
Andrey
40c8b9cc36 Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-01-06 05:01:03 +00:00
Matthew Wild
95a8ac1387 Enable Russian and Ukranian languages by default 2024-01-04 14:11:27 +00:00
Roberto Resoli
4c6e26e66b Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2024-01-04 10:17:34 +00:00
Andrey
ad2b351a99 Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-01-04 10:17:33 +00:00
pep
3bda1f9863 Translated using Weblate (French)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2024-01-04 10:17:33 +00:00
Matthew Wild
f46d95db66 Merge pull request #172 from Zash/form-translation-fix
Fix incomplete translation of Announcement form and Limited user role
2024-01-04 09:05:33 +00:00
Kim Alvefur
ddfdd2fd55 Remove stray HTML closing tag
Where is the <form> ???
2024-01-04 09:54:45 +01:00
Kim Alvefur
17d586e384 Fix translation of "Limited"
Strings in Forms must use lazy_gettext aka _l
2024-01-04 09:54:43 +01:00
Kim Alvefur
dbec07d149 Fix translation of AnnouncementForm
Form translation things must use lazy_gettext aka _l
2024-01-04 09:53:03 +01:00
Weblate
ebf142b505 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-01-03 16:12:45 +00:00
Matthew Wild
0539d0ab88 translation: Update copyright year to satisfy lint check 2024-01-03 16:12:28 +00:00
Jonas Schäfer
2736bff76b Translated using Weblate (German)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/de/
2024-01-03 16:08:48 +00:00
Dmytro Vozniuk
192601f387 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/uk/
2023-12-25 16:00:58 +00:00
Dmytro Vozniuk
bc9cfeabab Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2023-12-25 16:00:53 +00:00
Dmytro Vozniuk
b770086071 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/uk/
2023-12-24 04:00:53 +00:00
Dmytro Vozniuk
b2c1fdd23b Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2023-12-24 04:00:52 +00:00
Dmytro Vozniuk
906978556e Translated using Weblate (Ukrainian)
Currently translated at 79.2% (286 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/uk/
2023-12-19 13:00:48 +00:00
riccio
274c8e4658 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2023-12-19 13:00:48 +00:00
Dmytro Vozniuk
257a44dac2 Translated using Weblate (Ukrainian)
Currently translated at 29.6% (107 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/uk/
2023-12-17 22:49:22 +00:00
Roberto Resoli
f393a3980b Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2023-12-17 22:49:22 +00:00
riccio
badff7eed8 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2023-12-17 22:49:22 +00:00
Andrey
384e07c2a9 Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2023-12-17 22:49:21 +00:00
pep
89724a9712 Translated using Weblate (French)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-17 22:49:21 +00:00
misiek
94f4325f40 Translated using Weblate (Polish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2023-12-17 22:49:21 +00:00
Matthew Wild
af1285b650 Translated using Weblate (Russian)
Currently translated at 99.4% (359 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2023-12-16 14:41:14 +00:00
Andrey
52eba53d8e Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2023-12-16 08:38:56 +00:00
pep
94f240687a Translated using Weblate (French)
Currently translated at 99.7% (360 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-16 08:38:56 +00:00
misiek
1b2bdfa881 Translated using Weblate (Polish)
Currently translated at 93.0% (336 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2023-12-16 08:38:56 +00:00
Kim Alvefur
271f450c86 Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2023-12-16 00:19:29 +00:00
Deleted User
6186e8b635 Translated using Weblate (French)
Currently translated at 99.7% (360 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-16 00:19:29 +00:00
pep
dfc6c392c3 Translated using Weblate (French)
Currently translated at 99.7% (360 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-16 00:19:28 +00:00
uira
0ec9a2ae02 Translated using Weblate (Indonesian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2023-12-15 16:11:00 +00:00
Weblate
09fcf64818 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-12-15 15:05:45 +00:00
Kim Alvefur
c25db5c3ae Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2023-12-15 15:05:44 +00:00
Deleted User
c85fff7581 Translated using Weblate (French)
Currently translated at 99.7% (360 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-15 15:05:44 +00:00
Link Mauve
039f4b8210 Translated using Weblate (French)
Currently translated at 99.7% (360 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-15 15:05:44 +00:00
uira
7be7ee67c2 Translated using Weblate (Indonesian)
Currently translated at 93.0% (336 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2023-12-15 15:05:44 +00:00
Jonas Schäfer
6f5fc14dbc Translated using Weblate (German)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/de/
2023-12-15 15:05:44 +00:00
Matthew Wild
65edd3a52b Merge pull request #169 from snikket-im/feature/tweak-user-profile-display
Tweak user profile display
2023-12-15 15:05:36 +00:00
Matthew Wild
ab7149403a Update translations pot (no actual string changes) 2023-12-15 15:04:46 +00:00
Matthew Wild
5b2f3db867 Update user listing in 'edit circle' view to use new user profile layout 2023-12-15 15:00:13 +00:00
Matthew Wild
e12941eab0 Improve user name/JID display
Show the display name more prominently (if one is set), as it is more
"friendly" name. The JID is now displayed instead of just the username, as
this makes it more clear what is being displayed.

The inspiration for this change was the observation that many users on my own
server have the same display name and username, causing a repetitive list
like:

  Jane
  jane

  Robert
  robert

  Sally
  sally

  Steven
  steven

In addition, some accounts do not have a display name set, so it was not
obvious why some people had their name rendered once, and some twice, and why
the capitalization differences.
2023-12-15 14:41:17 +00:00
Andriy Utkin
eda3f4826c Translated using Weblate (Ukrainian)
Currently translated at 6.0% (22 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/uk/
2023-12-15 11:56:32 +00:00
pep
61161eb472 Translated using Weblate (French)
Currently translated at 96.9% (350 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2023-12-15 11:56:31 +00:00
Matthew Wild
325826c19b Merge pull request #168 from snikket-im/fix/rc2-runtime-errors
Fix a couple of runtime errors
2023-12-14 15:35:04 +00:00
Matthew Wild
587839f852 Added translation using Weblate (Ukrainian) 2023-12-14 13:11:43 +00:00
Matthew Wild
7411f4a9e1 prosodyclient: Use empty name if none provided by the server
In parallel, I have updated the server to use the group name for groups with
no name (the MUCs with no name are typically the default auto-created group
MUC).
2023-12-14 12:43:59 +00:00
Matthew Wild
d63ae4768a infra: Fix string to use correct translation function name 2023-12-14 12:43:52 +00:00
Weblate
92a8da724f Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-12-12 18:25:32 +00:00
Matthew Wild
ea3a081b6c Merge pull request #167 from snikket-im/fix/remove-useless-qr-js
Remove broken/needless JS from certain pages
2023-12-12 18:25:20 +00:00
Matthew Wild
0647ba2601 Remove broken/needless JS from certain pages 2023-12-12 18:24:01 +00:00
Kim Alvefur
2769036f94 Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2023-12-09 16:59:59 +00:00
Weblate
c76befad1c Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-12-08 12:14:28 +00:00
Matthew Wild
74ecfb8653 Merge pull request #166 from snikket-im/feature/admin-users-ui-updates-dec-23
Admin user management UI updates (Dec 23)
2023-12-08 12:14:19 +00:00
Matthew Wild
55b195cd7f Update translations 2023-12-08 12:08:43 +00:00
Matthew Wild
46a7d0c37d Fix some type annotations 2023-12-08 12:06:43 +00:00
Matthew Wild
c63b95c6e0 Align avatar flush with left edge of container 2023-12-08 11:42:30 +00:00
Matthew Wild
6848691141 css: Remove avatar border and round the edges to match the app 2023-12-08 11:42:03 +00:00
Matthew Wild
1e83881a24 Ensure we only have a single primary button to reduce confusion 2023-12-08 11:12:26 +00:00
Matthew Wild
35e6bec328 Improvements for admin user listing view 2023-12-08 11:11:31 +00:00
Matthew Wild
d345f0d98d css: Fix dark mode contrast issue for legend text 2023-12-08 11:11:02 +00:00
Matthew Wild
f5ccb7d858 admin: Support for unlocking/restoring locked/deleted user accounts 2023-12-08 11:10:32 +00:00
Matthew Wild
f7c8bccfa2 import-icons.sh: Use sensible defaults where possible 2023-12-08 10:52:48 +00:00
Matthew Wild
e5d06877a4 prosodyclient: Update for new mod_http_admin_api (5c589fab6f53)
This adds new features including:

- User account enabled/disabled status (read and write)
- Deletion status (if an account is scheduled for deletion)
- Avatar metadata
2023-12-08 10:50:23 +00:00
Matthew Wild
e7ed9dd176 infra: Extend time/date utilities 2023-12-08 10:50:06 +00:00
Matthew Wild
6778557db8 On success, return to user listing (edit is complete) 2023-12-08 10:49:26 +00:00
Matthew Wild
73f3f25515 Add lock_open and restore_from_trash icons 2023-12-08 10:45:08 +00:00
Matthew Wild
bd66600d05 Merge pull request #165 from snikket-im/feature/multiple-circle-mucs
Support circles with multiple group chats, remove default group chat
2023-11-06 14:26:11 +00:00
40 changed files with 8248 additions and 3159 deletions

View File

@@ -66,10 +66,10 @@ jobs:
pip install flask-babel
- name: Linting
run: |
sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot
sed -ri '/^"POT-Creation-Date: /d;/^"Generated-By: /d' snikket_web/translations/messages.pot
git add snikket_web/translations/messages.pot
make extract_translations
sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot
sed -ri '/^"POT-Creation-Date: /d;/^"Generated-By: /d' snikket_web/translations/messages.pot
git diff --exit-code --color -- snikket_web/translations/messages.pot

View File

@@ -158,7 +158,9 @@ class AppConfig:
"id",
"it",
"pl",
"ru",
"sv",
"uk",
"zh_Hans_CN",
], converter=autosplit)
apple_store_url = environ.var(

View File

@@ -76,7 +76,7 @@ class EditUserForm(BaseForm):
role = wtforms.RadioField(
_l("Access Level"),
choices=[
("prosody:restricted", _("Limited")),
("prosody:restricted", _l("Limited")),
("prosody:registered", _l("Normal user")),
("prosody:admin", _l("Administrator")),
],
@@ -86,6 +86,14 @@ class EditUserForm(BaseForm):
_l("Update user"),
)
action_restore = wtforms.SubmitField(
_l("Restore account"),
)
action_enable = wtforms.SubmitField(
_l("Unlock account"),
)
action_create_reset = wtforms.SubmitField(
_l("Create password reset link"),
)
@@ -112,6 +120,32 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
".user_password_reset_link",
id_=reset_link.id_,
))
elif form.action_restore.data or form.action_enable.data:
await client.enable_user_account(localpart)
try:
if form.action_restore.data:
await flash(
_("User account restored"),
"success",
)
else:
await flash(
_("User account unlocked"),
"success",
)
return redirect(url_for(".users"))
except aiohttp.ClientResponseError:
if form.action_restore.data:
await flash(
_("Could not restore user account"),
"alert",
)
else:
await flash(
_("Could not unlock user account"),
"alert",
)
return redirect(url_for(".edit_user", localpart=localpart))
await client.update_user(
localpart,
@@ -123,7 +157,7 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
_("User information updated."),
"success",
)
return redirect(url_for(".edit_user", localpart=localpart))
return redirect(url_for(".users"))
elif request.method == "GET":
form.localpart.data = target_user_info.localpart
@@ -699,21 +733,21 @@ def get_system_stats() -> typing.MutableMapping[
class AnnouncementForm(BaseForm):
text = wtforms.StringField(
_("Message contents"),
_l("Message contents"),
widget=wtforms.widgets.TextArea(),
validators=[wtforms.validators.DataRequired()],
)
online_only = wtforms.BooleanField(
_("Only send to online users"),
_l("Only send to online users"),
)
action_post_all = wtforms.SubmitField(
_("Post to all users"),
_l("Post to all users"),
)
action_send_preview = wtforms.SubmitField(
_("Send preview to yourself"),
_l("Send preview to yourself"),
)

View File

@@ -4,6 +4,8 @@ import math
import secrets
import typing
from datetime import datetime, timedelta, timezone
import quart.flask_patch # noqa:F401
from quart import (
current_app,
@@ -13,7 +15,8 @@ from quart import (
import flask_babel
import flask_wtf
from flask_babel import _
from flask_babel import lazy_gettext as _l
import flask_babel as _
from . import prosodyclient
@@ -50,7 +53,7 @@ def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable:
def circle_name(c: typing.Any) -> str:
if c.id_ == "default" and c.name == "default":
return _("Main")
return _l("Main")
return c.name
@@ -70,6 +73,43 @@ def format_bytes(n: float) -> str:
return "{}{}".format(n, unit)
def format_last_activity(timestamp: typing.Optional[int]) -> str:
if timestamp is None:
return _l("Never")
last_active = datetime.fromtimestamp(timestamp, tz=timezone.utc)
# TODO: This 'now' should use the user's local time zone, but we
# don't have that information. Thus 'today'/'yesterday' may be
# slightly inaccurate, but compared to alternative solutions it
# should hopefully be "good enough".
now = datetime.now(tz=timezone.utc)
time_ago = now - last_active
yesterday = now - timedelta(days=1)
if (
last_active.year == now.year
and last_active.month == now.month
and last_active.day == now.day
):
return _l("Today")
elif (
last_active.year == yesterday.year
and last_active.month == yesterday.month
and last_active.day == yesterday.day
):
return _l("Yesterday")
return _.gettext(
"%(time)s ago",
time=flask_babel.format_timedelta(time_ago, granularity="day"),
)
def template_now() -> typing.Dict[str, typing.Any]:
return dict(now=lambda: datetime.now(timezone.utc))
def add_vary_language_header(resp: quart.Response) -> quart.Response:
if getattr(g, "language_header_accessed", False):
resp.vary.add("Accept-Language")
@@ -86,6 +126,8 @@ def init_templating(app: quart.Quart) -> None:
app.template_filter("format_bytes")(format_bytes)
app.template_filter("flatten")(flatten)
app.template_filter("circle_name")(circle_name)
app.template_filter("format_last_activity")(format_last_activity)
app.context_processor(template_now)
app.after_request(add_vary_language_header)

View File

@@ -116,6 +116,10 @@ class RegisterForm(BaseForm):
password = wtforms.PasswordField(
_l("Password"),
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.Length(min=10),
],
)
password_confirm = wtforms.PasswordField(
@@ -184,6 +188,10 @@ async def register(id_: str) -> typing.Union[str, werkzeug.Response]:
class ResetForm(BaseForm):
password = wtforms.PasswordField(
_l("Password"),
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.Length(min=10),
],
)
password_confirm = wtforms.PasswordField(

View File

@@ -9,7 +9,7 @@ import types
import typing
import typing_extensions
from datetime import datetime
from datetime import datetime, timezone
import aiohttp
@@ -29,6 +29,7 @@ from . import xmpputil
from .xmpputil import split_jid
SCOPE_RESTRICTED = "prosody:restricted"
SCOPE_DEFAULT = "prosody:registered"
SCOPE_ADMIN = "prosody:admin"
@@ -42,6 +43,52 @@ class TokenInfo:
scopes: typing.Collection[str]
@dataclasses.dataclass(frozen=True)
class UserDeletionRequestInfo:
deleted_at: datetime
pending_until: datetime
@classmethod
def from_api_response(
cls,
data: typing.Optional[typing.Mapping[str, typing.Any]],
) -> typing.Optional["UserDeletionRequestInfo"]:
if data is None:
return None
return cls(
deleted_at=datetime.fromtimestamp(
data["deleted_at"],
tz=timezone.utc
),
pending_until=datetime.fromtimestamp(
data["pending_until"],
tz=timezone.utc
)
)
@dataclasses.dataclass(frozen=True)
class AvatarMetadata:
bytes: int
hash: str
type: str
width: typing.Optional[int]
height: typing.Optional[int]
@classmethod
def from_api_response(
cls,
data: typing.Mapping[str, typing.Any],
) -> "AvatarMetadata":
return cls(
hash=data["hash"],
bytes=data["bytes"],
type=data["type"],
width=data.get("width") or None,
height=data.get("height") or None,
)
@dataclasses.dataclass(frozen=True)
class AdminUserInfo:
localpart: str
@@ -49,6 +96,10 @@ class AdminUserInfo:
email: typing.Optional[str]
phone: typing.Optional[str]
roles: typing.Optional[typing.List[str]]
enabled: bool
last_active: typing.Optional[int]
deletion_request: typing.Optional[UserDeletionRequestInfo]
avatar_info: typing.List[AvatarMetadata]
@property
def has_admin_role(self) -> bool:
@@ -75,6 +126,15 @@ class AdminUserInfo:
email=data.get("email") or None,
phone=data.get("phone") or None,
roles=roles,
enabled=data.get("enabled", True),
last_active=data.get("last_active") or None,
deletion_request=UserDeletionRequestInfo.from_api_response(
data.get("deletion_request")
),
avatar_info=[
AvatarMetadata.from_api_response(avatar_info)
for avatar_info in data.get("avatar_info", [])
],
)
@@ -131,7 +191,7 @@ class AdminGroupChatInfo:
return cls(
id_=data["id"],
jid=data["jid"],
name=data["name"],
name=data.get("name", ""),
)
@@ -340,7 +400,7 @@ class ProsodyClient:
request.add_field("password", password)
request.add_field(
"scope",
" ".join([SCOPE_DEFAULT, SCOPE_ADMIN])
" ".join([SCOPE_RESTRICTED, SCOPE_DEFAULT, SCOPE_ADMIN])
)
self.logger.debug("sending OAuth2 request (payload omitted)")
@@ -849,7 +909,7 @@ class ProsodyClient:
self.session_address,
current_password,
)
await self._xml_iq_call(
password_changed = await self._xml_iq_call(
session,
xmpputil.make_password_change_request(
self.session_address,
@@ -860,7 +920,7 @@ class ProsodyClient:
},
sensitive=True,
)
# TODO: error handling
xmpputil.extract_iq_reply(password_changed)
# TODO: obtain a new token using the new password to allow the
# server to expire/revoke all tokens on password change.
self._store_token_in_session(token_info)
@@ -925,6 +985,36 @@ class ProsodyClient:
) as resp:
self._raise_error_from_response(resp)
@autosession
async def enable_user_account(
self,
localpart: str,
*,
session: aiohttp.ClientSession,
) -> None:
async with session.patch(
self._admin_v1_endpoint("/users/{}".format(localpart)),
json={
"enabled": True,
},
) as resp:
self._raise_error_from_response(resp)
@autosession
async def disable_user_account(
self,
localpart: str,
*,
session: aiohttp.ClientSession,
) -> None:
async with session.patch(
self._admin_v1_endpoint("/users/{}".format(localpart)),
json={
"enabled": False,
},
) as resp:
self._raise_error_from_response(resp)
@autosession
async def get_user_debug_info(
self,

View File

@@ -708,8 +708,7 @@ input[type="submit"], button, .button {
height: 1.5em;
vertical-align: middle;
background-size: cover;
box-shadow: inset 0px 0px 0px 2px rgba(0, 0, 0, 0.2);
border-radius: $w-s4;
border-radius: 10%;
margin: 0 0.25em;
@@ -1068,6 +1067,10 @@ pre.guru-meditation {
}
}
label, legend {
color: $gray-800 !important;
}
.box {
background-color: black;
border-color: $gray-800;
@@ -1202,6 +1205,13 @@ pre.guru-meditation {
p.form-desc.weak, p.field-desc.weak {
color: $gray-700;
}
.user-badge-icon {
color: $gray-900 !important;
background-color: $gray-100 !important;
border-color: $gray-300 !important;
box-shadow: black 0 0 2px !important;
}
}
/* tooltip magic */
@@ -1252,3 +1262,53 @@ pre.guru-meditation {
.with-tooltip:hover:before, .with-tooltip:hover:after {
display: block;
}
.username-with-avatar {
display: flex;
align-items: center;
.avatar-container {
position: relative;
.avatar {
margin-left: 0;
}
}
.user-badge-icon {
position: absolute;
bottom: -10px;
right: 0px;
background: white;
border-radius: 50%;
width: 1.2em;
height: 1.2em;
border-color: $gray-500;
border-width: 1px;
border-style: solid;
text-align: center;
margin: 0;
padding: 0;
margin: 0;
padding: 0;
box-shadow: $gray-500 0px 0px 2px;
line-height: 1;
.icon {
/* vertical-align: text-bottom; */
padding: 0.1em;
}
}
.user-info-container {
margin-left: 0.5em;
}
.user-display-name {
font-size: 110%;
}
.user-jid {
font-size: 90%;
}
}

View File

@@ -42,6 +42,16 @@ licensed under the terms of the Apache 2.0 License -->
<g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z" />
</symbol>
<!-- from: action/lock_open/materialiconsround/24px.svg -->
<symbol id="icon-lock_open" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12 13c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6-5h-1V6c0-2.76-2.24-5-5-5-2.28 0-4.27 1.54-4.84 3.75-.14.54.18 1.08.72 1.22.53.14 1.08-.18 1.22-.72C9.44 3.93 10.63 3 12 3c1.65 0 3 1.35 3 3v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 11c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-8c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v8z" />
</symbol>
<!-- from: action/restore_from_trash/materialiconsround/24px.svg -->
<symbol id="icon-restore_from_trash" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v10zm5.65-8.65c.2-.2.51-.2.71 0L16 14h-2v4h-4v-4H8l3.65-3.65zM15.5 4l-.71-.71c-.18-.18-.44-.29-.7-.29H9.91c-.26 0-.52.11-.7.29L8.5 4H6c-.55 0-1 .45-1 1s.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1h-2.5z" />
</symbol>
<!-- from: communication/import_export/materialiconsround/24px.svg -->
<symbol id="icon-import_export" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button, icon %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button, icon, render_user with context %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -47,7 +47,7 @@
<tbody>
{%- for chat in circle_chats -%}
<tr>
<td class="collapsible">{% call value_or_hint(chat.name) %}{% endcall %}</td>
<td>{% call value_or_hint(chat.name) %}{% endcall %}</td>
<td class="nowrap">
{%- call custom_form_button("delete", form.action_remove_group_chat.name, chat.id_, class="primary danger", slim=True) -%}
{% trans name=chat.name %}Delete group chat '{{ name }}'{% endtrans %}
@@ -71,7 +71,6 @@
<div class="el-2 elevated"><table>
<thead>
<th>{% trans %}Login name{% endtrans %}</th>
<th class="collapsible">{% trans %}Display name{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</thead>
<tbody>
@@ -79,13 +78,12 @@
<tr>
<td>
{%- if member -%}
{{ localpart }}
{%- call render_user(member) -%}{%- endcall -%}
{%- else -%}
{{ localpart }}
<span class="with-tooltip above" data-tooltip="{% trans %}The user has been deleted from the server.{% endtrans %}"><em> ({% trans %}deleted{% endtrans %})</em></span>
{%- endif -%}
</td>
<td class="collapsible">{% call value_or_hint(member.display_name) %}{% endcall %}</td>
<td class="nowrap">
{%- call custom_form_button("remove_user", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
{% trans username=member.localpart %}Remove user {{ username }} from circle{% endtrans %}

View File

@@ -19,12 +19,33 @@
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
{% if target_user.deletion_request %}
<div class="box alert">
<header>{% trans %}This user account is pending deletion{% endtrans %}</header>
<p>{% trans date=target_user.deletion_request.deleted_at | format_datetime %}The owner of the account sent a deletion request on {{ date }} using their app.{% endtrans %}
<p>{% trans time=(target_user.deletion_request.pending_until - now())|format_timedelta %}The account has been locked, and will be automatically deleted permanently in {{ time }}.{% endtrans %}</p>
<p>{% trans %}If this was a mistake, you can cancel the deletion and restore the account.{% endtrans %}</p>
{%- call form_button("restore_from_trash", form.action_restore, class="secondary") %}{% endcall %}
</div>
{% elif not target_user.enabled %}
<div class="box alert">
<header>{% trans %}This user account is locked{% endtrans %}</header>
<p>{% trans %}The user will not be able to log in to their account until it is unlocked again.{% endtrans %}</p>
{%- call form_button("lock_open", form.action_enable, class="secondary") %}{% endcall %}
</div>
{% endif %}
<h2 class="form-title">{% trans %}Edit user{% endtrans %}</h2>
<div class="f-ebox">
{{ form.localpart.label }}
{{ form.localpart(readonly="readonly") }}
<p class="form-desc weak">{% trans %}The login name cannot be changed.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.display_name.label }}
{{ form.display_name }}
@@ -63,14 +84,14 @@
{% trans %}If the user has lost their password, you can use the button below to create a special link which allows to change the password of the account, once.{% endtrans %}
</p>
<div class="f-bbox">
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
{%- call form_button("passwd", form.action_create_reset, class="secondary") -%}{%- endcall -%}
</div>
<h2 class="form-title">{% trans %}Debug information{% endtrans %}</h2>
<p class="form-desc">
{% trans %}In some cases, extended information about the user account and the connected devices is necessary to troubleshoot issues. The button below reveals this (sensitive) information.{% endtrans %}
</p>
<div class="f-bbox">
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%}
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="secondary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>

View File

@@ -1,12 +1,12 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
{% from "library.j2" import action_button, avatar, icon, render_user, value_or_hint, custom_form_button with context %}
{% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1>
<div class="elevated el-2"><table>
<thead>
<tr>
<th>{% trans %}Login name{% endtrans %}</th>
<th>{% trans %}Display name{% endtrans %}</th>
<th>{% trans %}User{% endtrans %}</th>
<th>{% trans %}Last active{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
@@ -14,20 +14,19 @@
{% for user in users %}
<tr>
<td>
{{- user.localpart -}}
{%- if user.has_admin_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
{%- endif -%}
{%- if user.has_restricted_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
{%- endif -%}
{%- call render_user(user) -%}{%- endcall -%}
</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
{% if user.enabled %}
<td>{{ user.last_active | format_last_activity }}</td>
{% elif user.deletion_request %}
<td>{% trans %}Deleted{% endtrans %}</td>
{% else %}
<td>{% trans %}Locked{% endtrans %}</td>
{% endif %}
<td class="nowrap">
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
{% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}
{%- endcall -%}
</form>
</td>
</tr>
{% endfor %}

View File

@@ -7,7 +7,6 @@
{% block head_lead %}
{{ super() }}
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
{% endblock %}
{% block content %}
<form method="POST"><div class="form layout-expanded">
@@ -27,9 +26,4 @@
{%- call form_button("passwd", form.action_reset, class="primary") -%}{%- endcall -%}
</div>
</div></form>
<script type="text/javascript">
var onload = function() {
apply_qr_code(document.getElementById("qr-uri"));
};
</script>
{% endblock %}

View File

@@ -134,7 +134,6 @@
var onload = function() {
apply_qr_code(document.getElementById("qr-invite-page"));
apply_qr_code(document.getElementById("qr-uri"));
var popover_as = document.getElementsByClassName("popover");
for (var i = 0; i < popover_as.length; ++i) {
var a = popover_as[i];

View File

@@ -10,6 +10,29 @@
{%- endif -%}
{%- endmacro %}
{% macro render_user(user, caller=None) -%}
<div class="username-with-avatar">
<div class="avatar-container">
{%- call avatar(user.localpart+"@"+config["SNIKKET_DOMAIN"], user.avatar_info[0].hash if user.avatar_info | length > 0 else None ) %}{% endcall -%}
{%- if user.has_admin_role -%}
<div class="user-badge-icon">
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
</div>
{%- elif user.has_restricted_role -%}
<div class="user-badge-icon">
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
</div>
{%- endif -%}
</div>
<div class="user-info-container">
{%- if user.display_name %}
<div class="user-display-name">{{- user.display_name -}}</div>
{%- endif %}
<div class="user-jid"><span class="user-jid-localpart">{{- user.localpart -}}</span><span class="user-jid-at">@</span><span class="user-jid-domain">{{- config["SNIKKET_DOMAIN"] -}}</span></div>
</div>
</div>
{%- endmacro -%}
{% macro showuri(uri, caller=None, id_=None) %}
{%- if uri is none -%}
<em>—</em>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,33 @@
# Translations template for PROJECT.
# Copyright (C) 2023 ORGANIZATION
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2023.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-11-06 13:46+0000\n"
"POT-Creation-Date: 2024-01-08 22:58+0000\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"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.13.1\n"
"Generated-By: Babel 2.12.1\n"
#: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_edit_circle.html:73
#: snikket_web/templates/admin_users.html:8
msgid "Login name"
msgstr ""
#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_edit_circle.html:74
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63
#: snikket_web/user.py:69
msgid "Display name"
msgstr ""
#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:32
#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:53
msgid "Access Level"
msgstr ""
@@ -50,187 +48,228 @@ msgid "Update user"
msgstr ""
#: snikket_web/admin.py:90
msgid "Restore account"
msgstr ""
#: snikket_web/admin.py:94
msgid "Unlock account"
msgstr ""
#: snikket_web/admin.py:98
msgid "Create password reset link"
msgstr ""
#: snikket_web/admin.py:108
#: snikket_web/admin.py:116
msgid "Password reset link created"
msgstr ""
#: snikket_web/admin.py:123
msgid "User information updated."
#: snikket_web/admin.py:128
msgid "User account restored"
msgstr ""
#: snikket_web/admin.py:133
msgid "User account unlocked"
msgstr ""
#: snikket_web/admin.py:140
msgid "Could not restore user account"
msgstr ""
#: snikket_web/admin.py:145
msgid "Could not unlock user account"
msgstr ""
#: snikket_web/admin.py:157
msgid "User information updated."
msgstr ""
#: snikket_web/admin.py:179
msgid "Delete user permanently"
msgstr ""
#: snikket_web/admin.py:158
#: snikket_web/admin.py:192
msgid "User deleted"
msgstr ""
#: snikket_web/admin.py:196
#: snikket_web/admin.py:230
msgid "Password reset link not found"
msgstr ""
#: snikket_web/admin.py:208
#: snikket_web/admin.py:242
msgid "Password reset link deleted"
msgstr ""
#: snikket_web/admin.py:228
#: snikket_web/admin.py:262
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:234
#: snikket_web/admin.py:268
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:239
#: snikket_web/admin.py:273
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:241
#: snikket_web/admin.py:275
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:242
#: snikket_web/admin.py:276
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:243
#: snikket_web/admin.py:277
msgid "One day"
msgstr ""
#: snikket_web/admin.py:244
#: snikket_web/admin.py:278
msgid "One week"
msgstr ""
#: snikket_web/admin.py:245
#: snikket_web/admin.py:279
msgid "Four weeks"
msgstr ""
#: snikket_web/admin.py:251 snikket_web/templates/admin_edit_invite.html:17
#: snikket_web/admin.py:285 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type"
msgstr ""
#: snikket_web/admin.py:253 snikket_web/templates/library.j2:116
#: snikket_web/admin.py:287 snikket_web/templates/library.j2:139
msgid "Individual"
msgstr ""
#: snikket_web/admin.py:254 snikket_web/templates/library.j2:114
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:137
msgid "Group"
msgstr ""
#: snikket_web/admin.py:260
#: snikket_web/admin.py:294
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:322
#: snikket_web/admin.py:356
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:346
#: snikket_web/admin.py:380
msgid "Invitation created"
msgstr ""
#: snikket_web/admin.py:362
#: snikket_web/admin.py:396
msgid "No such invitation exists"
msgstr ""
#: snikket_web/admin.py:377
#: snikket_web/admin.py:411
msgid "Invitation revoked"
msgstr ""
#: snikket_web/admin.py:394 snikket_web/admin.py:442
#: snikket_web/admin.py:428 snikket_web/admin.py:476
#: snikket_web/templates/admin_delete_circle.html:10
#: snikket_web/templates/admin_edit_circle.html:44
msgid "Name"
msgstr ""
#: snikket_web/admin.py:399 snikket_web/templates/admin_circles.html:47
#: snikket_web/admin.py:433 snikket_web/templates/admin_circles.html:47
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:429
#: snikket_web/admin.py:463
msgid "Circle created"
msgstr ""
#: snikket_web/admin.py:447
#: snikket_web/admin.py:481
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:452
#: snikket_web/admin.py:486
msgid "Update circle"
msgstr ""
#: snikket_web/admin.py:458
#: snikket_web/admin.py:492
msgid "Add user"
msgstr ""
#: snikket_web/admin.py:476 snikket_web/admin.py:575 snikket_web/admin.py:623
#: snikket_web/admin.py:510 snikket_web/admin.py:609 snikket_web/admin.py:657
msgid "No such circle exists"
msgstr ""
#: snikket_web/admin.py:513
#: snikket_web/admin.py:547
msgid "Circle data updated"
msgstr ""
#: snikket_web/admin.py:523
#: snikket_web/admin.py:557
msgid "User added to circle"
msgstr ""
#: snikket_web/admin.py:532
#: snikket_web/admin.py:566
msgid "User removed from circle"
msgstr ""
#: snikket_web/admin.py:541
#: snikket_web/admin.py:575
msgid "Chat removed from circle"
msgstr ""
#: snikket_web/admin.py:559
#: snikket_web/admin.py:593
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:586
#: snikket_web/admin.py:620
msgid "Circle deleted"
msgstr ""
#: snikket_web/admin.py:600
#: snikket_web/admin.py:634
msgid "Group chat name"
msgstr ""
#: snikket_web/admin.py:605
#: snikket_web/admin.py:639
msgid "Create group chat"
msgstr ""
#: snikket_web/admin.py:635
#: snikket_web/admin.py:669
msgid "New group chat added to circle"
msgstr ""
#: snikket_web/admin.py:702
#: snikket_web/admin.py:736
msgid "Message contents"
msgstr ""
#: snikket_web/admin.py:708
#: snikket_web/admin.py:742
msgid "Only send to online users"
msgstr ""
#: snikket_web/admin.py:712
#: snikket_web/admin.py:746
msgid "Post to all users"
msgstr ""
#: snikket_web/admin.py:716
#: snikket_web/admin.py:750
msgid "Send preview to yourself"
msgstr ""
#: snikket_web/admin.py:738
#: snikket_web/admin.py:772
msgid "Announcement sent!"
msgstr ""
#: snikket_web/infra.py:53
#: snikket_web/infra.py:56
msgid "Main"
msgstr ""
#: snikket_web/infra.py:78
msgid "Never"
msgstr ""
#: snikket_web/infra.py:95
msgid "Today"
msgstr ""
#: snikket_web/infra.py:101
msgid "Yesterday"
msgstr ""
#: snikket_web/infra.py:105
#, python-format
msgid "%(time)s ago"
msgstr ""
#: snikket_web/invite.py:35
msgid ""
"The account data you tried to import is too large to upload. Please "
@@ -241,56 +280,56 @@ msgstr ""
msgid "Username"
msgstr ""
#: snikket_web/invite.py:118 snikket_web/invite.py:186 snikket_web/main.py:43
#: snikket_web/invite.py:118 snikket_web/invite.py:190 snikket_web/main.py:43
msgid "Password"
msgstr ""
#: snikket_web/invite.py:122 snikket_web/invite.py:190
#: snikket_web/invite.py:126 snikket_web/invite.py:198
msgid "Confirm password"
msgstr ""
#: snikket_web/invite.py:126 snikket_web/invite.py:194
#: snikket_web/invite.py:130 snikket_web/invite.py:202
msgid "The passwords must match."
msgstr ""
#: snikket_web/invite.py:131
#: snikket_web/invite.py:135
msgid "Create account"
msgstr ""
#: snikket_web/invite.py:158
#: snikket_web/invite.py:162
msgid "That username is already taken."
msgstr ""
#: snikket_web/invite.py:162 snikket_web/invite.py:227
#: snikket_web/invite.py:166 snikket_web/invite.py:235
msgid "Registration was declined for unknown reasons."
msgstr ""
#: snikket_web/invite.py:166
#: snikket_web/invite.py:170
msgid "The username is not valid."
msgstr ""
#: snikket_web/invite.py:199 snikket_web/templates/user_home.html:32
#: snikket_web/invite.py:207 snikket_web/templates/user_home.html:32
#: snikket_web/templates/user_passwd.html:29
msgid "Change password"
msgstr ""
#: snikket_web/invite.py:246
#: snikket_web/invite.py:254
msgid "Account data file"
msgstr ""
#: snikket_web/invite.py:250
#: snikket_web/invite.py:258
msgid "Import data"
msgstr ""
#: snikket_web/invite.py:271
#: snikket_web/invite.py:279
#, python-format
msgid ""
"The account data you tried to import is in an unknown format. Please "
"upload an XML file in XEP-0227 format (provided format: %(mimetype)s)."
msgstr ""
#: snikket_web/invite.py:291 snikket_web/templates/unauth.html:18
#: snikket_web/user.py:178
#: snikket_web/invite.py:299 snikket_web/templates/unauth.html:18
#: snikket_web/user.py:184
msgid "Error"
msgstr ""
@@ -318,73 +357,73 @@ msgstr ""
msgid "New password"
msgstr ""
#: snikket_web/user.py:39
#: snikket_web/user.py:42
msgid "Confirm new password"
msgstr ""
#: snikket_web/user.py:43
#: snikket_web/user.py:47
msgid "The new passwords must match."
msgstr ""
#: snikket_web/user.py:50
#: snikket_web/user.py:56
msgid "Sign out"
msgstr ""
#: snikket_web/user.py:55
#: snikket_web/user.py:61
msgid "Nobody"
msgstr ""
#: snikket_web/user.py:56
#: snikket_web/user.py:62
msgid "Friends only"
msgstr ""
#: snikket_web/user.py:57
#: snikket_web/user.py:63
msgid "Everyone"
msgstr ""
#: snikket_web/user.py:67
#: snikket_web/user.py:73
msgid "Avatar"
msgstr ""
#: snikket_web/user.py:71
#: snikket_web/user.py:77
msgid "Profile visibility"
msgstr ""
#: snikket_web/user.py:76
#: snikket_web/user.py:82
msgid "Update profile"
msgstr ""
#: snikket_web/user.py:82
#: snikket_web/user.py:88
msgid "Account data"
msgstr ""
#: snikket_web/user.py:86
#: snikket_web/user.py:92
msgid "Upload"
msgstr ""
#: snikket_web/user.py:111
#: snikket_web/user.py:117
msgid "Incorrect password."
msgstr ""
#: snikket_web/user.py:115
#: snikket_web/user.py:121
msgid "Password changed"
msgstr ""
#: snikket_web/user.py:123
#: snikket_web/user.py:129
msgid ""
"The chosen avatar is too big. To be able to upload larger avatars, please"
" use the app."
msgstr ""
#: snikket_web/user.py:170
#: snikket_web/user.py:176
msgid "Profile updated"
msgstr ""
#: snikket_web/user.py:184
#: snikket_web/user.py:190
msgid "Export"
msgstr ""
#: snikket_web/user.py:202
#: snikket_web/user.py:208
msgid "You currently have no account data to export."
msgstr ""
@@ -505,7 +544,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_edit_circle.html:45
#: snikket_web/templates/admin_edit_circle.html:75
#: snikket_web/templates/admin_edit_circle.html:74
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:10
msgid "Actions"
@@ -639,7 +678,7 @@ msgid "Delete user %(user_name)s"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:6
#: snikket_web/templates/admin_edit_user.html:53
#: snikket_web/templates/admin_edit_user.html:74
msgid "Delete user"
msgstr ""
@@ -703,37 +742,37 @@ msgstr ""
msgid "All members of the circle will see each other in their contact list."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:85
#: snikket_web/templates/admin_edit_circle.html:84
msgid "The user has been deleted from the server."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:85
#: snikket_web/templates/library.j2:108
#: snikket_web/templates/admin_edit_circle.html:84
#: snikket_web/templates/library.j2:131
msgid "deleted"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:91
#: snikket_web/templates/admin_edit_circle.html:89
#, python-format
msgid "Remove user %(username)s from circle"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:99
#: snikket_web/templates/admin_edit_circle.html:97
msgid "This circle currently has no members."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:101
#: snikket_web/templates/admin_edit_circle.html:99
msgid "Invite more members"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:104
#: snikket_web/templates/admin_edit_circle.html:102
msgid "Add existing user"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:115
#: snikket_web/templates/admin_edit_circle.html:113
msgid "All users added"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:116
#: snikket_web/templates/admin_edit_circle.html:114
msgid "All users on this service are already in this circle."
msgstr ""
@@ -808,56 +847,90 @@ msgstr ""
msgid "Edit user %(user_name)s"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:22
msgid "Edit user"
#: snikket_web/templates/admin_edit_user.html:24
msgid "This user account is pending deletion"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:25
#, python-format
msgid ""
"The owner of the account sent a deletion request on %(date)s using their "
"app."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:26
#, python-format
msgid ""
"The account has been locked, and will be automatically deleted "
"permanently in %(time)s."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:28
msgid ""
"If this was a mistake, you can cancel the deletion and restore the "
"account."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:34
msgid "This user account is locked"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:35
msgid ""
"The user will not be able to log in to their account until it is unlocked"
" again."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:41
msgid "Edit user"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:46
msgid "The login name cannot be changed."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:33
#: snikket_web/templates/admin_edit_user.html:54
msgid ""
"The access level of a user determines what interactions are allowed for "
"them on your Snikket service."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:40
#: snikket_web/templates/admin_edit_user.html:61
#, python-format
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:50
#: snikket_web/templates/admin_edit_user.html:71
msgid "Return to user list"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:58
#: snikket_web/templates/admin_edit_user.html:79
msgid "Further actions"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:60
#: snikket_web/templates/admin_edit_user.html:81
msgid "Reset password"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:63
#: snikket_web/templates/admin_edit_user.html:84
msgid ""
"If the user has lost their password, you can use the button below to "
"create a special link which allows to change the password of the account,"
" once."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:68
#: snikket_web/templates/admin_edit_user.html:89
msgid "Debug information"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:70
#: snikket_web/templates/admin_edit_user.html:91
msgid ""
"In some cases, extended information about the user account and the "
"connected devices is necessary to troubleshoot issues. The button below "
"reveals this (sensitive) information."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:74
#: snikket_web/templates/admin_edit_user.html:95
msgid "Show debug information"
msgstr ""
@@ -1048,20 +1121,20 @@ msgid ""
"your Snikket server. Use it wisely."
msgstr ""
#: snikket_web/templates/admin_users.html:19
msgid "The user is an administrator."
#: snikket_web/templates/admin_users.html:8
msgid "User"
msgstr ""
#: snikket_web/templates/admin_users.html:19
msgid " (Administrator)"
#: snikket_web/templates/admin_users.html:9
msgid "Last active"
msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid "The user is restricted."
msgid "Deleted"
msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid " (Restricted)"
#: snikket_web/templates/admin_users.html:24
msgid "Locked"
msgstr ""
#: snikket_web/templates/app.html:4
@@ -1187,11 +1260,11 @@ msgstr ""
msgid "Reset your password | Snikket"
msgstr ""
#: snikket_web/templates/invite_reset.html:15
#: snikket_web/templates/invite_reset.html:14
msgid "Reset your password online"
msgstr ""
#: snikket_web/templates/invite_reset.html:16
#: snikket_web/templates/invite_reset.html:15
msgid ""
"To reset your password online, fill out the fields below and confirm "
"using the button."
@@ -1455,19 +1528,35 @@ msgstr ""
msgid "First install Snikket from F-Droid using the button below:"
msgstr ""
#: snikket_web/templates/library.j2:18
#: snikket_web/templates/library.j2:19
msgid "The user is an administrator."
msgstr ""
#: snikket_web/templates/library.j2:19
msgid " (Administrator)"
msgstr ""
#: snikket_web/templates/library.j2:23
msgid "The user is restricted."
msgstr ""
#: snikket_web/templates/library.j2:23
msgid " (Restricted)"
msgstr ""
#: snikket_web/templates/library.j2:41
msgid "Copy link"
msgstr ""
#: snikket_web/templates/library.j2:81
#: snikket_web/templates/library.j2:104
msgid "Invalid input"
msgstr ""
#: snikket_web/templates/library.j2:122
#: 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:124
#: snikket_web/templates/library.j2:147
msgid "Can be used once to create an account on this Snikket service."
msgstr ""

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,16 +32,22 @@ class ChangePasswordForm(BaseForm):
new_password = wtforms.PasswordField(
_l("New password"),
validators=[wtforms.validators.InputRequired()]
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.Length(min=10),
]
)
new_password_confirm = wtforms.PasswordField(
_l("Confirm new password"),
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"new_password",
_l("The new passwords must match.")
)]
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"new_password",
_l("The new passwords must match.")
),
wtforms.validators.Length(min=10),
]
)

View File

@@ -6,6 +6,8 @@ action/logout:logout
action/login:login
action/exit_to_app:exit_to_app
action/lock:lock
action/lock_open:lock_open
action/restore_from_trash:restore_from_trash
communication/import_export:import_export
communication/qr_code:qrcode
communication/vpn_key:passwd

6
tools/import-icons.sh Normal file → Executable file
View File

@@ -9,9 +9,9 @@ set -euo pipefail
# FLAVOR one of '', 'round', 'sharp', 'outlined', 'twoshade'
# SVGOUT path to the newly created SVG file
root="$1/src"
iconlist_file="$2"
flavor="$3"
output_file="$4"
iconlist_file="${2-tools/icons.list}"
flavor="${3-round}"
output_file="${4-snikket_web/static/img/icons.svg}"
printf '<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n<defs>\n' > "$output_file"
printf '<!-- These icons are sourced from Googles Material Icons set,\nlicensed under the terms of the Apache 2.0 License -->\n' >> "$output_file"