You've already forked snikket-web-portal
Compare commits
135 Commits
feature/mu
...
beta.20240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f41aa24d8 | ||
|
|
15516cdaa5 | ||
|
|
948e415dbd | ||
|
|
a3fcf7d1d4 | ||
|
|
65de73f1fe | ||
|
|
989fe7b5b6 | ||
|
|
4bc929e1ce | ||
|
|
5817b24c48 | ||
|
|
550526efc9 | ||
|
|
2a2e36ade2 | ||
|
|
22f7d6f36a | ||
|
|
2d42099017 | ||
|
|
2ff47c486a | ||
|
|
338ee0b278 | ||
|
|
64c6548a48 | ||
|
|
8c824149cc | ||
|
|
607863cfc4 | ||
|
|
13c5d44544 | ||
|
|
6407eb90db | ||
|
|
a8c6b1a70c | ||
|
|
67c94bb045 | ||
|
|
f4c1173a34 | ||
|
|
e39b0082b1 | ||
|
|
9eb187a951 | ||
|
|
b928e74a74 | ||
|
|
75c0f504d0 | ||
|
|
7c0310a141 | ||
|
|
5e2e645787 | ||
|
|
9b31894e85 | ||
|
|
a4472e1a44 | ||
|
|
b99cae84de | ||
|
|
1cac19e4c9 | ||
|
|
d4883765b2 | ||
|
|
041f26274b | ||
|
|
82db30ffd9 | ||
|
|
b8684329b4 | ||
|
|
7e26b5f994 | ||
|
|
4bdcb46a8a | ||
|
|
ed6f413c18 | ||
|
|
f63549ee87 | ||
|
|
bd71ab1449 | ||
|
|
220bf9994b | ||
|
|
33d28e5890 | ||
|
|
f0f0fa15c9 | ||
|
|
30a9a6816f | ||
|
|
970b8fa7f1 | ||
|
|
629d725ff5 | ||
|
|
6998e66b22 | ||
|
|
c668c4c56a | ||
|
|
a13fbd87a6 | ||
|
|
7ffcd76cea | ||
|
|
bda0f52320 | ||
|
|
5efc2a671e | ||
|
|
1578654816 | ||
|
|
e8ab33e12f | ||
|
|
712b0dc502 | ||
|
|
e56c0f9029 | ||
|
|
794b48a50b | ||
|
|
393b30cf5c | ||
|
|
97198a1da4 | ||
|
|
3ba1195fbe | ||
|
|
121f3eddb5 | ||
|
|
38ad81b0e2 | ||
|
|
ec94c64dbc | ||
|
|
28a9a33aa1 | ||
|
|
97eeb85032 | ||
|
|
ceef9f024c | ||
|
|
40c8b9cc36 | ||
|
|
95a8ac1387 | ||
|
|
4c6e26e66b | ||
|
|
ad2b351a99 | ||
|
|
3bda1f9863 | ||
|
|
f46d95db66 | ||
|
|
ddfdd2fd55 | ||
|
|
17d586e384 | ||
|
|
dbec07d149 | ||
|
|
ebf142b505 | ||
|
|
0539d0ab88 | ||
|
|
2736bff76b | ||
|
|
192601f387 | ||
|
|
bc9cfeabab | ||
|
|
b770086071 | ||
|
|
b2c1fdd23b | ||
|
|
906978556e | ||
|
|
274c8e4658 | ||
|
|
257a44dac2 | ||
|
|
f393a3980b | ||
|
|
badff7eed8 | ||
|
|
384e07c2a9 | ||
|
|
89724a9712 | ||
|
|
94f4325f40 | ||
|
|
af1285b650 | ||
|
|
52eba53d8e | ||
|
|
94f240687a | ||
|
|
1b2bdfa881 | ||
|
|
271f450c86 | ||
|
|
6186e8b635 | ||
|
|
dfc6c392c3 | ||
|
|
0ec9a2ae02 | ||
|
|
09fcf64818 | ||
|
|
c25db5c3ae | ||
|
|
c85fff7581 | ||
|
|
039f4b8210 | ||
|
|
7be7ee67c2 | ||
|
|
6f5fc14dbc | ||
|
|
65edd3a52b | ||
|
|
ab7149403a | ||
|
|
5b2f3db867 | ||
|
|
e12941eab0 | ||
|
|
eda3f4826c | ||
|
|
61161eb472 | ||
|
|
325826c19b | ||
|
|
587839f852 | ||
|
|
7411f4a9e1 | ||
|
|
d63ae4768a | ||
|
|
92a8da724f | ||
|
|
ea3a081b6c | ||
|
|
0647ba2601 | ||
|
|
2769036f94 | ||
|
|
c76befad1c | ||
|
|
74ecfb8653 | ||
|
|
55b195cd7f | ||
|
|
46a7d0c37d | ||
|
|
c63b95c6e0 | ||
|
|
6848691141 | ||
|
|
1e83881a24 | ||
|
|
35e6bec328 | ||
|
|
d345f0d98d | ||
|
|
f5ccb7d858 | ||
|
|
f7c8bccfa2 | ||
|
|
e5d06877a4 | ||
|
|
e7ed9dd176 | ||
|
|
6778557db8 | ||
|
|
73f3f25515 | ||
|
|
bd66600d05 |
4
.github/workflows/main.yaml
vendored
4
.github/workflows/main.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
aiohttp~=3.6
|
||||
quart~=0.17,<0.18
|
||||
flask-wtf~=1.0
|
||||
aiohttp~=3.8,<3.9
|
||||
quart~=0.18,<0.19
|
||||
flask-wtf~=1.1,<1.2
|
||||
hsluv~=5.0
|
||||
flask-babel~=1.0
|
||||
email-validator~=1.1
|
||||
flask-babel~=2.0,<3
|
||||
email-validator~=1.3
|
||||
environ-config~=20.0
|
||||
wtforms~=3.0
|
||||
wtforms~=3.0,<4
|
||||
typing-extensions
|
||||
werkzeug~=2.2,<3
|
||||
|
||||
@@ -158,7 +158,9 @@ class AppConfig:
|
||||
"id",
|
||||
"it",
|
||||
"pl",
|
||||
"ru",
|
||||
"sv",
|
||||
"uk",
|
||||
"zh_Hans_CN",
|
||||
], converter=autosplit)
|
||||
apple_store_url = environ.var(
|
||||
@@ -210,6 +212,8 @@ def create_app() -> quart.Quart:
|
||||
app.config["PRIVACY_URI"] = config.privacy_uri
|
||||
app.config["ABUSE_EMAIL"] = config.abuse_email
|
||||
app.config["SECURITY_EMAIL"] = config.security_email
|
||||
app.config["SESSION_COOKIE_SECURE"] = True
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
|
||||
app.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
|
||||
@@ -76,16 +76,25 @@ 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")),
|
||||
],
|
||||
default="prosody:registered",
|
||||
)
|
||||
|
||||
action_save = wtforms.SubmitField(
|
||||
_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 +121,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 +158,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
|
||||
@@ -256,6 +291,20 @@ class InvitePost(BaseForm):
|
||||
default="account",
|
||||
)
|
||||
|
||||
role = wtforms.RadioField(
|
||||
_l("Access Level"),
|
||||
choices=[
|
||||
("prosody:restricted", _l("Limited")),
|
||||
("prosody:registered", _l("Normal user")),
|
||||
("prosody:admin", _l("Administrator")),
|
||||
],
|
||||
default="prosody:registered",
|
||||
)
|
||||
|
||||
note = wtforms.StringField(
|
||||
_l("Comment (optional)"),
|
||||
)
|
||||
|
||||
action_create_invite = wtforms.SubmitField(
|
||||
_l("New invitation link")
|
||||
)
|
||||
@@ -335,12 +384,16 @@ async def create_invite() -> typing.Union[str, werkzeug.Response]:
|
||||
if form.type_.data == "group":
|
||||
invite = await client.create_group_invite(
|
||||
group_ids=form.circles.data,
|
||||
role_names=[form.role.data],
|
||||
ttl=form.lifetime.data,
|
||||
note=form.note.data,
|
||||
)
|
||||
else:
|
||||
invite = await client.create_account_invite(
|
||||
group_ids=form.circles.data,
|
||||
role_names=[form.role.data],
|
||||
ttl=form.lifetime.data,
|
||||
note=form.note.data,
|
||||
)
|
||||
await flash(
|
||||
_("Invitation created"),
|
||||
@@ -699,21 +752,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"),
|
||||
)
|
||||
|
||||
|
||||
@@ -778,6 +831,11 @@ async def system() -> typing.Union[str, werkzeug.Response]:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
metrics["users"] = prosody_metrics["users"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for k in list(metrics.keys()):
|
||||
if metrics[k] is None:
|
||||
# so that defaulting in jinja works
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
@@ -69,12 +120,26 @@ class AdminUserInfo:
|
||||
roles.extend(data.get("secondary_roles", []))
|
||||
except KeyError:
|
||||
roles = data.get("roles")
|
||||
avatar_info: typing.List[AvatarMetadata] = []
|
||||
for avatar in data.get("avatar_info", []):
|
||||
# Ignore somehow broken avatars.
|
||||
try:
|
||||
avatar_metadata = AvatarMetadata.from_api_response(avatar)
|
||||
avatar_info.append(avatar_metadata)
|
||||
except KeyError:
|
||||
pass
|
||||
return cls(
|
||||
localpart=data["username"],
|
||||
display_name=data.get("display_name") or None,
|
||||
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=avatar_info,
|
||||
)
|
||||
|
||||
|
||||
@@ -95,7 +160,9 @@ class AdminInviteInfo:
|
||||
expires: datetime
|
||||
reusable: bool
|
||||
group_ids: typing.Collection[str]
|
||||
role_names: typing.Collection[str]
|
||||
is_reset: bool
|
||||
note: typing.Optional[str]
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
@@ -112,8 +179,10 @@ class AdminInviteInfo:
|
||||
xmpp_uri=data.get("xmpp_uri"),
|
||||
landing_page=data.get("landing_page"),
|
||||
group_ids=data.get("groups", []),
|
||||
role_names=data.get("roles", []),
|
||||
reusable=data["reusable"],
|
||||
is_reset=data.get("reset", False),
|
||||
note=data.get("note"),
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +200,7 @@ class AdminGroupChatInfo:
|
||||
return cls(
|
||||
id_=data["id"],
|
||||
jid=data["jid"],
|
||||
name=data["name"],
|
||||
name=data.get("name", ""),
|
||||
)
|
||||
|
||||
|
||||
@@ -340,7 +409,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 +918,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 +929,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 +994,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,
|
||||
@@ -991,16 +1090,21 @@ class ProsodyClient:
|
||||
self,
|
||||
*,
|
||||
group_ids: typing.Collection[str] = [],
|
||||
role_names: typing.Collection[str] = [],
|
||||
restrict_username: typing.Optional[str] = None,
|
||||
ttl: typing.Optional[int] = None,
|
||||
note: typing.Optional[str] = None,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> AdminInviteInfo:
|
||||
payload: typing.Dict[str, typing.Any] = {}
|
||||
payload["groups"] = list(group_ids)
|
||||
payload["roles"] = list(role_names)
|
||||
if restrict_username is not None:
|
||||
payload["username"] = restrict_username
|
||||
if ttl is not None:
|
||||
payload["ttl"] = ttl
|
||||
if note is not None:
|
||||
payload["note"] = note
|
||||
|
||||
async with session.post(
|
||||
self._admin_v1_endpoint("/invites/account"),
|
||||
@@ -1013,14 +1117,19 @@ class ProsodyClient:
|
||||
self,
|
||||
*,
|
||||
group_ids: typing.Collection[str] = [],
|
||||
role_names: typing.Collection[str] = [],
|
||||
ttl: typing.Optional[int] = None,
|
||||
note: typing.Optional[str] = None,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> AdminInviteInfo:
|
||||
payload: typing.Dict[str, typing.Any] = {
|
||||
"groups": list(group_ids),
|
||||
"roles": list(role_names),
|
||||
}
|
||||
if ttl is not None:
|
||||
payload["ttl"] = ttl
|
||||
if note is not None:
|
||||
payload["note"] = note
|
||||
|
||||
async with session.post(
|
||||
self._admin_v1_endpoint("/invites/group"),
|
||||
|
||||
@@ -259,6 +259,13 @@ div.form.layout-expanded {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset.descriptive-radio-selection {
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: $w-s2;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] + label, input[type="checkbox"] + label {
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
@@ -363,6 +370,10 @@ div.form.layout-expanded {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.radio-button-ext {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
div.select-wrap {
|
||||
display: block;
|
||||
border-bottom: $w-s4 solid $primary-500;
|
||||
@@ -708,8 +719,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;
|
||||
|
||||
@@ -982,19 +992,18 @@ div.profile-card {
|
||||
}
|
||||
}
|
||||
|
||||
/* clipboard button */
|
||||
/* clipboard and share buttons */
|
||||
|
||||
.copy-to-clipboard {
|
||||
.copy-to-clipboard, .share-button {
|
||||
cursor: pointer;
|
||||
font-style: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body.no-copy .copy-to-clipboard {
|
||||
body.no-copy .copy-to-clipboard, body.no-share .share-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* magic */
|
||||
|
||||
pre.guru-meditation {
|
||||
@@ -1068,6 +1077,10 @@ pre.guru-meditation {
|
||||
}
|
||||
}
|
||||
|
||||
label, legend {
|
||||
color: $gray-800 !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: black;
|
||||
border-color: $gray-800;
|
||||
@@ -1202,6 +1215,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 +1272,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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -138,6 +148,11 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V18c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-1.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05.02.01.03.03.04.04 1.14.83 1.93 1.94 1.93 3.41V18c0 .35-.07.69-.18 1H22c.55 0 1-.45 1-1v-1.5c0-2.33-4.67-3.5-7-3.5z" />
|
||||
</symbol>
|
||||
<!-- from: social/person/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-person" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4z" />
|
||||
</symbol>
|
||||
<!-- from: social/group_add/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-create_group" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -178,4 +193,9 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
|
||||
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
|
||||
</symbol>
|
||||
<!-- from: social/share/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-share" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z" />
|
||||
</symbol>
|
||||
</defs></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,19 +1,57 @@
|
||||
{% from "library.j2" import form_button, render_errors %}
|
||||
{% from "library.j2" import form_button,
|
||||
render_errors,
|
||||
access_level_description, access_level_icon,
|
||||
invite_type_description, invite_type_icon
|
||||
%}
|
||||
<form method="POST" action="{{ url_for(".create_invite") }}">
|
||||
{{- invite_form.csrf_token -}}
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans %}Create new invitation{% endtrans %}</h2>
|
||||
<p class="form-descr weak">{% trans %}Create a new invitation link to invite more users to your Snikket service by clicking the button below.{% endtrans %}</p>
|
||||
|
||||
<!-- Invitation type -->
|
||||
<div class="f-ebox">
|
||||
<fieldset>{#- -#}
|
||||
<fieldset class="descriptive-radio-selection">{#- -#}
|
||||
<legend>{{ invite_form.type_.label.text }}</legend>
|
||||
{{- invite_form.type_ -}}
|
||||
<p>{% trans %}Choose whether this invitation link will allow more than one person to join.{% endtrans %}</p>
|
||||
|
||||
{%- for invite_type in invite_form.type_ -%}
|
||||
<div class="radio-button-ext">
|
||||
{{ invite_type }}<label for="{{ invite_type.id }}">
|
||||
{%- trans title=invite_type.label.text, icon=invite_type_icon(invite_type.data), description=invite_type_description(invite_type.data) -%}
|
||||
<span class="invite-type">{{ title }}{{ icon }}</span><p>{{ description }}</p>
|
||||
{%- endtrans -%}
|
||||
</label>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Access level -->
|
||||
<div class="f-ebox">
|
||||
<fieldset class="descriptive-radio-selection">{#- -#}
|
||||
<legend>{{ invite_form.role.label.text }}</legend>
|
||||
<p>{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
|
||||
{%- for level in invite_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) -%}
|
||||
<span class="access-level">{{ title }}{{ icon }}</span><p>{{ description }}</p>
|
||||
{%- endtrans -%}
|
||||
</label>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Valid for -->
|
||||
<div class="f-ebox">
|
||||
{{ invite_form.lifetime.label }}
|
||||
<div class="select-wrap">{{ invite_form.lifetime }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Invite to circle -->
|
||||
<div class="f-ebox">
|
||||
{#
|
||||
NOTE: This is for when/if we ever support multi-group invites.
|
||||
@@ -27,6 +65,13 @@
|
||||
<div class="select-wrap">{{ invite_form.circles }}</div>
|
||||
{%- call render_errors(invite_form.circles) -%}{%- endcall -%}
|
||||
</div>
|
||||
|
||||
<!-- Comment -->
|
||||
<div class="f-ebox">
|
||||
{{ invite_form.note.label }}
|
||||
{{ invite_form.note }}
|
||||
</div>
|
||||
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("create_link", invite_form.action_create_invite, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_description %}
|
||||
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_name, invite_type_description %}
|
||||
{% block head_lead %}
|
||||
{{ super() }}
|
||||
{% include "copy-snippet.html" %}
|
||||
@@ -13,9 +13,10 @@
|
||||
<dt>{% trans %}Valid until{% endtrans %}</dt>
|
||||
<dd>{{ invite.expires | format_date }}</dd>
|
||||
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
|
||||
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% endcall %}</dd>
|
||||
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% trans %}Invitation to Snikket{% endtrans %}{% endcall %}</dd>
|
||||
<dt>{% trans %}Invitation type{% endtrans %}</dt>
|
||||
<dd>{% call invite_type_description(invite) %}{% endcall %}</dd>
|
||||
{% set invite_type = invite.reusable and "group" or "account" %}
|
||||
<dd><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></dd>
|
||||
{%- set ngroups = invite.group_ids | length -%}
|
||||
{%- if ngroups > 1 -%}
|
||||
{#- not supported via the web UI, but we should still display it properly -#}
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
{% 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:registered" -%}
|
||||
{% 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 %}
|
||||
{% from "library.j2" import box, form_button, standard_button, icon, access_level_description, access_level_icon %}
|
||||
{% 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 +68,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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import action_button, icon, clipboard_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
|
||||
{% from "library.j2" import action_button, icon, clipboard_button, share_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
|
||||
{% block head_lead %}
|
||||
{{ super() }}
|
||||
{% include "copy-snippet.html" %}
|
||||
@@ -18,17 +18,18 @@
|
||||
<col/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Expires{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Type{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
|
||||
<th>{% trans %}Expires{% endtrans %}</th>
|
||||
<th>{% trans %}Comment{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invite in invites %}
|
||||
{% set invite_type = invite.reusable and "group" or "account" %}
|
||||
<tr>
|
||||
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
|
||||
<td class="collapsible"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite) %}{% endcall %}">{% call invite_type_name(invite) %}{% endcall %}</span></td>
|
||||
<td class="collapsible"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></td>
|
||||
<td class="collapsible">
|
||||
{#- -#}
|
||||
<ul class="inline">
|
||||
@@ -38,6 +39,8 @@
|
||||
</ul>
|
||||
{#- -#}
|
||||
</td>
|
||||
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
|
||||
<td>{% if invite.note is not none %}{{ invite.note }}{% endif %}</td>
|
||||
<td class="nowrap">
|
||||
{%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%}
|
||||
{% trans %}Show invite details{% endtrans %}
|
||||
@@ -45,6 +48,9 @@
|
||||
{%- call clipboard_button(invite.landing_page, class="primary") -%}
|
||||
{% trans %}Copy invite link to clipboard{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call share_button("Invitation to Snikket", invite.landing_page, class="primary") -%}
|
||||
{% trans %}Share invitation link{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call custom_form_button("remove_link", form.action_revoke.name, invite.id_, class="secondary danger", slim=True) -%}
|
||||
{% trans %}Delete invitation{% endtrans %}
|
||||
{%- endcall -%}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<dt>{% trans %}Valid until{% endtrans %}</dt>
|
||||
<dd>{{ reset_link.expires | format_date }}</dd>
|
||||
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
|
||||
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}{% endcall %}</dd>
|
||||
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}Reset your Snikket password{% endcall %}</dd>
|
||||
</dd>
|
||||
<div class="f-bbox">
|
||||
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
|
||||
|
||||
@@ -76,13 +76,20 @@
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Connected devices{% endtrans %}</dt>
|
||||
<dt>{% trans %}Active users{% endtrans %}</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
{%- if metrics.prosody_devices | default(None) is not none -%}
|
||||
{{ metrics.prosody_devices }}
|
||||
<li>{% trans %}Connected now:{% endtrans %} {{ metrics.prosody_devices }}</li>
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
<li><em>{% trans %}unknown{% endtrans %}</em></li>
|
||||
{%- endif -%}
|
||||
{%- if metrics.users | default(None) is not none -%}
|
||||
<li>{% trans %}Past 24 hours:{% endtrans %} {{ metrics.users.active_1d }}</li>
|
||||
<li>{% trans %}Past 7 days:{% endtrans %} {{ metrics.users.active_7d }}</li>
|
||||
<li>{% trans %}Past 30 days:{% endtrans %} {{ metrics.users.active_30d }}</li>
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %}"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
|
||||
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %} no-copy no-share"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
|
||||
</html>
|
||||
|
||||
@@ -115,8 +115,63 @@ var copy_to_clipboard_btn = function(el) {
|
||||
});
|
||||
};
|
||||
|
||||
var copy_to_clipboard_btn = function(el) {
|
||||
var text = el.dataset.cliptext;
|
||||
if (!text) {
|
||||
console.error('copy_to_clipboard used on element without text to copy');
|
||||
}
|
||||
copyTextToClipboard(text, el, function(success) {
|
||||
var existing_result_el = document.getElementById("clipboard-result");
|
||||
if (existing_result_el !== null) {
|
||||
existing_result_el.parentNode.removeChild(existing_result_el);
|
||||
}
|
||||
|
||||
var icon = "done";
|
||||
if (!success) {
|
||||
icon = "cancel";
|
||||
}
|
||||
var icon_bak = get_current_icon(el.firstChild);
|
||||
change_icon(el.firstChild, icon);
|
||||
setTimeout(function() {
|
||||
change_icon(el.firstChild, icon_bak);
|
||||
el.blur();
|
||||
}, 1500);
|
||||
});
|
||||
};
|
||||
|
||||
var share_url_btn = function(el) {
|
||||
let data = {
|
||||
"title": el.dataset.shareTitle,
|
||||
"url": el.dataset.shareUrl,
|
||||
}
|
||||
|
||||
let icon_bak = get_current_icon(el.firstChild);
|
||||
|
||||
new Promise(function (resolve, reject) {
|
||||
if(!navigator.canShare || !navigator.canShare(data)) {
|
||||
return reject();
|
||||
}
|
||||
return resolve(navigator.share(data));
|
||||
}).then(function () {
|
||||
// Success
|
||||
change_icon(el.firstChild, "done");
|
||||
}, function () {
|
||||
// Failure
|
||||
change_icon(el.firstChild, "cancel");
|
||||
}).finally(function () {
|
||||
// Either way, clear status icon after 1.5s
|
||||
setTimeout(function() {
|
||||
change_icon(el.firstChild, icon_bak);
|
||||
el.blur();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
document.body.classList.remove("no-copy");
|
||||
if(navigator.share) {
|
||||
document.body.classList.remove("no-share");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -10,12 +10,38 @@
|
||||
{%- 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>
|
||||
{%- else -%}
|
||||
<div><input type="text" {% if id_ %}id="{{ id_ }}" {% endif %}readonly="readonly" value="{{ uri }}"></div>
|
||||
<div>{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}</div>
|
||||
<div>
|
||||
{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}
|
||||
{% call share_button(caller() if caller is not none else None, uri, show_label=True) %}{% trans %}Share{% endtrans %}{% endcall %}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -59,7 +85,7 @@
|
||||
|
||||
{% macro clipboard_button(data, show_label=False, caller=None, class=None) -%}
|
||||
{%- set label = caller() -%}
|
||||
<a class="button{% if class %} {{ class }}{% endif %}"
|
||||
<a class="button copy-to-clipboard{% if class %} {{ class }}{% endif %}"
|
||||
href="#"
|
||||
{% if not show_label %}
|
||||
aria-label="{{ label }}"
|
||||
@@ -74,6 +100,24 @@
|
||||
</a>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro share_button(title, url, show_label=False, caller=None, class=None) -%}
|
||||
{%- set label = caller() -%}
|
||||
<a class="button share-button{% if class %} {{ class }}{% endif %}"
|
||||
href="#"
|
||||
{% if not show_label %}
|
||||
aria-label="{{ label }}"
|
||||
title="{{ label }}"
|
||||
{% endif %}
|
||||
data-share-title="{{ title }}"
|
||||
data-share-url="{{ url }}"
|
||||
onclick="share_url_btn(this); return false;">
|
||||
{%- call icon("share") %}{% endcall -%}
|
||||
{%- if show_label %}
|
||||
<span>{{ label }}</span>
|
||||
{% endif -%}
|
||||
</a>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_errors(field, caller=None) -%}
|
||||
{%- set error_list = field.errors if field.errors is not mapping else (field.errors.values() | flatten | list) -%}
|
||||
{%- if error_list -%}
|
||||
@@ -109,18 +153,44 @@
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro invite_type_name(invite_info, caller=None) -%}
|
||||
{%- if invite_info.reusable -%}
|
||||
{% trans %}Group{% endtrans %}
|
||||
{%- else -%}
|
||||
{%- macro invite_type_name(invite_type, caller=None) -%}
|
||||
{%- if invite_type == "account" -%}
|
||||
{% trans %}Individual{% endtrans %}
|
||||
{%- else -%}
|
||||
{% trans %}Group{% endtrans %}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro invite_type_description(invite_info, caller=None) -%}
|
||||
{%- if invite_info.reusable -%}
|
||||
{% trans %}Can be used multiple times to create accounts on this Snikket service.{% endtrans %}
|
||||
{%- else -%}
|
||||
{% trans %}Can be used once to create an account on this Snikket service.{% endtrans %}
|
||||
{% 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:registered" -%}
|
||||
{% 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 -%}
|
||||
{% 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 %}
|
||||
|
||||
{% macro invite_type_description(invite_type, caller=None) %}
|
||||
{%- if invite_type == "account" -%}
|
||||
{% trans %}Invite a single person (invitation link can only be used once).{% endtrans %}
|
||||
{%- elif invite_type == "group" -%}
|
||||
{% trans %}Invite a group of people (invitation link can be used multiple times).{% endtrans %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro invite_type_icon(invite_type, caller=None) %}
|
||||
{%- if invite_type == "account" -%}
|
||||
{% call icon("person") %}{% endcall %}
|
||||
{%- elif invite_type == "group" -%}
|
||||
{% call icon("people") %}{% endcall %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
|
||||
</div>
|
||||
</from>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
var domainCheck = function() {
|
||||
var form = document.getElementById("login-form");
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
{% include "copy-snippet.html" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Welcome!{% endtrans %}</h1>
|
||||
<p>{% trans user_name=user_info.display_name %}Welcome home, {{ user_name }}.{% endtrans %}</p>
|
||||
{% if user_info.is_admin and metrics.users and metrics.users.active_1d <= 1 %}
|
||||
<aside class="box hint">
|
||||
<header>{% trans %}Welcome to Snikket!{% endtrans %}</header>
|
||||
<p>{% trans %}Now your Snikket instance is up and running, the next step is to invite people to join it. Family, friends, colleagues... you choose!{% endtrans %}</p>
|
||||
<a href="/admin/invitations">{% trans %}Create new invitation{% endtrans %}</a>
|
||||
</aside>
|
||||
{% endif %}
|
||||
<nav class="welcome">
|
||||
<ul>
|
||||
<li class="wide">
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/es/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/es/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
1939
snikket_web/translations/es/LC_MESSAGES/messages.po
Normal file
1939
snikket_web/translations/es/LC_MESSAGES/messages.po
Normal file
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
Binary file not shown.
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
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/ru/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/ru/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/uk/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/uk/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
1961
snikket_web/translations/uk/LC_MESSAGES/messages.po
Normal file
1961
snikket_web/translations/uk/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -91,7 +97,12 @@ class ImportAccountDataForm(BaseForm):
|
||||
@client.require_session()
|
||||
async def index() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
return await render_template("user_home.html", user_info=user_info)
|
||||
metrics = await client.get_system_metrics()
|
||||
return await render_template(
|
||||
"user_home.html",
|
||||
user_info=user_info,
|
||||
metrics=metrics,
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/passwd', methods=["GET", "POST"])
|
||||
|
||||
@@ -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
|
||||
@@ -25,6 +27,7 @@ navigation/cancel:cancel
|
||||
navigation/more_vert:more
|
||||
social/groups:groups
|
||||
social/people:people
|
||||
social/person:person
|
||||
social/group_add:create_group
|
||||
social/person_add:add_user
|
||||
social/person_remove:remove_user
|
||||
@@ -33,3 +36,4 @@ image/edit:edit
|
||||
action/admin_panel_settings:admin
|
||||
content/link:link
|
||||
content/insights:insights
|
||||
social/share:share
|
||||
|
||||
6
tools/import-icons.sh
Normal file → Executable file
6
tools/import-icons.sh
Normal file → Executable 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 Google’s Material Icons set,\nlicensed under the terms of the Apache 2.0 License -->\n' >> "$output_file"
|
||||
|
||||
Reference in New Issue
Block a user