You've already forked snikket-web-portal
Compare commits
71 Commits
feature/wt
...
feature/ht
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb464f428 | ||
|
|
2a6ef3c8f1 | ||
|
|
b5d148458a | ||
|
|
261758b07a | ||
|
|
ff99c9488a | ||
|
|
fe78631039 | ||
|
|
12ddd288bf | ||
|
|
633fb0d084 | ||
|
|
f9690063bc | ||
|
|
65ed50acd3 | ||
|
|
aa04320d70 | ||
|
|
818d50a1bb | ||
|
|
c7ba7985ea | ||
|
|
223d127364 | ||
|
|
3a2c4543c4 | ||
|
|
c307f057b9 | ||
|
|
243d5ba236 | ||
|
|
3d62efccfc | ||
|
|
9d26e39025 | ||
|
|
874f0447ba | ||
|
|
0f2127a672 | ||
|
|
20d84e7dd1 | ||
|
|
a02e66023c | ||
|
|
e7db9cc772 | ||
|
|
e91fb45374 | ||
|
|
531565d55c | ||
|
|
c6307619f9 | ||
|
|
da2668cbbc | ||
|
|
765e3890b4 | ||
|
|
b40a625283 | ||
|
|
8a293985ca | ||
|
|
13b2a76c3d | ||
|
|
28e01c336d | ||
|
|
5fb0b91178 | ||
|
|
b007afc901 | ||
|
|
7f02746f63 | ||
|
|
f2788aeb36 | ||
|
|
536a05b0eb | ||
|
|
e0226d47e3 | ||
|
|
0fe10a44ce | ||
|
|
e892d81815 | ||
|
|
c58ce8450f | ||
|
|
03573d1f05 | ||
|
|
486596f89f | ||
|
|
425b4d4295 | ||
|
|
87de808046 | ||
|
|
05455ac743 | ||
|
|
1e926714cb | ||
|
|
e1602f3140 | ||
|
|
2e89973263 | ||
|
|
a6f1361ddd | ||
|
|
552a3bbd41 | ||
|
|
3f2de1e5bf | ||
|
|
059a10f475 | ||
|
|
a48abacf1d | ||
|
|
ea7ed7c030 | ||
|
|
cca899bd8c | ||
|
|
359e6b4ce2 | ||
|
|
6650dd2046 | ||
|
|
97b4a7be0f | ||
|
|
329916e200 | ||
|
|
3571b8909b | ||
|
|
c6c01b82f5 | ||
|
|
c4b575f091 | ||
|
|
fdb55568ec | ||
|
|
a9a651be09 | ||
|
|
d2069289b0 | ||
|
|
552b5d2940 | ||
|
|
b0f9ae5d57 | ||
|
|
dd4a012612 | ||
|
|
e7aa0a2c45 |
1
.github/workflows/main.yaml
vendored
1
.github/workflows/main.yaml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
pip install mypy
|
||||
pip install -r requirements.txt
|
||||
pip install -r build-requirements.txt
|
||||
- name: Typecheck
|
||||
run: |
|
||||
python -m mypy --config mypy.ini -p snikket_web
|
||||
|
||||
@@ -37,5 +37,7 @@ ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
|
||||
|
||||
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
|
||||
|
||||
RUN echo "$BUILD_SERIES $BUILD_ID" > /opt/snikket-web-portal/.app_version
|
||||
|
||||
ADD docker/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pyscss~=1.3
|
||||
mypy
|
||||
python-dotenv~=0.15
|
||||
types-toml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiohttp~=3.6
|
||||
quart~=0.11
|
||||
quart~=0.11,<0.15
|
||||
flask-wtf~=0.14
|
||||
hsluv~=0.0.2
|
||||
flask-babel~=1.0
|
||||
|
||||
@@ -21,7 +21,7 @@ from quart import (
|
||||
import environ
|
||||
|
||||
from . import colour, infra
|
||||
from ._version import version, version_info # noqa:F401
|
||||
from ._version import version # noqa:F401
|
||||
|
||||
|
||||
async def proc() -> typing.Dict[str, typing.Any]:
|
||||
@@ -154,12 +154,15 @@ class AppConfig:
|
||||
"pl",
|
||||
"sv",
|
||||
], converter=autosplit)
|
||||
apple_store_url = environ.var("")
|
||||
apple_store_url = environ.var(
|
||||
"https://apps.apple.com/us/app/snikket/id1545164189",
|
||||
)
|
||||
# Default limit of 1 MiB is what was discovered to be the effective limit
|
||||
# in #67, hence we set that here for now.
|
||||
# Future versions may change this default, and the standard deployment
|
||||
# tools may also very well override it.
|
||||
max_avatar_size = environ.var(1024*1024, converter=int)
|
||||
show_metrics = environ.bool_var(True)
|
||||
|
||||
|
||||
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
|
||||
@@ -172,7 +175,7 @@ def create_app() -> quart.Quart:
|
||||
pass
|
||||
else:
|
||||
import runpy
|
||||
init_vars = runpy.run_path(env_init) # type:ignore
|
||||
init_vars = runpy.run_path(env_init)
|
||||
for name, value in init_vars.items():
|
||||
if not name:
|
||||
continue
|
||||
@@ -191,6 +194,7 @@ def create_app() -> quart.Quart:
|
||||
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
|
||||
app.config["APPLE_STORE_URL"] = config.apple_store_url
|
||||
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
|
||||
app.config["SHOW_METRICS"] = config.show_metrics
|
||||
|
||||
app.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
version_info = (0, 1, 2, "a0")
|
||||
version = (
|
||||
".".join(map(str, version_info[:3])) +
|
||||
(f"-{version_info[3]}" if version_info[3] else "")
|
||||
)
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
version = "(unknown)"
|
||||
|
||||
if os.path.exists(".app_version"):
|
||||
with open(".app_version") as f:
|
||||
version = f.read().strip()
|
||||
elif os.path.exists(".git"):
|
||||
try:
|
||||
version = subprocess.check_output([
|
||||
"git", "describe", "--always"
|
||||
]).strip().decode("utf8")
|
||||
except OSError:
|
||||
version = "dev (unknown)"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import resource
|
||||
import time
|
||||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
@@ -18,11 +20,12 @@ from quart import (
|
||||
request,
|
||||
abort,
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
|
||||
from flask_babel import lazy_gettext as _l, _
|
||||
|
||||
from . import prosodyclient
|
||||
from . import prosodyclient, _version
|
||||
from .infra import client, circle_name, BaseForm
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@@ -31,11 +34,14 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@bp.route("/")
|
||||
@client.require_admin_session()
|
||||
async def index() -> str:
|
||||
return await render_template("admin_home.html")
|
||||
show_metrics = current_app.config["SHOW_METRICS"]
|
||||
return await render_template(
|
||||
"admin_home.html",
|
||||
show_metrics=show_metrics,
|
||||
)
|
||||
|
||||
|
||||
class PasswordResetLinkPost(BaseForm):
|
||||
action_create = wtforms.StringField()
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
@@ -46,11 +52,95 @@ async def users() -> str:
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
)
|
||||
invite_form = InvitePost()
|
||||
await invite_form.init_choices()
|
||||
reset_form = PasswordResetLinkPost()
|
||||
return await render_template(
|
||||
"admin_users.html",
|
||||
users=users,
|
||||
reset_form=reset_form,
|
||||
invite_form=invite_form,
|
||||
)
|
||||
|
||||
|
||||
_LIMITED_ROLE_NAME = _("Limited")
|
||||
|
||||
|
||||
class EditUserForm(BaseForm):
|
||||
localpart = wtforms.StringField(
|
||||
_l("Login name"),
|
||||
)
|
||||
|
||||
display_name = wtforms.StringField(
|
||||
_l("Display name"),
|
||||
)
|
||||
|
||||
role = wtforms.RadioField(
|
||||
_l("Access Level"),
|
||||
choices=[
|
||||
# NOTE: enable this only after something has been done which
|
||||
# actually enforces the described restrictions :).
|
||||
# ("prosody:restricted", _LIMITED_ROLE_NAME),
|
||||
("prosody:normal", _l("Normal user")),
|
||||
("prosody:admin", _l("Administrator")),
|
||||
],
|
||||
)
|
||||
|
||||
action_save = wtforms.SubmitField(
|
||||
_l("Update user"),
|
||||
)
|
||||
|
||||
action_create_reset = wtforms.SubmitField(
|
||||
_l("Create password reset link"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
|
||||
form = EditUserForm()
|
||||
if form.validate_on_submit():
|
||||
if form.action_create_reset.data:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
reset_link = await client.create_password_reset_invite(
|
||||
localpart=localpart,
|
||||
ttl=86400,
|
||||
)
|
||||
await flash(
|
||||
_("Password reset link created"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(
|
||||
".user_password_reset_link",
|
||||
id_=reset_link.id_,
|
||||
))
|
||||
|
||||
await client.update_user(
|
||||
localpart,
|
||||
display_name=form.display_name.data,
|
||||
roles=[form.role.data],
|
||||
)
|
||||
|
||||
await flash(
|
||||
_("User information updated."),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
|
||||
elif request.method == "GET":
|
||||
form.localpart.data = target_user_info.localpart
|
||||
form.display_name.data = target_user_info.display_name
|
||||
if target_user_info.roles:
|
||||
form.role.data = target_user_info.roles[0]
|
||||
else:
|
||||
form.role.data = "prosody:normal"
|
||||
|
||||
return await render_template(
|
||||
"admin_edit_user.html",
|
||||
target_user=target_user_info,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@@ -97,36 +187,38 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/users/password-reset/-", methods=["POST"])
|
||||
@bp.route("/users/password-reset/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_password_reset_link() -> typing.Union[str, quart.Response]:
|
||||
form = PasswordResetLinkPost()
|
||||
if not form.validate_on_submit():
|
||||
abort(400)
|
||||
|
||||
if form.action_create.data:
|
||||
localpart = form.action_create.data
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
reset_link = await client.create_password_reset_invite(
|
||||
localpart=localpart,
|
||||
ttl=86400,
|
||||
)
|
||||
async def user_password_reset_link(
|
||||
id_: str,
|
||||
) -> typing.Union[str, quart.Response]:
|
||||
invite_info = await client.get_invite_by_id(
|
||||
id_,
|
||||
)
|
||||
if invite_info.jid is None:
|
||||
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",
|
||||
_("Password reset link not found"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".users"))
|
||||
|
||||
localpart = prosodyclient.split_jid(invite_info.jid)[0]
|
||||
|
||||
form = PasswordResetLinkPost()
|
||||
if form.validate_on_submit():
|
||||
if form.action_revoke.data:
|
||||
await client.delete_invite(id_)
|
||||
await flash(
|
||||
_("Password reset link deleted"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
abort(400)
|
||||
|
||||
return await render_template(
|
||||
"admin_reset_user_password.html",
|
||||
target_user=target_user_info,
|
||||
reset_link=reset_link,
|
||||
localpart=localpart,
|
||||
reset_link=invite_info,
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -393,21 +485,21 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
return redirect(url_for(".circles"))
|
||||
raise
|
||||
|
||||
users = sorted(
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
)
|
||||
users = {
|
||||
user.localpart: user
|
||||
for user in await client.list_users()
|
||||
}
|
||||
circle_members = [
|
||||
user for user in users
|
||||
if user.localpart in circle.members
|
||||
(localpart, users.get(localpart))
|
||||
for localpart in sorted(circle.members)
|
||||
]
|
||||
|
||||
form = EditCircleForm()
|
||||
form.user_to_add.choices = [
|
||||
(user.localpart, user.localpart)
|
||||
for user in users
|
||||
if user.localpart not in circle.members
|
||||
]
|
||||
form.user_to_add.choices = sorted(
|
||||
(localpart, localpart)
|
||||
for localpart in users.keys()
|
||||
if localpart not in circle.members
|
||||
)
|
||||
valid_users = [x[0] for x in form.user_to_add.choices]
|
||||
|
||||
invite_form = InvitePost()
|
||||
@@ -465,3 +557,148 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
circle_members=circle_members,
|
||||
invite_form=invite_form,
|
||||
)
|
||||
|
||||
|
||||
_CPU_EPOCH = time.process_time()
|
||||
_MONOTONIC_EPOCH = time.monotonic()
|
||||
|
||||
|
||||
def get_system_stats() -> typing.MutableMapping[
|
||||
str,
|
||||
typing.Optional[typing.Union[int, float]]]:
|
||||
pagesize = resource.getpagesize()
|
||||
my_rss: typing.Optional[int] = None
|
||||
try:
|
||||
with open("/proc/self/statm") as f:
|
||||
stats = f.read().split()
|
||||
my_rss = int(stats[1]) * pagesize
|
||||
except (ValueError, IndexError, TypeError, OSError):
|
||||
pass
|
||||
|
||||
my_cpu = (
|
||||
(time.process_time() - _CPU_EPOCH) /
|
||||
(time.monotonic() - _MONOTONIC_EPOCH)
|
||||
)
|
||||
|
||||
mem_total, mem_available = None, None
|
||||
load5: typing.Optional[float] = None
|
||||
|
||||
try:
|
||||
with open("/proc/loadavg") as f:
|
||||
stats = f.read().split()
|
||||
load5 = float(stats[1])
|
||||
except (ValueError, IndexError, TypeError, OSError):
|
||||
pass
|
||||
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemTotal"):
|
||||
mem_total = int(line.split()[1]) * 1024
|
||||
elif line.startswith("MemAvailable"):
|
||||
mem_available = int(line.split()[1]) * 1024
|
||||
if mem_total is not None and mem_available is not None:
|
||||
break
|
||||
except (ValueError, TypeError, IndexError, OSError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"portal_rss": my_rss,
|
||||
"portal_cpu": my_cpu,
|
||||
"load5": load5,
|
||||
"mem_total": mem_total,
|
||||
"mem_available": mem_available,
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(BaseForm):
|
||||
text = wtforms.StringField(
|
||||
_("Message contents"),
|
||||
widget=wtforms.widgets.TextArea(),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
)
|
||||
|
||||
online_only = wtforms.BooleanField(
|
||||
_("Only send to online users"),
|
||||
)
|
||||
|
||||
action_post_all = wtforms.SubmitField(
|
||||
_("Post to all users"),
|
||||
)
|
||||
|
||||
action_send_preview = wtforms.SubmitField(
|
||||
_("Send preview to yourself"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/system/", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def system() -> typing.Union[str, quart.Response]:
|
||||
form = AnnouncementForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
recipients = "self"
|
||||
if form.action_post_all.data:
|
||||
if form.online_only.data:
|
||||
recipients = "online"
|
||||
else:
|
||||
recipients = "all"
|
||||
|
||||
await client.post_announcement(
|
||||
form.text.data,
|
||||
recipients=recipients,
|
||||
)
|
||||
await flash(
|
||||
_("Announcement sent!"),
|
||||
"success",
|
||||
)
|
||||
if recipients != "self":
|
||||
# redirect only if not previewing
|
||||
return redirect(url_for(".system"))
|
||||
|
||||
version = None
|
||||
now = None
|
||||
show_metrics = current_app.config["SHOW_METRICS"]
|
||||
if show_metrics:
|
||||
version = await client.get_server_version()
|
||||
now = time.time()
|
||||
try:
|
||||
prosody_metrics = await client.get_system_metrics()
|
||||
except quart.exceptions.NotFound:
|
||||
# server does not offer the endpoint for whatever reason -- ignore
|
||||
prosody_metrics = {}
|
||||
|
||||
metrics = get_system_stats()
|
||||
try:
|
||||
prosody_cpu_metrics = prosody_metrics["cpu"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
metrics["prosody_cpu"] = (prosody_cpu_metrics["value"] /
|
||||
(now - prosody_cpu_metrics["since"]))
|
||||
|
||||
try:
|
||||
metrics["prosody_rss"] = prosody_metrics["memory"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
metrics["prosody_devices"] = prosody_metrics["c2s"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for k in list(metrics.keys()):
|
||||
if metrics[k] is None:
|
||||
# so that defaulting in jinja works
|
||||
del metrics[k]
|
||||
else:
|
||||
metrics = {}
|
||||
|
||||
return await render_template(
|
||||
"admin_system.html",
|
||||
metrics=metrics,
|
||||
version=_version.version,
|
||||
prosody_version=version,
|
||||
form=form,
|
||||
show_metrics=show_metrics,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import itertools
|
||||
import math
|
||||
import secrets
|
||||
import typing
|
||||
|
||||
@@ -22,11 +23,20 @@ client.default_login_redirect = "main.login"
|
||||
babel = flask_babel.Babel()
|
||||
|
||||
|
||||
BYTE_UNIT_SCALE_MAP = [
|
||||
"B",
|
||||
"kiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
]
|
||||
|
||||
|
||||
@babel.localeselector # type:ignore
|
||||
def selected_locale() -> str:
|
||||
selected = request.accept_languages.best_match(
|
||||
current_app.config['LANGUAGES']
|
||||
)
|
||||
) or current_app.config['LANGUAGES'][0]
|
||||
return selected
|
||||
|
||||
|
||||
@@ -42,12 +52,27 @@ def circle_name(c: typing.Any) -> str:
|
||||
return c.name
|
||||
|
||||
|
||||
def format_bytes(n: float) -> str:
|
||||
scale = math.floor(math.log(n, 1024))
|
||||
try:
|
||||
unit = BYTE_UNIT_SCALE_MAP[scale]
|
||||
factor = 1024**scale
|
||||
except ValueError:
|
||||
unit = "TiB"
|
||||
factor = 1024**4
|
||||
if factor > 1:
|
||||
return "{:.1f} {}".format(n / factor, unit)
|
||||
return "{} {}".format(n, unit)
|
||||
|
||||
|
||||
def init_templating(app: quart.Quart) -> None:
|
||||
app.template_filter("repr")(repr)
|
||||
app.template_filter("format_datetime")(flask_babel.format_datetime)
|
||||
app.template_filter("format_date")(flask_babel.format_date)
|
||||
app.template_filter("format_time")(flask_babel.format_time)
|
||||
app.template_filter("format_timedelta")(flask_babel.format_timedelta)
|
||||
app.template_filter("format_percent")(flask_babel.format_percent)
|
||||
app.template_filter("format_bytes")(format_bytes)
|
||||
app.template_filter("flatten")(flatten)
|
||||
app.template_filter("circle_name")(circle_name)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class RegisterForm(BaseForm):
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"password",
|
||||
_l("The passwords must match")
|
||||
_l("The passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ class ResetForm(BaseForm):
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"password",
|
||||
_l("The passwords must match")
|
||||
_l("The passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ def repad(s: str) -> str:
|
||||
|
||||
@bp.route("/avatar/<from_>/<code>")
|
||||
async def avatar(from_: str, code: str) -> quart.Response:
|
||||
etag: typing.Optional[str]
|
||||
try:
|
||||
etag = request.headers["if-none-match"]
|
||||
except KeyError:
|
||||
|
||||
@@ -44,6 +44,15 @@ class AdminUserInfo:
|
||||
display_name: typing.Optional[str]
|
||||
email: typing.Optional[str]
|
||||
phone: typing.Optional[str]
|
||||
roles: typing.Optional[typing.List[str]]
|
||||
|
||||
@property
|
||||
def has_admin_role(self) -> bool:
|
||||
return bool(self.roles and "prosody:admin" in self.roles)
|
||||
|
||||
@property
|
||||
def has_restricted_role(self) -> bool:
|
||||
return bool(self.roles and "prosody:restricted" in self.roles)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
@@ -55,6 +64,7 @@ class AdminUserInfo:
|
||||
display_name=data.get("display_name") or None,
|
||||
email=data.get("email") or None,
|
||||
phone=data.get("phone") or None,
|
||||
roles=data.get("roles"),
|
||||
)
|
||||
|
||||
|
||||
@@ -500,7 +510,7 @@ class ProsodyClient:
|
||||
"to": self.session_address,
|
||||
}
|
||||
|
||||
async with session.post(self._rest_endpoint, data=req) as resp:
|
||||
async with session.post(self._rest_endpoint, json=req) as resp:
|
||||
return resp.status == 200
|
||||
|
||||
@autosession
|
||||
@@ -509,11 +519,11 @@ class ProsodyClient:
|
||||
req = {
|
||||
"kind": "iq",
|
||||
"type": "get",
|
||||
"version": True,
|
||||
"version": {},
|
||||
"to": domain,
|
||||
}
|
||||
|
||||
async with session.post(self._rest_endpoint, data=req) as resp:
|
||||
async with session.post(self._rest_endpoint, json=req) as resp:
|
||||
if resp.status != 200:
|
||||
return "unknwn"
|
||||
try:
|
||||
@@ -858,6 +868,29 @@ class ProsodyClient:
|
||||
self._raise_error_from_response(resp)
|
||||
return AdminUserInfo.from_api_response(await resp.json())
|
||||
|
||||
@autosession
|
||||
async def update_user(
|
||||
self,
|
||||
localpart: str,
|
||||
*,
|
||||
display_name: typing.Optional[str],
|
||||
roles: typing.Optional[typing.Collection[str]],
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
payload: typing.Dict[str, typing.Any] = {
|
||||
"username": localpart,
|
||||
}
|
||||
if display_name is not None:
|
||||
payload["display_name"] = display_name
|
||||
if roles is not None:
|
||||
payload["roles"] = list(roles)
|
||||
|
||||
async with session.put(
|
||||
self._admin_v1_endpoint("/users/{}".format(localpart)),
|
||||
json=payload,
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def get_user_debug_info(
|
||||
self,
|
||||
@@ -1142,3 +1175,41 @@ class ProsodyClient:
|
||||
json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
return (await resp.json())["jid"]
|
||||
|
||||
@autosession
|
||||
async def get_system_metrics(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> typing.Mapping:
|
||||
async with session.get(
|
||||
self._admin_v1_endpoint("/server/metrics"),
|
||||
) as resp:
|
||||
if resp.status == 404:
|
||||
return {}
|
||||
self._raise_error_from_response(resp)
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
||||
@autosession
|
||||
async def post_announcement(
|
||||
self,
|
||||
body: str,
|
||||
recipients: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
recipients_payload: typing.Union[str, typing.Sequence[str]]
|
||||
if recipients == "self":
|
||||
recipients_payload = [self.session_address]
|
||||
else:
|
||||
recipients_payload = recipients
|
||||
|
||||
payload = {
|
||||
"recipients": recipients_payload,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
self._admin_v1_endpoint("/server/announcement"),
|
||||
json=payload) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
resp.raise_for_status()
|
||||
|
||||
@@ -354,6 +354,15 @@ div.form.layout-expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.radio-button-ext > label > p {
|
||||
margin-left: 1.75rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.radio-button-ext > label .icon {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
div.select-wrap {
|
||||
display: block;
|
||||
border-bottom: $w-s4 solid $primary-500;
|
||||
|
||||
@@ -37,6 +37,11 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
|
||||
</symbol>
|
||||
<!-- from: action/lock/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-lock" viewBox="0 0 24 24">
|
||||
<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: communication/qr_code/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-qrcode" viewBox="0 0 24 24">
|
||||
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
|
||||
@@ -47,6 +52,12 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
|
||||
</symbol>
|
||||
<!-- from: communication/rss_feed/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-broadcast" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<circle cx="6.18" cy="17.82" r="2.18" />
|
||||
<path d="M5.59 10.23c-.84-.14-1.59.55-1.59 1.4 0 .71.53 1.28 1.23 1.4 2.92.51 5.22 2.82 5.74 5.74.12.7.69 1.23 1.4 1.23.85 0 1.54-.75 1.41-1.59-.68-4.2-3.99-7.51-8.19-8.18zm-.03-5.71C4.73 4.43 4 5.1 4 5.93c0 .73.55 1.33 1.27 1.4 6.01.6 10.79 5.38 11.39 11.39.07.73.67 1.28 1.4 1.28.84 0 1.5-.73 1.42-1.56-.73-7.34-6.57-13.19-13.92-13.92z" />
|
||||
</symbol>
|
||||
<!-- from: content/add_circle_outline/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-add" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -72,6 +83,11 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M21.94 11.23C21.57 8.76 19.32 7 16.82 7h-2.87c-.52 0-.95.43-.95.95s.43.95.95.95h2.9c1.6 0 3.04 1.14 3.22 2.73.17 1.43-.64 2.69-1.85 3.22l1.4 1.4c1.63-1.02 2.64-2.91 2.32-5.02zM4.12 3.56c-.39-.39-1.02-.39-1.41 0s-.39 1.02 0 1.41l2.4 2.4c-1.94.8-3.27 2.77-3.09 5.04C2.23 15.05 4.59 17 7.23 17h2.82c.52 0 .95-.43.95-.95s-.43-.95-.95-.95H7.16c-1.63 0-3.1-1.19-3.25-2.82-.15-1.72 1.11-3.17 2.75-3.35l2.1 2.1c-.43.09-.76.46-.76.92v.1c0 .52.43.95.95.95h1.78L13 15.27V17h1.73l3.3 3.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.12 3.56zM16 11.95c0-.52-.43-.95-.95-.95h-.66l1.49 1.49c.07-.13.12-.28.12-.44v-.1z" />
|
||||
</symbol>
|
||||
<!-- from: content/send/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-send" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3.4 20.4l17.45-7.48c.81-.35.81-1.49 0-1.84L3.4 3.6c-.66-.29-1.39.2-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z" />
|
||||
</symbol>
|
||||
<!-- from: navigation/arrow_back/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-back" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -137,4 +153,9 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" />
|
||||
</symbol>
|
||||
<!-- from: content/insights/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-insights" viewBox="0 0 24 24">
|
||||
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
|
||||
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
|
||||
</symbol>
|
||||
</defs></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
@@ -16,7 +16,7 @@
|
||||
<p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p>
|
||||
{% endcall %}
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".index"), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for(".edit_user", localpart=target_user.localpart), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
|
||||
</div>
|
||||
</form></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button %}
|
||||
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button, icon %}
|
||||
{% block head_lead %}
|
||||
{{ super() }}
|
||||
{% include "copy-snippet.html" %}
|
||||
@@ -40,8 +40,8 @@
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".circles"), class="secondary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
|
||||
{% trans %}Return to circle list{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
@@ -56,14 +56,21 @@
|
||||
{%- if circle_members -%}
|
||||
<div class="el-2 elevated"><table>
|
||||
<thead>
|
||||
<th>Login name</th>
|
||||
<th class="collapsible">Display name</th>
|
||||
<th>Actions</th>
|
||||
<th>{% trans %}Login name{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Display name{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for member in circle_members -%}
|
||||
{%- for localpart, member in circle_members -%}
|
||||
<tr>
|
||||
<td>{{ member.localpart }}</td>
|
||||
<td>
|
||||
{%- if member -%}
|
||||
{{ localpart }}
|
||||
{%- 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) -%}
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
<dd>{{ invite.created_at | format_date }}</dd>
|
||||
</dl>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for(".invitations"), class="primary") %}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- call standard_button("back", url_for(".invitations"), class="tertiary") %}
|
||||
{% trans %}Return to invitation list{% endtrans %}
|
||||
{%- endcall %}
|
||||
{%- call form_button("remove_link", form.action_revoke, class="primary danger") %}{% endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
78
snikket_web/templates/admin_edit_user.html
Normal file
78
snikket_web/templates/admin_edit_user.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import box, form_button, standard_button, icon %}
|
||||
{% macro access_level_description(role, caller=None) %}
|
||||
{%- if role == "prosody:restricted" -%}
|
||||
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
|
||||
{%- elif role == "prosody:normal" -%}
|
||||
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
|
||||
{%- elif role == "prosody:admin" -%}
|
||||
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
{% macro access_level_icon(role, caller=None) %}
|
||||
{%- if role == "prosody:restricted" -%}
|
||||
{% call icon("lock") %}{% endcall %}
|
||||
{%- elif role == "prosody:admin" -%}
|
||||
{% call icon("admin") %}{% endcall %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
{% 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">
|
||||
<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 }}
|
||||
</div>
|
||||
<h3 class="form-title">{% trans %}Access Level{% endtrans %}</h3>
|
||||
<p class="form-descr weak">{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
<fieldset>{#- -#}
|
||||
<legend class="a11y-only">{{ form.role.label.text }}</legend>
|
||||
{%- for level in form.role -%}
|
||||
<div class="radio-button-ext">
|
||||
{{ level }}<label for="{{ level.id }}">
|
||||
{%- trans title=level.label.text, icon=access_level_icon(level.data), description=access_level_description(level.data) -%}
|
||||
<strong>{{ title }}{{ icon }}</strong><p>{{ description }}</p>
|
||||
{%- endtrans -%}
|
||||
</label>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".users"), class="tertiary") -%}
|
||||
{%- trans -%}Return to user list{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- call standard_button("delete", url_for(".delete_user", localpart=target_user.localpart), class="secondary") -%}
|
||||
{%- trans -%}Delete user{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{% trans %}Further actions{% endtrans %}</h2>
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans %}Reset password{% endtrans %}</h2>
|
||||
{{ form.csrf_token }}
|
||||
<p class="form-desc">
|
||||
{% 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 -%}
|
||||
</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") -%}
|
||||
{%- trans -%}Show debug information{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
@@ -31,6 +31,18 @@
|
||||
<div>{% call standard_button("link", url_for(".invitations"), class="primary") %}{% trans %}Manage invitations{% endtrans %}{% endcall %}</div>
|
||||
{#- -#}
|
||||
</li>
|
||||
<li>
|
||||
<h2>{% trans %}System health{% endtrans %}</h2>
|
||||
{#- -#}
|
||||
{%- if show_metrics -%}
|
||||
<p>{% trans %}View the server status or send a broadcast message to all users.{% endtrans %}</p>
|
||||
{%- else -%}
|
||||
<p>{% trans %}Send a broadcast message to all users.{% endtrans %}</p>
|
||||
{%- endif -%}
|
||||
{#- -#}
|
||||
<div>{% call standard_button("insights", url_for(".system"), class="primary") %}{% trans %}Manage system{% endtrans %}{% endcall %}</div>
|
||||
{#- -#}
|
||||
</li>
|
||||
<li>
|
||||
{#- -#}
|
||||
<p>{% trans %}Go back to your user's web portal page.{% endtrans %}</p>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form method="POST">
|
||||
{{- form.csrf_token -}}
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
|
||||
<h2 class="form-title">{% trans user_name=localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
|
||||
<p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
|
||||
<dd>
|
||||
<dt>{% trans %}Valid until{% endtrans %}</dt>
|
||||
@@ -21,7 +21,7 @@
|
||||
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
|
||||
{% trans %}Destroy link{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call standard_button("back", url_for(".users"), class="primary") -%}
|
||||
{%- call standard_button("back", url_for(".edit_user", localpart=localpart), class="primary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
|
||||
97
snikket_web/templates/admin_system.html
Normal file
97
snikket_web/templates/admin_system.html
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import form_button %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage system{% endtrans %}</h1>
|
||||
{% if show_metrics %}
|
||||
<h2>{% trans %}Overall system status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}System load (5 minute average){% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.load5 -%}
|
||||
{{ metrics.load5 }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.mem_total and metrics.mem_available -%}
|
||||
{% trans percentage_global=((1 - (metrics.mem_available / metrics.mem_total)) | format_percent), percentage_snikket=((((metrics.prosody_rss | default(0)) + (metrics.portal_rss | default(0))) / metrics.mem_total) | format_percent), mem_available=(metrics.mem_total | format_bytes) %}{{ percentage_global }} of {{ mem_available }}. Of that, Snikket uses {{ percentage_snikket }}.{% endtrans %}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<h2>{% trans %}Web portal status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}Version{% endtrans %}</dt>
|
||||
<dd>{{ version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
|
||||
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.portal_cpu -%}
|
||||
{{ metrics.portal_cpu | format_percent }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Current memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.portal_rss -%}
|
||||
{{ metrics.portal_rss | format_bytes }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<h2>{% trans %}Snikket server status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}Version{% endtrans %}</dt>
|
||||
<dd>{{ prosody_version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
|
||||
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_cpu -%}
|
||||
{{ metrics.prosody_cpu | format_percent }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Current memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_rss -%}
|
||||
{{ metrics.prosody_rss | format_bytes }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Connected devices{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_devices | default(None) is not none -%}
|
||||
{{ metrics.prosody_devices }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>{% trans %}Broadcast message{% endtrans %}</h2>
|
||||
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
|
||||
<p class="form-desc">{% trans %}This form allows you to send a message to all users currently online on your Snikket server. Use it wisely.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
{{ form.text.label }}
|
||||
{{ form.text }}
|
||||
</div>
|
||||
<div class="f-ebox">
|
||||
{{ form.online_only }}{{ form.online_only.label }}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("send", form.action_send_preview, class="primary") -%}{%- endcall -%}
|
||||
{%- call form_button("broadcast", form.action_post_all, class="secondary accent") -%}{%- endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import action_button, value_or_hint, custom_form_button %}
|
||||
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage users{% endtrans %}</h1>
|
||||
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
|
||||
{{- reset_form.csrf_token -}}
|
||||
<div class="elevated el-2"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -15,17 +13,19 @@
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.localpart }}</td>
|
||||
<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 -%}
|
||||
</td>
|
||||
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
|
||||
<td class="nowrap">
|
||||
{%- call action_button("delete", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
|
||||
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%}
|
||||
{% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call custom_form_button("passwd", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%}
|
||||
{% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %}
|
||||
{%- 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>
|
||||
@@ -33,5 +33,5 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</form>
|
||||
{%- include "admin_create_invite_form.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
{% trans %}Copy address{% endtrans %}
|
||||
{%- endcall -%}
|
||||
<p>{% trans %}You can now set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}</p>
|
||||
<p>{% trans %}You can now safely close this page.{% endtrans %}</p>
|
||||
<p>{% trans login_url=url_for('main.login') %}You can now safely close this page, or log in to the web portal to <a href="{{ login_url }}">manage your account</a>.{% endtrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
{#- -#}
|
||||
<div id="qr-info-url" class="tab-pane active">
|
||||
<p>{% trans %}Use a <em>QR code</em> scanner on your mobile device to scan the code below:{% endtrans %}</p>
|
||||
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True) }}" class="qr"></div>
|
||||
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True, _scheme="https") }}" class="qr"></div>
|
||||
</div>
|
||||
{#- -#}
|
||||
<div id="qr-info-uri" class="tab-pane">
|
||||
@@ -96,9 +96,9 @@
|
||||
{% trans %}Close{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</header>
|
||||
<p>{% trans %}After downloading Snikket from the app store, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<p>{% trans %}After downloading Snikket from the App Store, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<ol>
|
||||
<li><p>{% trans %}First download Snikket from the app store using the button below:{% endtrans %}</p>
|
||||
<li><p>{% trans %}First download Snikket from the App Store using the button below:{% endtrans %}</p>
|
||||
<p><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></p>
|
||||
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
|
||||
<p>
|
||||
@@ -127,7 +127,7 @@
|
||||
<p>{% trans %}After installing Snikket via F-Droid, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<ol>
|
||||
<li><p>{% trans %}First install Snikket from F-Droid using the button below:{% endtrans %}</p>
|
||||
<p><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<p><a href="{{ f_droid_url }}"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
|
||||
<p>
|
||||
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p class="form-desc">{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}</p>
|
||||
{{ form.csrf_token }}
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for("user.index"), class="secondary") -%}
|
||||
{%- call standard_button("back", url_for("user.index"), class="tertiary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<p>{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call custom_form_button("passwd", "", "", class="primary") -%}
|
||||
{% trans %}Change password{% endtrans %}
|
||||
{%- endcall -%}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
BIN
snikket_web/translations/da/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/da/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/de/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/de/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/en/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/en/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/en_GB/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/en_GB/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/es_MX/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/es_MX/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/fr/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/fr/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/id/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/id/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/it/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/it/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,144 +8,208 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-03-20 16:27+0100\n"
|
||||
"POT-Creation-Date: 2021-06-18 16:05+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
|
||||
#: snikket_web/admin.py:59
|
||||
msgid "Delete user permanently"
|
||||
#: snikket_web/admin.py:66
|
||||
msgid "Limited"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:72
|
||||
msgid "User deleted"
|
||||
#: snikket_web/admin.py:71 snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_edit_circle.html:59
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:115
|
||||
#: snikket_web/admin.py:75 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_edit_circle.html:60
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:79 snikket_web/templates/admin_edit_user.html:32
|
||||
msgid "Access Level"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:84
|
||||
msgid "Normal user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:85
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:90
|
||||
msgid "Update user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:94
|
||||
msgid "Create password reset link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:112
|
||||
msgid "Password reset link created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:121
|
||||
#: snikket_web/admin.py:127
|
||||
msgid "User information updated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:149
|
||||
msgid "Delete user permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:162
|
||||
msgid "User deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:200
|
||||
msgid "Password reset link not found"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:212
|
||||
msgid "Password reset link deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:140
|
||||
#: snikket_web/admin.py:232
|
||||
msgid "Invite to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:146
|
||||
#: snikket_web/admin.py:238
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:151
|
||||
#: snikket_web/admin.py:243
|
||||
msgid "Valid for"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:153
|
||||
#: snikket_web/admin.py:245
|
||||
msgid "One hour"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:154
|
||||
#: snikket_web/admin.py:246
|
||||
msgid "Twelve hours"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:155
|
||||
#: snikket_web/admin.py:247
|
||||
msgid "One day"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:156
|
||||
#: snikket_web/admin.py:248
|
||||
msgid "One week"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:157
|
||||
#: snikket_web/admin.py:249
|
||||
msgid "Four weeks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:163 snikket_web/templates/admin_edit_invite.html:17
|
||||
#: snikket_web/admin.py:255 snikket_web/templates/admin_edit_invite.html:17
|
||||
msgid "Invitation type"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:165 snikket_web/templates/library.j2:116
|
||||
#: snikket_web/admin.py:257 snikket_web/templates/library.j2:116
|
||||
msgid "Individual"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:114
|
||||
#: snikket_web/admin.py:258 snikket_web/templates/library.j2:114
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:172
|
||||
#: snikket_web/admin.py:264
|
||||
msgid "New invitation link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:234
|
||||
#: snikket_web/admin.py:326
|
||||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:258
|
||||
#: snikket_web/admin.py:350
|
||||
msgid "Invitation created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:274
|
||||
#: snikket_web/admin.py:366
|
||||
msgid "No such invitation exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:289
|
||||
#: snikket_web/admin.py:381
|
||||
msgid "Invitation revoked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:306 snikket_web/admin.py:354
|
||||
#: snikket_web/admin.py:398 snikket_web/admin.py:446
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:311 snikket_web/templates/admin_circles.html:47
|
||||
#: snikket_web/admin.py:403 snikket_web/templates/admin_circles.html:47
|
||||
msgid "Create circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:341
|
||||
#: snikket_web/admin.py:433
|
||||
msgid "Circle created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:359
|
||||
#: snikket_web/admin.py:451
|
||||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:364
|
||||
#: snikket_web/admin.py:456
|
||||
msgid "Update circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:368
|
||||
#: snikket_web/admin.py:460
|
||||
msgid "Delete circle permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:374
|
||||
#: snikket_web/admin.py:466
|
||||
msgid "Add user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:390
|
||||
#: snikket_web/admin.py:482
|
||||
msgid "No such circle exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:427
|
||||
#: snikket_web/admin.py:519
|
||||
msgid "Circle data updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:433
|
||||
#: snikket_web/admin.py:525
|
||||
msgid "Circle deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:444
|
||||
#: snikket_web/admin.py:536
|
||||
msgid "User added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:453
|
||||
#: snikket_web/admin.py:545
|
||||
msgid "User removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:41
|
||||
#: snikket_web/admin.py:616
|
||||
msgid "Message contents"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:622
|
||||
msgid "Only send to online users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:626
|
||||
msgid "Post to all users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:630
|
||||
msgid "Send preview to yourself"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:652
|
||||
msgid "Announcement sent!"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:51
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
@@ -162,7 +226,7 @@ msgid "Confirm password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:118 snikket_web/invite.py:185
|
||||
msgid "The passwords must match"
|
||||
msgid "The passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:123
|
||||
@@ -215,7 +279,7 @@ msgid "Confirm new password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:41
|
||||
msgid "The new passwords must match"
|
||||
msgid "The new passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:48
|
||||
@@ -234,11 +298,6 @@ msgstr ""
|
||||
msgid "Everyone"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:65
|
||||
msgid "Avatar"
|
||||
msgstr ""
|
||||
@@ -383,8 +442,9 @@ msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:15
|
||||
#: snikket_web/templates/admin_edit_circle.html:61
|
||||
#: snikket_web/templates/admin_invites.html:24
|
||||
#: snikket_web/templates/admin_users.html:12
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
@@ -455,12 +515,12 @@ msgid "Copy complete output"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:4
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
#, python-format
|
||||
msgid "Delete user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:53
|
||||
msgid "Delete user"
|
||||
msgstr ""
|
||||
|
||||
@@ -468,11 +528,6 @@ msgstr ""
|
||||
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
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:15
|
||||
msgid "Danger"
|
||||
msgstr ""
|
||||
@@ -485,8 +540,6 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:19
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
#: snikket_web/templates/admin_edit_invite.html:49
|
||||
#: snikket_web/templates/admin_reset_user_password.html:25
|
||||
#: snikket_web/templates/user_logout.html:10
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
@@ -522,6 +575,10 @@ msgstr ""
|
||||
msgid "This circle has no group chat associated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
msgid "Return to circle list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:48
|
||||
msgid "Delete circle"
|
||||
msgstr ""
|
||||
@@ -534,28 +591,37 @@ msgstr ""
|
||||
msgid "Circle members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:70
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
msgid "The user has been deleted from the server."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
#: snikket_web/templates/library.j2:108
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:77
|
||||
#, python-format
|
||||
msgid "Remove user %(username)s from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:78
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
msgid "This circle currently has no members."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:80
|
||||
#: snikket_web/templates/admin_edit_circle.html:87
|
||||
msgid "Invite more members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:83
|
||||
#: snikket_web/templates/admin_edit_circle.html:90
|
||||
msgid "Add existing user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:94
|
||||
#: snikket_web/templates/admin_edit_circle.html:101
|
||||
msgid "All users added"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:95
|
||||
#: snikket_web/templates/admin_edit_circle.html:102
|
||||
msgid "All users on this service are already in this circle."
|
||||
msgstr ""
|
||||
|
||||
@@ -604,6 +670,85 @@ msgstr ""
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:48
|
||||
msgid "Return to invitation list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:5
|
||||
msgid ""
|
||||
"Limited users can interact with users on the same Snikket service and be "
|
||||
"members of circles."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:7
|
||||
msgid ""
|
||||
"Like limited users and can also interact with users on other Snikket "
|
||||
"services."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:9
|
||||
msgid "Like normal users and can access the admin panel in the web portal."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:20
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Edit user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:22
|
||||
msgid "Edit user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:26
|
||||
msgid "The login name cannot be changed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:33
|
||||
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
|
||||
#, python-format
|
||||
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:50
|
||||
msgid "Return to user list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:58
|
||||
msgid "Further actions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:60
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:63
|
||||
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
|
||||
msgid "Debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:70
|
||||
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
|
||||
msgid "Show debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:4
|
||||
msgid "Welcome to the admin panel!"
|
||||
msgstr ""
|
||||
@@ -643,11 +788,28 @@ msgstr ""
|
||||
msgid "Manage invitations"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:36
|
||||
msgid "Go back to your user's web portal page."
|
||||
#: snikket_web/templates/admin_home.html:35
|
||||
msgid "System health"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:38
|
||||
msgid "View the server status or send a broadcast message to all users."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:40
|
||||
msgid "Send a broadcast message to all users."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:43
|
||||
#: snikket_web/templates/admin_system.html:4
|
||||
msgid "Manage system"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:48
|
||||
msgid "Go back to your user's web portal page."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:50
|
||||
msgid "Exit admin panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -698,14 +860,91 @@ msgstr ""
|
||||
msgid "Destroy link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:25
|
||||
#, python-format
|
||||
msgid "Show debug information for %(user_name)s"
|
||||
#: snikket_web/templates/admin_system.html:6
|
||||
msgid "Overall system status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#: snikket_web/templates/admin_system.html:9
|
||||
msgid "System load (5 minute average)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:14
|
||||
#: snikket_web/templates/admin_system.html:22
|
||||
#: snikket_web/templates/admin_system.html:37
|
||||
#: snikket_web/templates/admin_system.html:45
|
||||
#: snikket_web/templates/admin_system.html:60
|
||||
#: snikket_web/templates/admin_system.html:68
|
||||
#: snikket_web/templates/admin_system.html:76
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:17
|
||||
msgid "Memory use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:20
|
||||
#, python-format
|
||||
msgid "Create password reset link for %(user_name)s"
|
||||
msgid ""
|
||||
"%(percentage_global)s of %(mem_available)s. Of that, Snikket uses "
|
||||
"%(percentage_snikket)s."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:27
|
||||
msgid "Web portal status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:30
|
||||
#: snikket_web/templates/admin_system.html:53
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:31
|
||||
#: snikket_web/templates/admin_system.html:54
|
||||
msgid "View all versions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:32
|
||||
#: snikket_web/templates/admin_system.html:55
|
||||
msgid "Average CPU use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:40
|
||||
#: snikket_web/templates/admin_system.html:63
|
||||
msgid "Current memory use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:50
|
||||
msgid "Snikket server status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:71
|
||||
msgid "Connected devices"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:82
|
||||
msgid "Broadcast message"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:84
|
||||
msgid ""
|
||||
"This form allows you to send a message to all users currently online on "
|
||||
"your Snikket server. Use it wisely."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid "The user is an administrator."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid " (Administrator)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid "The user is restricted."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
@@ -1048,12 +1287,12 @@ msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
msgid ""
|
||||
"After downloading Snikket from the app store, you have to return to this "
|
||||
"After downloading Snikket from the App Store, you have to return to this "
|
||||
"invite link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:101
|
||||
msgid "First download Snikket from the app store using the button below:"
|
||||
msgid "First download Snikket from the App Store using the button below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:103
|
||||
@@ -1086,10 +1325,6 @@ msgstr ""
|
||||
msgid "Invalid input"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:108
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:122
|
||||
msgid "Can be used multiple times to create accounts on this Snikket service."
|
||||
msgstr ""
|
||||
|
||||
BIN
snikket_web/translations/pl/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/pl/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/sv/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/sv/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ class ChangePasswordForm(BaseForm):
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"new_password",
|
||||
_l("The new passwords must match")
|
||||
_l("The new passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ action/delete:delete
|
||||
action/logout:logout
|
||||
action/login:login
|
||||
action/exit_to_app:exit_to_app
|
||||
action/lock:lock
|
||||
communication/qr_code:qrcode
|
||||
communication/vpn_key:passwd
|
||||
communication/rss_feed:broadcast
|
||||
content/add_circle_outline:add
|
||||
content/add_link:create_link
|
||||
content/remove_circle_outline:remove
|
||||
content/content_copy:copy
|
||||
content/link_off:remove_link
|
||||
content/send:send
|
||||
navigation/arrow_back:back
|
||||
navigation/arrow_forward:forward
|
||||
navigation/cancel:cancel
|
||||
@@ -25,3 +28,4 @@ navigation/close:close
|
||||
image/edit:edit
|
||||
action/admin_panel_settings:admin
|
||||
content/link:link
|
||||
content/insights:insights
|
||||
|
||||
Reference in New Issue
Block a user