Compare commits

...

449 Commits

Author SHA1 Message Date
1900be4c0e Add custom homepage template 2025-07-07 17:29:34 +09:00
00b4769ed9 Switch to uv, add project config files. 2025-07-07 17:26:11 +09:00
Matthew Wild
488dc9a3f3 Merge pull request #202 from snikket-im/ka/oauthtweaks
OAuth tweaks
2025-06-05 17:55:56 +01:00
Kim Alvefur
1a65ba6150 Include a software id in oauth client registration
This is supposed to be a unique and persistent identifier for the
software itself, regardless of version or deployment instance.

Generated from the domain name in the comment using uuid_generate_sha1()
2025-06-05 17:52:04 +01:00
Kim Alvefur
9474238dee Declare as a web application in oauth client registration
It is, even if the password grant isn't restricted to that, but if ever
the authorization code flow is implemented, it'll be correct.
2025-06-05 17:52:01 +01:00
Kim Alvefur
60e663316b Declare that oauth client credentials are using POST method
Not enforced by mod_http_oauth2, but could be in the future
2025-06-05 17:50:52 +01:00
Kim Alvefur
770d05c72c Declare use of no response types, since password grant uses none
Needless restriction removed in
https://hg.prosody.im/prosody-modules/rev/ef81c67e1ae7
2025-06-05 17:50:52 +01:00
Kim Alvefur
ea75d8e832 Include requested scopes in oauth client registration
This can be used on the oauth server side to enforce that no additional
scopes are added.
2025-06-05 17:50:52 +01:00
Kim Alvefur
145dda8c19 Include web portal version in oauth client registration
This could be shown in client listings and audit logs, and checked to
ensure old versions stop being used. Not the most relevant for the web
portal as it is closely tied together with the server, but could help
answer questions about where old grants come from.
2025-06-05 17:50:52 +01:00
Matthew Wild
149a79cb2c Merge pull request #203 from snikket-im/make-lint
prosodyclient: Fixes to satisfy mypy
2025-06-05 17:48:23 +01:00
Matthew Wild
69f77020b8 prosodyclient: Fixes to satisfy mypy 2025-06-05 17:46:05 +01:00
Matthew Wild
5ac481a4b4 prosodyclient: Switch to black formatting and remove lint issues 2025-06-05 17:33:22 +01:00
Matthew Wild
56470eec01 Github: Use flake8 target 2025-06-05 17:32:34 +01:00
Matthew Wild
9b4903b230 Makefile: Add lint, format (black), flake8 and mypy targets 2025-06-05 17:29:44 +01:00
Matthew Wild
74c3946609 Bump log level of oauth errors 2025-06-02 11:36:08 +01:00
Matthew Wild
feabed6565 Register as an OAuth client and authenticate token requests
mod_http_oauth2 in prosody-modules was updated to require client
authentication for the password grant, which previously did not need
client authentication.

This means that the first request we make to Prosody will now register as a
client in order to obtain client_id and client_secret.

There is no real security gain from this approach (unlike other grant types,
the password grant does not do redirects which could be intercepted). In the
future, however, some security could be gained by having Prosody restrict
the ability to use the password grant to privileged OAuth clients. This would
prevent third-party OAuth clients from using the password grant which is not
suitable for that purpose.
2025-06-02 11:36:08 +01:00
Jonas Schäfer
af13a3cc47 Merge pull request #197 from snikket-im/z/play-badge-l10n
Serve localized Google Play badges locally
2025-04-13 08:57:16 +02:00
Kim Alvefur
466e3e79b7 Serve localized Google Play badges locally
Fixes #196

Badges downloaded from <https://play.google.com/intl/en_us/badges/> with
a bit of automation to get one per supported language.
2025-04-12 20:23:10 +02:00
Weblate
3f1ce7565b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-08-11 19:21:49 +00:00
Matthew Wild
265ca4db8f Merge pull request #191 from snikket-im/fix/issue190
Ignore that users do not have access to metrics
2024-08-11 20:21:32 +01:00
Kim Alvefur
5015c4aa43 fixup: refresh translation code references 2024-08-11 16:36:53 +02:00
Kim Alvefur
465720c5b1 fixup: please flake8 2024-08-11 16:34:06 +02:00
Kim Alvefur
2a8e7ae72b fixup: please mypy 2024-08-11 16:25:17 +02:00
Andrey
449e345ee5 Ignore that users do not have access to metrics
Fixes #190

The templates appear to handle this being False, so that seems the path
of least resistance.
2024-08-11 15:56:40 +02:00
Andrey
51798ecc43 Translated using Weblate (Russian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-07-18 17:08:03 +00:00
uira
1e15ef5fce Translated using Weblate (Indonesian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2024-07-18 17:07:58 +00:00
misiek
0f41aa24d8 Translated using Weblate (Polish)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2024-05-07 17:04:52 +00:00
J👀
15516cdaa5 Translated using Weblate (Spanish)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/es/
2024-05-06 05:04:46 +00:00
Rosebud
948e415dbd Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/zh_Hans/
2024-05-02 21:15:30 +00:00
Kim Alvefur
a3fcf7d1d4 Translated using Weblate (Swedish)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2024-05-02 21:15:28 +00:00
Federico
65de73f1fe Translated using Weblate (Italian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2024-05-02 21:15:28 +00:00
Roberto Resoli
989fe7b5b6 Translated using Weblate (Italian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2024-05-02 21:15:27 +00:00
Andrey
4bc929e1ce Translated using Weblate (Russian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-05-02 21:15:27 +00:00
BetaRays
5817b24c48 Translated using Weblate (French)
Currently translated at 100.0% (373 of 373 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2024-05-02 21:15:19 +00:00
Weblate
550526efc9 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-04-30 09:56:03 +00:00
Kim Alvefur
2a2e36ade2 Translated using Weblate (Swedish)
Currently translated at 100.0% (370 of 370 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2024-04-30 09:56:01 +00:00
Andrey
22f7d6f36a Translated using Weblate (Russian)
Currently translated at 100.0% (370 of 370 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-04-30 09:56:01 +00:00
Matthew Wild
2d42099017 Merge pull request #188 from snikket-im/invitation-ui
Invitation admin UI improvements
2024-04-30 10:55:44 +01:00
Matthew Wild
2ff47c486a Update translation strings 2024-04-30 10:52:52 +01:00
Matthew Wild
338ee0b278 Add 'share' button for browsers supporting Web Share API 2024-04-30 10:48:51 +01:00
Matthew Wild
64c6548a48 Support for optional text notes on invitations 2024-04-29 18:39:06 +01:00
Matthew Wild
8c824149cc Fixes for invitation display
- Reorder columns, from generic to specific
- Fix empty tooltip on invitation types caused by incorrect macro usage
2024-04-29 18:19:07 +01:00
Matthew Wild
607863cfc4 Remove duplicate template macro 2024-04-29 18:00:22 +01:00
Matthew Wild
13c5d44544 Merge pull request #187 from snikket-im/cookie-samesite-attribute
Explicitly set cookie SameSite attribute to Lax
2024-04-29 11:22:21 +01:00
Matthew Wild
6407eb90db Explicitly set cookie SameSite attribute to Lax
With 'Secure' set, it may default to 'None', which we don't need or want.

'Strict' is not suitable for session cookies - the user would see the login
screen when navigating from another site (e.g. hosting dashboard) and we
already have CSRF protection on forms.
2024-04-29 11:18:55 +01:00
Matthew Wild
a8c6b1a70c Merge pull request #186 from snikket-im/cookie-secure-attribute
Add 'secure' attribute to session cookies
2024-04-29 11:09:44 +01:00
Matthew Wild
67c94bb045 Add 'secure' attribute to session cookies 2024-04-29 11:08:30 +01:00
Weblate
f4c1173a34 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-04-28 08:40:16 +00:00
Jonas Schäfer
e39b0082b1 Merge pull request #185 from Zash/translate-welcome
Translate welcome message
2024-04-28 10:39:57 +02:00
Kim Alvefur
9eb187a951 Make welcome message translatable 2024-04-27 14:22:39 +02:00
Kim Alvefur
b928e74a74 make extract_translations 2024-04-27 14:21:32 +02:00
Andrey
75c0f504d0 Translated using Weblate (Russian)
Currently translated at 100.0% (368 of 368 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/ru/
2024-04-23 17:14:06 +00:00
Matthew Wild
7c0310a141 Merge pull request #184 from Zash/really-fix-default-invite-role
Specify a default role in invite form
2024-04-19 14:50:01 +01:00
Kim Alvefur
5e2e645787 Specify a default role in invite form
Actually in the invite form this time
2024-04-19 15:48:40 +02:00
Weblate
9b31894e85 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-04-19 13:38:57 +00:00
Kim Alvefur
a4472e1a44 Translated using Weblate (Swedish)
Currently translated at 100.0% (362 of 362 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2024-04-19 13:38:56 +00:00
Matthew Wild
b99cae84de Update translations 2024-04-19 14:38:45 +01:00
Matthew Wild
1cac19e4c9 Merge pull request #183 from Zash/default-invite-role
Specify a default role in invite form
2024-04-19 14:37:22 +01:00
Kim Alvefur
d4883765b2 Specify a default role in invite form
The role creation appears to fail without an error, only refreshing the
page unless a role is selected.
2024-04-19 15:30:20 +02:00
Weblate
041f26274b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-04-17 08:41:42 +00:00
Matthew Wild
82db30ffd9 Merge pull request #182 from snikket-im/invitation-improvements
Allow selecting a role when creating an invitation
2024-04-17 09:41:31 +01:00
Matthew Wild
b8684329b4 Fix syntax error in template 2024-04-16 21:30:19 +01:00
Matthew Wild
7e26b5f994 Update translations 2024-04-16 21:22:07 +01:00
Matthew Wild
4bdcb46a8a Allow selecting a role when creating an invitation
Includes some reorganization and prettification of the creation form.
2024-04-16 21:16:06 +01:00
Matthew Wild
ed6f413c18 Don't fail if active user metrics are unavailable 2024-04-16 20:42:48 +01:00
Weblate
f63549ee87 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2024-04-16 14:51:58 +00:00
Matthew Wild
bd71ab1449 Update translations 2024-04-16 15:51:47 +01:00
Matthew Wild
220bf9994b Show invitation hint when user is alone on their instance 2024-04-16 15:09:16 +01:00
Matthew Wild
33d28e5890 Show active user counts in instance metrics 2024-04-16 15:09:16 +01:00
Matthew Wild
f0f0fa15c9 Small clarifications to the invitation type UI 2024-04-16 15:09:16 +01:00
Matthew Wild
30a9a6816f prosodyclient: Skip adding metadata of broken avatars 2024-04-16 15:09:16 +01:00
Sergio Moreno López
970b8fa7f1 Translated using Weblate (Spanish)
Currently translated at 39.6% (143 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/es/
2024-04-14 21:02:20 +00:00
Sergio Moreno López
629d725ff5 Translated using Weblate (Spanish)
Currently translated at 36.0% (130 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/es/
2024-04-05 21:27:23 +00:00
Sergio Moreno López
6998e66b22 Translated using Weblate (Spanish)
Currently translated at 29.9% (108 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/es/
2024-03-31 22:27:22 +00:00
Matthew Wild
c668c4c56a Added translation using Weblate (Spanish) 2024-03-29 10:13:30 +00:00
Matthew Wild
a13fbd87a6 Merge pull request #181 from snikket-im/feature/handle-invalid-avatars-gracefully
Handle broken/incorrect avatar metadata gracefully
2024-03-11 15:30:24 +00:00
Jonas Schäfer
7ffcd76cea Handle broken/incorrect avatar metadata gracefully
Fixes #180.
2024-03-10 10:49:29 +01:00
Federico
bda0f52320 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2024-01-20 23:01:47 +00:00
Matthew Wild
5efc2a671e Merge pull request #177 from snikket-im/feature/fix-requirements
Fix python requirements to actually work
2024-01-16 16:12:37 +00:00
Jonas Schäfer
1578654816 Fix python requirements to actually work
Didn't do deep research here, just fitted it against the current Docker
image release.
2024-01-16 16:52:47 +01:00
Matthew Wild
e8ab33e12f Merge pull request #175 from snikket-im/fix/html-tag-typo
templates: Fix typo in closing form tag
2024-01-11 15:29:26 +00:00
Matthew Wild
712b0dc502 templates: Fix typo in closing form tag 2024-01-11 15:20:17 +00:00
Matthew Wild
e56c0f9029 github: Strip Generated-By in both places 2024-01-09 15:07:21 +00:00
Weblate
794b48a50b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  Jane
  jane

  Robert
  robert

  Sally
  sally

  Steven
  steven

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

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

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

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

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

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

- User account enabled/disabled status (read and write)
- Deletion status (if an account is scheduled for deletion)
- Avatar metadata
2023-12-08 10:50:23 +00:00
Matthew Wild
e7ed9dd176 infra: Extend time/date utilities 2023-12-08 10:50:06 +00:00
Matthew Wild
6778557db8 On success, return to user listing (edit is complete) 2023-12-08 10:49:26 +00:00
Matthew Wild
73f3f25515 Add lock_open and restore_from_trash icons 2023-12-08 10:45:08 +00:00
Matthew Wild
bd66600d05 Merge pull request #165 from snikket-im/feature/multiple-circle-mucs
Support circles with multiple group chats, remove default group chat
2023-11-06 14:26:11 +00:00
Matthew Wild
db363367da Support circles with multiple group chats, remove default group chat 2023-11-06 13:52:30 +00:00
Matthew Wild
7ce13b55ac Merge pull request #162 from snikket-im/feature/policies-and-contacts
Add policy URLs and contact addresses for instances in the relevant places
2023-10-25 16:19:44 +01:00
Matthew Wild
d6d4bb5afb Add policy URLs and contact addresses for instances in the relevant places 2023-10-25 16:18:12 +01:00
Kim Alvefur
da52771ebe Merge pull request #161 from Zash/fix-logout
Fix revokation of token on logout
2023-10-21 15:57:46 +02:00
Kim Alvefur
e39b6ca8bb Fix revokation of token on logout
In OAuth 2.0, you don't authenticate with the revocation endpoint using
the token you are revoking, but rather the OAuth client credentials.
2023-10-07 17:39:37 +02:00
Kim Alvefur
14368c5e9a Merge pull request #158 from Zash/prosody-split-user-roles
Update for role changes in Prosody
2023-10-07 14:17:58 +02:00
Kim Alvefur
2cdcf7f282 Update for role changes in Prosody
See https://hg.prosody.im/trunk/rev/082c7d856e61
2023-10-07 12:59:43 +02:00
Kim Alvefur
0f1e76e38c Merge pull request #157 from Zash/debian12
Switch base image to Debian 12
2023-10-07 12:58:58 +02:00
Kim Alvefur
ad9af20f12 Workaround for Flask context change 2023-10-04 23:27:05 +02:00
Kim Alvefur
9672cd6870 Install as many packages as possible from Debian
The only missing piece appears to be environ-config.

This **fails to start** with

```
ImportError: cannot import name '_app_ctx_stack' from 'quart' (/usr/lib/python3/dist-packages/quart/__init__.py)
```
2023-10-04 23:27:05 +02:00
Kim Alvefur
d3a6be7bec Switch base image to Debian 12 2023-08-07 08:47:55 +02:00
Kim Alvefur
7a4b56914c Switch to sassc for CSS building
Because https://github.com/Kronuz/pyScss/pull/426 is not yet in a
release, also just look at the diffstat!
2023-08-02 22:35:32 +02:00
Roberto Resoli
0f74b1b8f2 Translated using Weblate (Italian)
Currently translated at 91.8% (294 of 320 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/it/
2023-07-13 11:17:54 +00:00
misiek
df78e8a8b0 Translated using Weblate (Polish)
Currently translated at 100.0% (320 of 320 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2023-04-04 14:11:31 +00:00
Kim Alvefur
77ccdd5eed Translated using Weblate (Swedish)
Currently translated at 100.0% (320 of 320 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2023-04-02 15:17:18 +00:00
uira
54b6cad7cd Translated using Weblate (Indonesian)
Currently translated at 100.0% (320 of 320 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2023-04-02 15:17:15 +00:00
Jonas Schäfer
fbb618c178 Translated using Weblate (German)
Currently translated at 100.0% (320 of 320 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/de/
2023-04-01 11:13:44 +00:00
Weblate
bd3d56851b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-04-01 10:17:56 +00:00
Jonas Schäfer
c475b83c02 Merge pull request #154 from snikket-im/feature/protect-circle-deletion
Add confirmation step to circle deletion
2023-04-01 10:22:23 +02:00
Jonas Schäfer
d9b73055a8 Add confirmation step to circle deletion
Deleting a circle is highly destructive. It removes the group chat
alongside history, as well as the user list. It should definitely be
protected by a confirmation dialogue, I have no clue why it wasn't.

Fixes #153.
2023-04-01 10:08:52 +02:00
Matthew Wild
f37270594e Merge pull request #152 from snikket-im/fix/role-config
Follow new role scheme in Prosody
2023-03-29 20:16:36 +01:00
Jonas Schäfer
fcfcdbeb23 Follow new role scheme in Prosody
Prosody changed its role scheme to only support a single primary role
for each user. In addition, the names of the built-in roles have been
changed. We thus follow those changes to be compatible with the most
recent trunk.

One open question is whether we should switch admin -> operator here,
too (operator being a server-wide admin), but so far there's no need
to.
2023-03-29 18:42:53 +02:00
Jonas Schäfer
fd566b7f30 Merge pull request #151 from snikket-im/fix/user-listing-roles
Make AdminUserInfo compatible with new API
2023-03-28 22:23:06 +02:00
Jonas Schäfer
2762304ae8 Make AdminUserInfo compatible with new API
The mod_http_admin_api changed recently [1], so we need to follow
suit.

Fixes #149.

   [1]: https://hg.prosody.im/prosody-modules/rev/d68348323406
2023-03-28 22:21:07 +02:00
Jonas Schäfer
49bbc3ab09 Merge pull request #148 from Zash/newscopes
Update to match new Prosody scope naming scheme
2023-03-28 21:42:00 +02:00
Kim Alvefur
8f1f80b7d7 Update to match new Prosody scope naming scheme
Ref https://hg.prosody.im/prosody-modules/rev/5ab134b7e510

Thanks Jonas
2023-03-28 21:14:20 +02:00
Jonas Schäfer
13bc283a3e Merge pull request #140 from snikket-im/feature/site-name-consistency
entrypoint: default SNIKKET_WEB_SITE_NAME to SNIKKET_SITE_NAME
2023-03-28 20:59:17 +02:00
Jonas Schäfer
abc0af3918 entrypoint: default SNIKKET_WEB_SITE_NAME to SNIKKET_SITE_NAME
The documentation only talks about SNIKKET_SITE_NAME,
and users thus do not know that they must
set SNIKKET_WEB_SITE_NAME to make
their site name appear on the portal.
2023-03-28 20:54:47 +02:00
Jonas Schäfer
0aff4fc99d Merge pull request #144 from snikket-im/fix/invite-link-header
Fix Link header on invite pages
2023-03-28 20:48:59 +02:00
Jonas Schäfer
40562d16f6 Fix Link header on invite pages
Thanks @singpolyma.
2023-03-28 19:19:43 +02:00
Jonas Schäfer
48a4a8f587 Merge pull request #147 from snikket-im/fix/i18n-lint
Fix i18n CI linting
2023-03-28 19:19:35 +02:00
Jonas Schäfer
664112bf53 Fix i18n CI linting
It diffs the things, and we're in 2023 now.
2023-03-28 19:16:27 +02:00
Jonas Schäfer
2dfc39e757 Merge pull request #141 from snikket-im/fix/quart-dep
Update WTForms and pin quart version
2023-03-28 19:14:30 +02:00
Jonas Schäfer
31b743a97f Update WTForms and pin quart version
The quart pin is required to fix an attribute error which otherwise
occurs during startup. Hence, this should be a good qualifier to know
when it's safe to upgrade.

Note that this is not a problem in Quart, but in flask-WTForms. But
downgrading flask-wtforms does not help [1], so we don't revert that
uprade.

```
AttributeError: module 'quart.json' has no attribute 'JSONEncoder'
```

   [1]: https://github.com/pallets/quart/issues/163
2022-10-24 10:14:03 +02:00
Weblate
14a335bb06 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2022-06-07 20:34:03 +00:00
Jonas Schäfer
6c8c213a88 Merge pull request #100 from snikket-im/feature/drop-xmpp-qr-code
Remove XMPP URI QR code
2022-06-07 22:33:53 +02:00
Jonas Schäfer
2e224d96ce Remove XMPP URI QR code
At the same time, we can also drop the CSS used for that makeshift tab
box. I always felt a bit uneasy about it, a11y-wise, so it's good
riddance.

Fixes #99.
2022-06-07 22:31:40 +02:00
Weblate
b70cb57497 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2022-06-06 17:55:08 +00:00
Jonas Schäfer
124e0ce145 Merge pull request #137 from snikket-im/premerge
Merge a bunch of things together
2022-06-06 19:54:54 +02:00
Jonas Schäfer
f2c79044e0 Clean up post-merge lint
I am a *bit* sorry for this commit, because ideally this would've been
folded into 6d50b1c2c7 and whatever the
source of the other "conflict" was.

However, as the things have been merged in a batch, I can't do much more
than this.
2022-06-06 19:52:24 +02:00
Jonas Schäfer
13bc4bb227 Merge branch 'fix/babel-extraction' into premerge 2022-06-06 19:46:59 +02:00
Jonas Schäfer
f1351eb5cc Merge branch 'fix/use-english-default' into premerge 2022-06-06 19:46:49 +02:00
Jonas Schäfer
41573569af Merge branch 'feature/export-back-button' into premerge 2022-06-06 19:46:43 +02:00
Jonas Schäfer
b1f3026b8a Merge branch 'feature/wtforms-3' into premerge 2022-06-06 19:46:32 +02:00
Jonas Schäfer
6794314a59 Merge branch 'feature/vary-accept-language' into premerge 2022-06-06 19:46:25 +02:00
Jonas Schäfer
077e957a00 Merge branch 'feature/ci-extract-translations' into premerge 2022-06-06 19:46:19 +02:00
Jonas Schäfer
4902941145 Merge branch 'feature/strip-versions' into premerge 2022-06-06 19:46:09 +02:00
Jonas Schäfer
5222c8eafe Merge branch 'feature/hypercorn-stdout' into premerge 2022-06-06 19:44:39 +02:00
Jonas Schäfer
03ca7ac5bb Unbreak translation text extraction
It was broken because of the same jinja2 update (presumably) which
prompted 68f72743c5.
2022-05-30 20:51:37 +02:00
Matthew Wild
56cee8bab6 Merge pull request #135 from snikket-im/feature/update-dependencies
Update dependencies
2022-05-30 16:59:22 +01:00
Jonas Schäfer
b36fc0d5ac Bump hsluv to 5.x
Fixes #134.
2022-05-30 17:38:18 +02:00
Jonas Schäfer
68f72743c5 Bump quart to version 0.17
This is needed because jinja2 had an update which caused the portal to
not work at all:

```
ImportError: cannot import name 'escape' from 'jinja2'
```

Quart needed updating for that.

This update required a lot of typefixes. Apparently, the "canned"
responses (like redirect) are now plain werkzeug responses, while
quart.Response does not inherit from werkzeug.Response (otherwise, we
could've changed the type annotations to werkzeug.Response everywhere,
but that doesn't work because a quart.Response is not a
werkzeug.Response).

P.S.: This time, I *did* check that avatar uploads don't break (see
b007afc).
2022-05-30 17:37:54 +02:00
Daniel Holmgaard
8741efb2c4 Translated using Weblate (Danish)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-30 14:01:08 +00:00
Daniel Holmgaard
a0e8933b64 Translated using Weblate (Danish)
Currently translated at 95.6% (307 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-26 23:01:53 +00:00
Zack Zhou
edb3154127 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/zh_Hans/
2022-05-20 00:01:45 +00:00
Jonas Schäfer
eb22688302 Use english as default language instead of danish
It is more likely that a user for whose language no translation exists
can read english than danish.

The fallback to english was apparently introduced in c58ce845, though it
is possible that `best_match` did that internally before.

Fixes #131.
2022-05-15 14:12:51 +02:00
Jonas Schäfer
c278d4ace9 Merge pull request #132 from Raka-loah/master
Add Simplified Chinese support
2022-05-15 08:14:08 +02:00
Raka-loah
bbfe8624ef Add Simplified Chinese support 2022-05-14 17:53:40 +08:00
David Baraniak
8bcf619cef Translated using Weblate (French)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2022-04-11 13:00:44 +00:00
Kim Alvefur
73fda3d623 Add a Back button from export panel for consistency
The other user related sections all have a Back button so it makes sense
that this one ought to have one as well.
2022-02-19 16:28:38 +01:00
Matthew Wild
846a5e49fd Merge pull request #122 from snikket-im/feature/autocomplete-hints
Add autocomplete hints to password forms
2022-02-07 11:43:20 +00:00
Matthew Wild
b3ff7f04b5 Merge pull request #127 from snikket-im/fix/unhealthy
Install netcat in final image instead of build image
2022-02-07 11:42:40 +00:00
Jonas Schäfer
0ac4ab8142 Install netcat in final image instead of build image
`nc` (from netcat) is required for the healthcheck. In
c1cf6ab1e5, the installation was
erroneously moved to the builder image, instead of the final image, so
it was missing since then from the actual application image, causing it
to always show as unhealthy.

Fixes #126.
2022-02-05 13:51:30 +01:00
Matthew Wild
d4a38f5049 Merge pull request #125 from snikket-im/fix/support-requirements-compilation
Dockerfile: Add dev headers required for building deps
2022-02-01 09:09:50 +00:00
Matthew Wild
344a4d3e93 Dockerfile: Add dev headers required for building deps 2022-02-01 09:04:25 +00:00
Matthew Wild
57f1047526 Merge pull request #124 from snikket-im/fix/support-requirements-compilation
Dockerfile: Ensure a compiler is available while pip-installing requirements
2022-02-01 08:16:33 +00:00
Matthew Wild
b036caa85e Dockerfile: Ensure a compiler is available while pip-installing requirements
Dependencies are not necessarily packaged for all architectures. In some cases
(such as aiohttp, and others, on ARM) pip will attempt to compile the
dependency from scratch. Since switching to multi-stage builds, we have been
installing these without a compiler present which caused the build to fail on
ARM architectures.

This commit temporarily installs build-essential packages while running pip,
then removes them again afterwards.
2022-01-31 21:45:39 +00:00
Matthew Wild
08845cb9f0 Merge pull request #123 from snikket-im/hotfix/error-handling
Fix error handling when building image
2022-01-31 18:11:58 +00:00
Jonas Schäfer
6aa6e12680 Fix error handling when building image
Apparently, we managed to publish an image without working aiohttp
because of this.

Partially a regression from 5d7183a.
2022-01-31 19:09:03 +01:00
Jonas Schäfer
4bd58c1104 Add autocomplete hints to password forms
This allows user agents to do smart things like filling in the current
password only where it makes sense or integrate nicely with a password
manager.

Fixes #94.
2022-01-22 15:34:27 +01:00
Jonas Schäfer
a998348804 Make hypercorn log to stdout in Docker
This may help with debugging issues, because we now actually see
incoming requests also on the hypercorn layer.

Fixes #97.
2022-01-22 15:20:36 +01:00
Jonas Schäfer
20abe4b903 Add Vary: Accept-Language to all pages using that information
It was found during testing that some user agents cache aggressively
even between switches of the UI language. To properly indicate that the
pages actually depend on that information, we add the correct Vary
header.

Fixes #106.
2022-01-22 15:19:29 +01:00
Jonas Schäfer
a1ecb4ce80 Port to WTForms 3.x
Fixes #103.
2022-01-22 15:17:48 +01:00
Jonas Schäfer
b84b84b394 Add check for a missing make extract_translations
Forgetting to run that causes weblate (or other translation tools) to
show outdated strings and not import new strings, which is bad for the
collaboration.

Fixes #118.
2022-01-22 14:57:59 +01:00
misiek
4f7a4fb5d4 Translated using Weblate (Polish)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2022-01-21 15:00:36 +00:00
Jonas Schäfer
6d50b1c2c7 Do not show dependency versions even to admins by default
Dependency versions are generally not useful, unless you are developing
or otherwise outside of a normal release situation: If you are on a
normal release, we can figure out the dep versions by looking at the
docker image.

To reduce the amount of information displayed and the amount of
information which needs to be conveyed in case of problems, we only show
the web portal and prosody versions to admins, unless debug mode is
enabled.

The behaviour that versions are only shown to logged in admins (unless
debug mode is enabled) remains unchanged.

Fixes #115.
2022-01-20 18:11:47 +01:00
uira
34a23f8505 Translated using Weblate (Indonesian)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2022-01-18 14:59:01 +00:00
Jonas Schäfer
ebcb083b6a Translated using Weblate (German)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/de/
2022-01-18 14:59:01 +00:00
Kim Alvefur
2f0b38b149 Translated using Weblate (Swedish)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2022-01-18 09:21:29 +00:00
Weblate
6244ad5c8a Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2022-01-17 16:27:36 +00:00
Jonas Schäfer
07fa1f0abd Fix missing space in string 2022-01-17 17:27:26 +01:00
Weblate
3d22458f9b Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2022-01-17 16:26:16 +00:00
Jonas Schäfer
3b768fe220 Extract translations 2022-01-17 17:26:02 +01:00
Matthew Wild
19cf82e894 Merge pull request #111 from snikket-im/fix/byte-scaling
Fix corner cases of byte number formatting
2022-01-17 15:47:57 +00:00
Matthew Wild
fe0316708b Merge pull request #113 from snikket-im/feature/flake8-print
Include flake8-print plugin
2022-01-17 15:47:21 +00:00
Jonas Schäfer
81b0a58dc9 Merge pull request #107 from Zash/storagestats
Show how much storage space is used by shared files
2022-01-17 16:40:16 +01:00
Jonas Schäfer
08aea153f9 Merge pull request #110 from snikket-im/feature/account-export
Feature: account import/export
2022-01-17 16:38:04 +01:00
Jonas Schäfer
958b3365f7 Remove strange greeting copied over from user_home 2022-01-17 16:34:30 +01:00
Matthew Wild
05caf38d37 Use PUT method instead of POST, as expected by API 2022-01-17 16:33:46 +01:00
Matthew Wild
390ecded42 Include PEP data in export/import 2022-01-17 16:33:29 +01:00
Matthew Wild
f6395d4d9c Complete the implementation of data import 2022-01-17 16:33:00 +01:00
Matthew Wild
32179c72cd Add account data import UI on registration success page 2022-01-17 16:24:00 +01:00
Matthew Wild
3cb8185b1a prosodyclient: Add API to import XEP-0227 account data 2022-01-17 16:23:58 +01:00
Matthew Wild
481379d03f Switch to HTTP 204 to indicate no data to export
This is more robust, as it indicates the request was successfully
authenticated and processed, but that there is no data to export. This is
different from the URL not existing (which would also happen if the module was
unavailable, which should be a notable error instead).
2022-01-17 16:23:57 +01:00
Matthew Wild
275b302531 Add UI for exporting user account data 2022-01-17 16:23:56 +01:00
Matthew Wild
e18f727db0 prosodyclient: Add support for exporting a user's account data 2022-01-17 16:23:55 +01:00
Matthew Wild
f7429413cd Add more icons to the repertoire 2022-01-17 16:23:35 +01:00
Jonas Schäfer
d5a46b69a6 Include flake8-print plugin
This alerts us of stray print statements, which should never occur
because this is a non-TUI application.
2022-01-15 17:07:16 +01:00
Jonas Schäfer
51f2ebbd13 Handle the correct exception when formatting extremely high amounts of bytes
Found in production. Yes really. Due to some borked LXC integration, my
snikket host reports

```
MemTotal:       9007199254740991 kB
MemFree:        9007199254690591 kB
MemAvailable:   9007199254690591 kB
```

That is more than 1024 TiB, so it tries to go further up in the scale,
which then causes a Guru Meditation because of the uncaught IndexError.
2022-01-10 17:32:11 +01:00
Jonas Schäfer
b4e6ee8943 Fix formatting of zero bytes
Previously, that would raise a ValueError (math domain error), because
log(0) is undefined.
2022-01-10 17:31:50 +01:00
Jonas Schäfer
52d8047546 Correctly detect presence of storage metric
If there have been no uploads yet, the metric will be zero, so the if
condition would fail the test, so it would render as "unknown".
2022-01-10 16:56:31 +01:00
Jonas Schäfer
aed9ad1cde Merge pull request #93 from Zash/debianbullseye
Dockerfile: Switch base image to Debian 11
2022-01-10 16:40:28 +01:00
Kim Alvefur
b545c137b1 Dockerfile: Switch base image to Debian 11 2022-01-10 16:27:38 +01:00
Matthew Wild
47642dc384 Merge pull request #108 from snikket-im/feature/multi-stage-dockerfile
Dockerfile: Split build into multiple stages
2022-01-10 14:21:57 +00:00
Jonas Schäfer
5d7183a0b8 Reinstate cache deletion in multi-stage build
Previously, the multi-stage build increased the image size by about 30
MiB (163MiB -> 191MiB). Dropping the caches reduces the image size down
to 159MiB, leading to a net improvement of 4 MiB.
2022-01-08 13:29:28 +01:00
Matthew Wild
c1cf6ab1e5 Dockerfile: Split build into multiple stages
Currently the Dockerfile has a single RUN directive with all the needed
commands in it. This optimizes for image size by not creating too many
"layers" (which are only additive). However it means the result that gets
cached can basically never be reused, because any change to the source code
will need to execute the whole RUN block again.

This commit switches to a docker "multi-stage" build, where we have a build
image that is separate from the final one that gets published. The build
image can be cached locally, and size is no longer a significant concern.

This approach allows the single RUN command to be split up into multiple RUN
commands that only execute when strictly needed (i.e. when their result
is not cached locally).

This drastically improves the build time when rebuilding the image after
a simple code change, because the build image doesn't have to install all
the apt packages, for example. This leads to a nicer developer experience
when using docker locally for development and testing.
2022-01-08 13:17:52 +01:00
Jonas Schäfer
aee53a2e1a Merge pull request #109 from snikket-im/feature/fix-mypy
Fix mypy false positives
2022-01-08 13:14:28 +01:00
Jonas Schäfer
3a81a0140b Revert "Fix spurious mypy error"
This reverts commit 28ff19c19c.
2022-01-08 13:12:30 +01:00
Jonas Schäfer
5b4d4ddd36 Fix some mypy regression 2022-01-08 13:12:30 +01:00
Jonas Schäfer
28ff19c19c Fix spurious mypy error
For whatever reason, it thinks that babel has no __version__ field, but
it in fact does.
2022-01-08 12:52:31 +01:00
uira
8e3837f704 Translated using Weblate (Indonesian)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-12-12 07:00:42 +00:00
Kim Alvefur
4af78f635e Show how much storage space is used by shared files
Requires at least https://hg.prosody.im/prosody-modules/rev/883ad8b0a7c0
2021-11-18 19:50:47 +01:00
Matthew Wild
98e7de3166 Merge pull request #104 from snikket-im/feature/enable-restricted-users
admin: Show restricted user role in the UI
2021-11-09 17:03:08 +00:00
Matthew Wild
61c71b2145 admin: Inline restricted user role name
It was a variable only for the benefit of translators while disabled.
2021-11-09 17:00:59 +00:00
Matthew Wild
6b35e9a259 admin: Show restricted user role in the UI 2021-11-09 16:40:50 +00:00
Matthew Wild
58c2112fec Merge pull request #102 from snikket-im/feature/pin-wtforms
Pin wtforms to 2.x
2021-11-09 10:48:18 +00:00
Jonas Schäfer
c856afee82 Pin wtforms to 2.x
wtforms 3.0 is incompatible with our code. A separate issue will be
filed to address the incompatibilities, but this should be enough to get
working images out of it.

With 3.x, we're seeing:

```
  File "/home/horazont/Projects/python/snikket-web-portal/snikket_web/main.py", line 35, in LoginForm
    address = wtforms.TextField(
AttributeError: module 'wtforms' has no attribute 'TextField'
```

and the portal fails to start.
2021-11-08 18:07:34 +01:00
Matthew Wild
c8356a8e9e Merge pull request #101 from snikket-im/feature/https-qr
Force invite QR code to HTTPS
2021-10-15 14:32:57 +01:00
Jonas Schäfer
0eb464f428 Force invite QR code to HTTPS
We could also do a thing with ProxyFix, but honestly, this should always
be HTTPS.
2021-10-15 15:21:22 +02:00
misiek
2a6ef3c8f1 Translated using Weblate (Polish)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-09-03 15:00:35 +00:00
Matthew Wild
b5d148458a Merge pull request #98 from snikket-im/feature/apple
🎉 Finally set the URL for the App Store
2021-09-02 14:42:02 +01:00
Jonas Schäfer
261758b07a 🎉 Finally set the URL for the App Store
See-Also: https://snikket.org/blog/snikket-ios-public-release/
2021-08-31 17:03:21 +02:00
Jonas Schäfer
ff99c9488a Merge pull request #96 from Zash/invite_success_link
Link to main page after successful registration
2021-08-30 15:47:41 +02:00
Kim Alvefur
fe78631039 Link to main page after successful registration
Someone who registers via the web might also be interested in the web
portal.

Thanks to Jonas and Matthew for feedback on draft commit and help with
whatever this template syntax is.

Also no thanks to git for not actually having draft commits as a concept.
Mercurial is so much nicer to work with.
2021-08-29 15:10:02 +02:00
Kim Alvefur
12ddd288bf Translated using Weblate (Swedish)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-08-01 14:00:44 +00:00
misiek
633fb0d084 Translated using Weblate (Polish)
Currently translated at 97.0% (294 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-08-01 14:00:44 +00:00
uira
f9690063bc Translated using Weblate (Indonesian)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-06-21 23:01:04 +00:00
Jonas Schäfer
65ed50acd3 Merge pull request #90 from snikket-im/hotfix/f-droid-button
Fix F-Droid installation button
2021-06-21 20:23:02 +02:00
Jonas Schäfer
aa04320d70 Fix F-Droid installation button
The button was broken because it was classified as popover, which
means that the JavaScript code will mess with it. In reality,
*that* button was supposed to point at the actual market:// URL.

So we just remove the class and associated data here to fix that.

Fixes #89.
2021-06-20 14:14:30 +02:00
Link Mauve
818d50a1bb Translated using Weblate (French)
Currently translated at 97.6% (296 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-06-19 15:01:05 +00:00
Jonas Schäfer
c7ba7985ea Translated using Weblate (English (United Kingdom))
Currently translated at 40.5% (123 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en_GB/
2021-06-19 15:01:05 +00:00
Jonas Schäfer
223d127364 Translated using Weblate (English)
Currently translated at 70.2% (213 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en/
2021-06-19 15:01:04 +00:00
Jonas Schäfer
3a2c4543c4 Translated using Weblate (German)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-06-19 15:01:04 +00:00
Weblate
c307f057b9 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-06-18 14:20:21 +00:00
Jonas Schäfer
243d5ba236 Merge pull request #86 from snikket-im/feature/show-deleted-circle-members
admin: Show deleted users in circle members
2021-06-18 16:20:18 +02:00
Jonas Schäfer
3d62efccfc admin: Show deleted users in circle members
This helps with removing those users from circles, to avoid them
popping up in peoples roster again.

Even though removal from a circle also only partially works
(roster entries are for instance not cleared), this helps with
ghost users reappearing all the time.
2021-06-18 16:18:22 +02:00
Jonas Schäfer
9d26e39025 Merge pull request #87 from snikket-im/feature/mypy-ci-fix
Install build requirements for mypy CI check
2021-06-18 16:18:12 +02:00
Jonas Schäfer
874f0447ba Install build requirements for mypy CI check
Otherwise, the toml type hints are missing which mypy does not
like.
2021-06-18 16:14:45 +02:00
Jonas Schäfer
0f2127a672 Bring happiness to mypy 2021-06-18 16:11:22 +02:00
Weblate
20d84e7dd1 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-06-18 14:05:55 +00:00
Jonas Schäfer
a02e66023c Fix missing extract_translations run 2021-06-18 16:05:38 +02:00
Jonas Schäfer
e7db9cc772 Fix untranslated table header in admin_edit_circle.html 2021-06-17 17:02:26 +02:00
Jonas Schäfer
e91fb45374 Merge pull request #85 from snikket-im/auto-versioning
Automatically determine version from build info or git
2021-05-31 17:51:59 +02:00
Matthew Wild
531565d55c Automatically determine version from build info or git 2021-05-31 11:20:39 +01:00
Kim Alvefur
c6307619f9 Translated using Weblate (Swedish)
Currently translated at 100.0% (302 of 302 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-05-28 21:00:56 +00:00
Weblate
da2668cbbc Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-05-27 18:31:31 +00:00
Link Mauve
765e3890b4 Translated using Weblate (French)
Currently translated at 97.4% (272 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-05-27 18:31:30 +00:00
Jonas Schäfer
b40a625283 admin: allow disabling display of metrics
This is useful in situations where the admins of the Snikket
server (i.e. those who care for the docker containers) are not the
same people as the people who are admins of the Snikket service
(i.e. those who care for the users).
2021-05-27 17:59:40 +02:00
Jonas Schäfer
8a293985ca Implement system status panel
This offers system metrics and a way to send a broadcast
message to all online or registered users.

Requires prosody-modules cade5dac1003.
2021-05-27 17:21:58 +02:00
Jonas Schäfer
13b2a76c3d Fix mypy errors introduced in b007afc901 2021-05-27 16:33:46 +02:00
Jonas Schäfer
28e01c336d Do not install quart 0.15
As we saw in b007afc901, we cannot
use that version right now.
2021-05-25 18:56:15 +02:00
Jonas Schäfer
5fb0b91178 Bumping version number to 0.2.1 2021-05-22 11:11:50 +02:00
Jonas Schäfer
b007afc901 Revert "Upgrade to quart 0.15"
This reverts commit 486596f89f.
It was discovered that multipart/form-data forms do not work
correctly with Quart 0.15. The upgrade to Quart 0.15 was rushed
and not tested correctly, which I apologize for.

See-Also: https://github.com/pgjones/quart/issues/126
2021-05-22 11:11:16 +02:00
Matthew Wild
7f02746f63 admin: Re-disable 'limited' role (accidentally uncommented in c58ce8450) 2021-05-19 21:59:16 +01:00
Roberto Resoli
f2788aeb36 Translated using Weblate (Italian)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-05-19 15:12:20 +00:00
misiek
536a05b0eb Translated using Weblate (Polish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-05-19 15:12:20 +00:00
uira
e0226d47e3 Translated using Weblate (Indonesian)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-05-19 15:12:20 +00:00
misiek
0fe10a44ce Translated using Weblate (Polish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-05-18 13:29:00 +00:00
Jonas Schäfer
e892d81815 Translated using Weblate (German)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-05-18 13:29:00 +00:00
Jonas Schäfer
c58ce8450f Fix type annotations after bumping dependencies 2021-05-18 14:33:06 +02:00
Jonas Schäfer
03573d1f05 Explicitly pass mod_rest JSON as JSON
Otherwise, it can get passed as x-www-form-urlencoded, which
Prosody understandably does not quite like.
2021-05-18 12:35:46 +02:00
Jonas Schäfer
486596f89f Upgrade to quart 0.15 2021-05-18 12:35:31 +02:00
Jonas Schäfer
425b4d4295 Fix dysfunctional password reset button 2021-05-18 12:20:45 +02:00
misiek
87de808046 Translated using Weblate (Polish)
Currently translated at 97.8% (273 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-04-23 20:00:57 +00:00
misiek
05455ac743 Translated using Weblate (Polish)
Currently translated at 96.0% (268 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-04-05 19:01:04 +00:00
Daniel Holmgaard
1e926714cb Translated using Weblate (Danish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-04-02 19:01:03 +00:00
Tilman Jiménez
e1602f3140 Translated using Weblate (Spanish (Mexico))
Currently translated at 49.1% (137 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-03-31 13:00:53 +00:00
uira
2e89973263 Translated using Weblate (Indonesian)
Currently translated at 99.2% (277 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-03-29 08:00:48 +00:00
Kim Alvefur
a6f1361ddd Translated using Weblate (Swedish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-27 00:01:12 +00:00
Weblate
552a3bbd41 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-25 16:35:52 +00:00
misiek
3f2de1e5bf Translated using Weblate (Polish)
Currently translated at 97.6% (248 of 254 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-03-25 16:35:49 +00:00
Jonas Schäfer
059a10f475 Merge pull request #76 from snikket-im/feature/roles
"Edit user" flow, role management
2021-03-25 17:35:42 +01:00
Jonas Schäfer
a48abacf1d Disable restricted role for now
It is not implemented in snikket-server yet, so we don’t want to
put anything misleading out there.
2021-03-25 17:32:03 +01:00
Jonas Schäfer
ea7ed7c030 Add support for roles
Requires patches to prosody trunk which have been submitted
already (2021-03-22) which introduce the set_roles function on
usermanager.

Fixes #42.
2021-03-25 17:31:56 +01:00
Jonas Schäfer
cca899bd8c Create "Edit user" form
This aggregates the user actions behind a single "edit" button on
the list view, making it less crammed. It also offers the
functionality of actually editing the user, mind.

Also in preparation for #42.

Requires https://hg.prosody.im/prosody-modules/rev/5bc706c2db8f.
2021-03-25 17:31:49 +01:00
Jonas Schäfer
359e6b4ce2 Use tertiary style for "back" buttons
This allows us to have two levels of emphasis for the actual
form buttons and is also in line with the global "Log out"
navigational button.
2021-03-25 17:31:43 +01:00
Jonas Schäfer
6650dd2046 Capitalize App Store in the invite for consistency 2021-03-25 17:28:36 +01:00
Jonas Schäfer
97b4a7be0f Merge pull request #77 from Zash/mod_rest-version-change
Update for switch to datamapper in mod_rest
2021-03-24 09:10:22 +01:00
Kim Alvefur
329916e200 Update for switch to datamapper in mod_rest
mod_rest after the switch to the new util.datamapper in
https://hg.prosody.im/prosody-modules/rev/073f5397c1d2 does not accept
boolean True as value for the xep-0092 'version' field. An empty object
is equivalent and compatible with both previous and future versions.
2021-03-23 21:38:34 +01:00
Kim Alvefur
3571b8909b Translated using Weblate (Swedish)
Currently translated at 100.0% (254 of 254 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-23 15:00:36 +00:00
Jonas Schäfer
c6c01b82f5 Merge pull request #74 from snikket-im/feature/invite-from-users-list
Render invite form below user list
2021-03-22 15:30:56 +01:00
Weblate
c4b575f091 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-22 14:09:01 +00:00
Jonas Schäfer
fdb55568ec Change problematic "Back" buttons
Fixes #39.
2021-03-22 15:08:33 +01:00
Jonas Schäfer
a9a651be09 Render invite form below user list
Fixes #73.
2021-03-22 15:03:18 +01:00
Kim Alvefur
d2069289b0 Translated using Weblate (Swedish)
Currently translated at 100.0% (252 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-21 16:31:20 +00:00
Link Mauve
552b5d2940 Translated using Weblate (French)
Currently translated at 96.8% (244 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-03-21 16:31:19 +00:00
Jonas Schäfer
b0f9ae5d57 Translated using Weblate (German)
Currently translated at 100.0% (252 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-03-21 16:31:19 +00:00
Weblate
dd4a012612 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-20 15:59:19 +00:00
Jonas Schäfer
e7aa0a2c45 Fix more dotless strings 2021-03-20 16:44:44 +01:00
Jonas Schäfer
ad229d6700 Use standard error rendering for the login form
This provides a consistent UX.
2021-03-20 16:30:42 +01:00
Jonas Schäfer
b822000f2e Improve install button layout on narrow screens
This allows the button container to add line breaks between the
buttons when necessary.
2021-03-20 16:30:42 +01:00
Jonas Schäfer
a6b67b3fdd Improve install button layout on narrow screens
This allows the button container to add line breaks between the
buttons when necessary.
2021-03-20 16:20:05 +01:00
Jonas Schäfer
885db355ab Add F-Droid download button
Using the mechanism introduced for iOS to describe the multi-step
process.

Fixes #52.
2021-03-20 16:15:20 +01:00
Jonas Schäfer
c3d5b06313 Add multi-step instructions for iOS installation
Fixes #53.
2021-03-20 16:15:07 +01:00
Jonas Schäfer
2dd8838852 Report validity issues of avatar input right away
This helps discovering the actual error message. Thanks @zash.
2021-03-20 15:56:44 +01:00
Jonas Schäfer
5df2c3945a Use browser API to indicate validity state 2021-03-20 14:40:31 +01:00
Jonas Schäfer
3eb8036ebd Implement size checking for the avatar
This checks the avatar size on the client side (if available) and
on the server side against a configuration-defined limit. The
default limit is set to use the same value as in the original
report, as no sensible limit value is known.

Fixes #67.
2021-03-20 12:57:11 +01:00
Jonas Schäfer
02ed390cd2 Fix type annotation 2021-03-20 12:36:06 +01:00
Jonas Schäfer
2506810b90 Return 404 on expired invite URLs 2021-03-19 16:54:40 +01:00
Kim Alvefur
05d1b42dc4 Hint to the browser that the avatar should be PNG
Should result in that the file picker by default only shows PNG files
for selection.
2021-03-17 15:26:30 +01:00
GodGoldfish
5ef5b93eb9 Translated using Weblate (Russian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ru/
2021-03-12 23:04:55 +00:00
GodGoldfish
0ff6e00e9d Translated using Weblate (French)
Currently translated at 90.1% (220 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-03-12 23:04:54 +00:00
Jonas Schäfer
c04ac4bee0 Make linter happy 2021-03-11 07:32:31 +01:00
Daniel Holmgaard
3e19d42c2a Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-10 19:03:47 +00:00
Matthew Wild
03732ac06b Fix health check to declare itself text/plain and match Prosody 2021-03-10 14:54:01 +00:00
Matthew Wild
c70228fed7 Add /_health endpoint 2021-03-10 14:22:38 +00:00
Jonas Schäfer
025172592f Improve error handling in Prosody version retrieval 2021-03-09 22:20:37 +01:00
Kim Alvefur
6de1e5313f Add support for displaying prosody version
This only works for authenticated users even in debug mode because
it requires a session with prosody to send the request.

Fixes #66.
2021-03-09 22:08:58 +01:00
Jonas Schäfer
3083c118a3 Add fully translated language codes 2021-03-09 22:03:08 +01:00
Daniel Holmgaard
fa1b13fbdb Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-06 17:03:39 +00:00
Daniel Holmgaard
ba30d728f4 Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-03 22:03:08 +00:00
Matthew Wild
af87301fa4 Added translation using Weblate (Danish) 2021-03-01 21:01:54 +00:00
Kim Alvefur
8ee0b0dd30 Translated using Weblate (Swedish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-02-25 16:02:18 +00:00
Roberto Resoli
4a27ef9d72 Translated using Weblate (Italian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-25 16:02:18 +00:00
misiek
9e9fdaf8d4 Translated using Weblate (Polish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-25 16:02:17 +00:00
uira
bdb186ca81 Translated using Weblate (Indonesian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-02-25 16:02:17 +00:00
Jonas Schäfer
4ca9b82bce Translated using Weblate (German)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-25 16:02:16 +00:00
Jonas Schäfer
6dbe2c2d5e Fix missing type annotation 2021-02-23 21:18:14 +01:00
Jonas Schäfer
e410aedfef Improve logging of rejected IQ calls 2021-02-23 20:21:44 +01:00
Jonas Schäfer
1713da61e7 Fix password change
This fixes a regression introduced in e476d9b7 which caused the
token to be incorrectly used when authenticating with mod_rest for
sending the password change IQ stanza.
2021-02-23 20:20:47 +01:00
Kim Alvefur
53aac690df Add health check to dockerfile 2021-02-23 15:56:19 +01:00
Weblate
5e4009ca11 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-02-23 06:55:53 +00:00
Jonas Schäfer
80860a3ac6 Extract missing strings 2021-02-23 07:55:38 +01:00
Kim Alvefur
e9d479a78b Translated using Weblate (Swedish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-02-22 23:23:17 +00:00
Jonas Schäfer
aac56f49e9 Added translation using Weblate (Swedish) 2021-02-22 18:12:31 +00:00
Jonas Schäfer
52f0bee006 Use buster-slim as base image
This reduces the overall image size, but more importantly,
deduplicates nicely with the other Snikket images which all use
buster-slim as base.

Fixes #63.
2021-02-22 15:02:14 +01:00
misiek
97c91b432d Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-22 11:02:09 +00:00
Jonas Schäfer
60647159f3 Merge pull request #62 from mmigel/patch-2
Update admin_invites.html
2021-02-20 15:55:42 +01:00
Michał Mazur
a21730f136 Update admin_invites.html
A better sounding variant "Expires" in place "Valid until".
2021-02-20 15:53:00 +01:00
Jonas Schäfer
e35ab1b723 Merge pull request #61 from snikket-im/enable-italian-translation
Enable Italian translation by default
2021-02-20 10:17:11 +01:00
Matthew Wild
4de4509fc9 Update __init__.py 2021-02-20 07:07:18 +00:00
misiek
93e3b325b1 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

Bonus: this also optimises the display of the circle by removing
O(n) backend requests.
2021-02-03 18:25:29 +01:00
riccio
387a989caa Translated using Weblate (English (United Kingdom))
Currently translated at 57.4% (127 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en_GB/
2021-02-02 21:01:57 +00:00
uira
ffab48cff0 Translated using Weblate (Indonesian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-02-02 21:01:57 +00:00
riccio
17bf7cb140 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-02 21:01:57 +00:00
GodGoldfish
408d837a0f Translated using Weblate (Russian)
Currently translated at 42.9% (95 of 221 strings)

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

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en/
2021-02-02 21:01:56 +00:00
Link Mauve
2aa3d629da Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-02 21:01:55 +00:00
Jonas Schäfer
6779341db3 Merge pull request #47 from snikket-im/feature/ci-no-push
Disable GitHub pushing to Docker Hub and include docker build in standard CI testing
2021-02-01 16:20:26 +01:00
Jonas Schäfer
10a0de0637 Add test build to standard CI 2021-02-01 16:17:06 +01:00
Jonas Schäfer
b3185a8d18 Remove build job pushing to docker hub
We don’t want that, Snikket got its own build servers.
2021-02-01 16:16:50 +01:00
Link Mauve
2db6cbe6fd Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:11:31 +00:00
GodGoldfish
9bc6e0b555 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:11:31 +00:00
Link Mauve
98a3eeba7c Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:09:57 +00:00
GodGoldfish
de97b08f01 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:09:57 +00:00
riccio
f2dc970731 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-01 11:20:44 +00:00
Jonas Schäfer
2f8e724104 Enable Polish by default 2021-01-31 20:51:03 +01:00
Jonas Schäfer
a3ab537de0 Add more translated languages to the list 2021-01-31 15:29:21 +01:00
Jonas Schäfer
b04c4fa42d Force setting the SNIKKET_DOMAIN as HTTP Host when talking to prosody
This is required if the portal is talking to a Prosody with
multiple HTTP domains and it doesn’t have http_default_host set...
In a true snikket, this doesn’t happen, but we all know...
2021-01-31 15:29:21 +01:00
Jonas Schäfer
078be4ba35 Added translation using Weblate (Italian) 2021-01-31 13:02:47 +00:00
Jonas Schäfer
c1f186a3da Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-01-31 12:54:49 +00:00
misiek
7aaeb0f368 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-31 12:54:48 +00:00
uira
b475e76189 Translated using Weblate (Indonesian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-31 12:54:48 +00:00
Jonas Schäfer
8b6f5e8e18 Added translation using Weblate (Russian) 2021-01-31 12:21:39 +00:00
116 changed files with 30270 additions and 4122 deletions

4
.djlintrc Normal file
View File

@@ -0,0 +1,4 @@
{
"extension": "html",
"profile": "django"
}

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
# Stop the editor from looking for .editorconfig files in the parent directories
root = true
[*]
# Non-configurable Prettier behaviors
charset = utf-8
insert_final_newline = true
# Caveat: Prettier wont trim trailing whitespace inside template strings, but your editor might.
trim_trailing_whitespace = true
# Configurable Prettier behaviors
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80

View File

@@ -1,23 +0,0 @@
name: Docker image build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
docker build . \
--build-arg=BUILD_SERIES=dev \
--build-arg=BUILD_ID="$(echo "$GITHUB_SHA" | head -c 12)" \
--tag snikket/snikket-web-portal:dev
- name: Log into registry
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
- name: Push the Docker image
run: docker push snikket/snikket-web-portal:dev

View File

@@ -1,31 +0,0 @@
---
name: Docker release image build
"on":
push:
tags:
- release/*.*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
echo "Building ref $GITHUB_REF...";
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
RELEASE_SERIES="${RELEASE_TAG%.*}";
RELEASE_VER="${RELEASE_TAG#$RELEASE_SERIES.}";
docker build . \
--build-arg=BUILD_SERIES="$RELEASE_SERIES" \
--build-arg=BUILD_ID="$RELEASE_VER" \
--tag snikket/snikket-web-portal:"$RELEASE_SERIES"
- name: Log into registry
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
- name: Push the Docker image
run: >-
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
RELEASE_SERIES="${RELEASE_TAG%.*}";
docker push snikket/snikket-web-portal:"$RELEASE_SERIES"

View File

@@ -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
@@ -44,7 +45,39 @@ jobs:
- name: Install
run: |
set -euo pipefail
pip install flake8
pip install flake8 flake8-print
- name: Linting
run: |
python -m flake8 snikket_web
make flake8
translation-check:
runs-on: ubuntu-latest
name: 'lint: i18n'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install
run: |
set -euo pipefail
pip install flask-babel
- name: Linting
run: |
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;/^"Generated-By: /d' snikket_web/translations/messages.pot
git diff --exit-code --color -- snikket_web/translations/messages.pot
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
docker build .

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ __pycache__
/snikket_web/static/css/*.css
/snikket_web/translations/*/LC_MESSAGES/*.mo
/.env
node_modules

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": [],
"overrides": [],
"proseWrap": "always"
}

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

8
.stylelintrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": ["stylelint-config-standard-scss"],
"rules": {
"color-hex-length": "long",
"color-function-notation": "legacy",
"alpha-value-notation": "number"
}
}

View File

@@ -1,54 +1,53 @@
FROM debian:buster
ARG BUILD_SERIES=dev
ARG BUILD_ID=0
ENV DEBIAN_FRONTEND noninteractive
# This Dockerfile attempts to strike a balance between image size and time it
# takes to do an incremental build on changes.
# Improvements welcome.
FROM debian:bookworm-slim AS build
RUN set -eu; \
export DEBIAN_FRONTEND=noninteractive ; \
apt-get update ; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-setuptools python3-wheel \
libpython3-dev \
make build-essential \
; \
apt-get clean ; rm -rf /var/lib/apt/lists
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
pip3 install -r requirements.txt; \
pip3 install -r build-requirements.txt; \
rm -rf /root/.cache;
python3 python3-mypy python3-dotenv python3-toml python3-babel python3-distutils \
sassc make;
COPY Makefile /opt/snikket-web-portal/Makefile
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
# NOTE: abusing true(1) as a terrible way to disable a specific command. If
# one merged all the RUN commands into one, one would want to run the
# uninstall/remove commands there, but with the split up RUN commands it is
# rather pointless.
RUN set -eu; \
make; \
true pip3 uninstall -yr build-requirements.txt; \
true apt-get remove -y build-essential make libpython3-dev; \
true apt-get autoremove -y; \
pip3 install hypercorn; \
rm -rf /root/.cache; \
apt-get clean ; rm -rf /var/lib/apt/lists
WORKDIR /opt/snikket-web-portal
RUN make
FROM debian:bookworm-slim
ARG BUILD_SERIES=dev
ARG BUILD_ID=0
COPY docker/env.py /etc/snikket-web-portal/env.py
ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
export DEBIAN_FRONTEND=noninteractive ; \
apt-get update ; \
apt-get install -y --no-install-recommends \
netcat-traditional python3 python3-setuptools python3-pip \
python3-aiohttp python3-email-validator python3-flask-babel \
python3-flaskext.wtf python3-hsluv python3-hypercorn \
python3-quart python3-typing-extensions python3-wtforms ; \
pip3 install --break-system-packages environ-config ; \
apt-get remove -y --purge python3-pip python3-setuptools; \
apt-get clean ; rm -rf /var/lib/apt/lists; \
rm -rf /root/.cache;
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
COPY --from=build /opt/snikket-web-portal/snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
RUN echo "$BUILD_SERIES $BUILD_ID" > /opt/snikket-web-portal/.app_version
ADD docker/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

View File

@@ -5,8 +5,10 @@ generated_css_files = $(patsubst snikket_web/scss/%.scss,snikket_web/static/css/
translation_basepath = snikket_web/translations
pot_file = $(translation_basepath)/messages.pot
black_formatted_py = snikket_web/prosodyclient.py
PYTHON3 ?= python3
SCSSC ?= $(PYTHON3) -m scss --load-path snikket_web/scss/
SCSSC ?= sassc --load-path snikket_web/scss/
all: build_css compile_translations
@@ -14,7 +16,7 @@ build_css: $(generated_css_files)
$(generated_css_files): snikket_web/static/css/%.css: snikket_web/scss/%.scss $(scss_files) $(scss_includes)
mkdir -p snikket_web/static/css/
$(SCSSC) -o "$@" "$<"
$(SCSSC) "$<" "$@"
clean:
rm -f $(generated_css_files)
@@ -34,4 +36,20 @@ compile_translations:
-pybabel compile -d $(translation_basepath)
.PHONY: lint
lint: format flake8
.PHONY: format
format:
$(PYTHON3) -m black $(black_formatted_py)
.PHONY: flake8
flake8:
$(PYTHON3) -m flake8 --exclude=$(subst $(space),$(comma),$(strip $(black_formatted_py))) snikket_web
$(PYTHON3) -m flake8 --ignore=E501,W503 $(black_formatted_py)
.PHONY: mypy
mypy:
$(PYTHON3) -m mypy --python-version 3.11 snikket_web
.PHONY: build_css clean update_translations compile_translations extract_translations force_update_translations

View File

@@ -1,4 +1,3 @@
[python: snikket_web/**.py]
[jinja2: snikket_web/templates/**.html]
[jinja2: snikket_web/templates/**.j2]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@@ -1,3 +1,3 @@
pyscss~=1.3
mypy
python-dotenv~=0.15
types-toml

View File

@@ -1,5 +1,16 @@
#!/bin/sh
export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN"
if [ -n "${SNIKKET_SITE_NAME:-}" ]; then
export SNIKKET_WEB_SITE_NAME="$SNIKKET_SITE_NAME"
fi
exec hypercorn -b "127.0.0.1:5765" 'snikket_web:create_app()'
export SNIKKET_WEB_TOS_URI="${SNIKKET_TOS_URI}"
export SNIKKET_WEB_PRIVACY_URI="${SNIKKET_PRIVACY_URI}"
export SNIKKET_WEB_ABUSE_EMAIL="${SNIKKET_ABUSE_EMAIL}"
export SNIKKET_WEB_SECURITY_EMAIL="${SNIKKET_SECURITY_EMAIL}"
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}"
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}"
exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" --access-logfile=- --log-file=- 'snikket_web:create_app()'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -37,7 +37,6 @@ SNIKKET_WEB_DOMAIN='localhost'
#
#SNIKKET_WEB_SITE_NAME
# OPTIONAL SETTINGS
# =================
@@ -48,3 +47,7 @@ SNIKKET_WEB_DOMAIN='localhost'
# not re-transferred.
#
#SNIKKET_WEB_AVATAR_CACHE_TTL=1800
# Your XMPP account name and email address, to be shown on
# the homepage.
#SNIKKET_WEB_ADMIN_CONTACT=""

View File

@@ -1,5 +1,5 @@
[mypy]
python_version = 3.7
python_version = 3.13
#warn_return_any = True
warn_unused_configs = True
disallow_untyped_calls = True

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "snikket-web-portal",
"version": "1.0.0",
"description": "",
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.12.2",
"devDependencies": {
"prettier": "^3.6.0",
"stylelint": "^16.21.0",
"stylelint-config-standard-scss": "^15.0.1"
}
}

971
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,971 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
prettier:
specifier: ^3.6.0
version: 3.6.0
stylelint:
specifier: ^16.21.0
version: 16.21.0
stylelint-config-standard-scss:
specifier: ^15.0.1
version: 15.0.1(postcss@8.5.6)(stylelint@16.21.0)
packages:
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@csstools/media-query-list-parser@4.0.3':
resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/selector-specificity@5.0.0':
resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==}
engines: {node: '>=18'}
peerDependencies:
postcss-selector-parser: ^7.0.0
'@dual-bundle/import-meta-resolve@4.1.0':
resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==}
'@keyv/serialize@1.0.3':
resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
astral-regex@2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
cacheable@1.10.0:
resolution: {integrity: sha512-SSgQTAnhd7WlJXnGlIi4jJJOiHzgnM5wRMEPaXAU4kECTAMpBoYKoZ9i5zHmclIEZbxcu3j7yY/CF8DTmwIsHg==}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
css-functions-list@3.2.3:
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
engines: {node: '>=12 || >=16'}
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
fastest-levenshtein@1.0.16:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
engines: {node: '>= 4.9.1'}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
file-entry-cache@10.1.1:
resolution: {integrity: sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
flat-cache@6.1.10:
resolution: {integrity: sha512-B6/v1f0NwjxzmeOhzfXPGWpKBVA207LS7lehaVKQnFrVktcFRfkzjZZ2gwj2i1TkEUMQht7ZMJbABUT5N+V1Nw==}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
global-modules@2.0.0:
resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
engines: {node: '>=6'}
global-prefix@3.0.0:
resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
engines: {node: '>=6'}
globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
globjoin@0.1.4:
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
hookified@1.9.1:
resolution: {integrity: sha512-u3pxtGhKjcSXnGm1CX6aXS9xew535j3lkOCegbA6jdyh0BaAjTbXI4aslKstCr6zUNtoCxFGFKwjbSHdGrMB8g==}
html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
keyv@5.3.4:
resolution: {integrity: sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==}
kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
known-css-properties@0.36.0:
resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==}
known-css-properties@0.37.0:
resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
mdn-data@2.21.0:
resolution: {integrity: sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==}
meow@13.2.0:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
postcss-media-query-parser@0.2.3:
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
postcss-resolve-nested-selector@0.1.6:
resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
postcss-safe-parser@7.0.1:
resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
engines: {node: '>=18.0'}
peerDependencies:
postcss: ^8.4.31
postcss-scss@4.0.9:
resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.4.29
postcss-selector-parser@7.1.0:
resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
prettier@3.6.0:
resolution: {integrity: sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==}
engines: {node: '>=14'}
hasBin: true
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
stylelint-config-recommended-scss@15.0.1:
resolution: {integrity: sha512-V24bxkNkFGggqPVJlP9iXaBabwSGEG7QTz+PyxrRtjPkcF+/NsWtB3tKYvFYEmczRkWiIEfuFMhGpJFj9Fxe6Q==}
engines: {node: '>=20'}
peerDependencies:
postcss: ^8.3.3
stylelint: ^16.16.0
peerDependenciesMeta:
postcss:
optional: true
stylelint-config-recommended@16.0.0:
resolution: {integrity: sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA==}
engines: {node: '>=18.12.0'}
peerDependencies:
stylelint: ^16.16.0
stylelint-config-standard-scss@15.0.1:
resolution: {integrity: sha512-8pmmfutrMlPHukLp+Th9asmk21tBXMVGxskZCzkRVWt1d8Z0SrXjUUQ3vn9KcBj1bJRd5msk6yfEFM0UYHBRdg==}
engines: {node: '>=20'}
peerDependencies:
postcss: ^8.3.3
stylelint: ^16.18.0
peerDependenciesMeta:
postcss:
optional: true
stylelint-config-standard@38.0.0:
resolution: {integrity: sha512-uj3JIX+dpFseqd/DJx8Gy3PcRAJhlEZ2IrlFOc4LUxBX/PNMEQ198x7LCOE2Q5oT9Vw8nyc4CIL78xSqPr6iag==}
engines: {node: '>=18.12.0'}
peerDependencies:
stylelint: ^16.18.0
stylelint-scss@6.12.1:
resolution: {integrity: sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==}
engines: {node: '>=18.12.0'}
peerDependencies:
stylelint: ^16.0.2
stylelint@16.21.0:
resolution: {integrity: sha512-ki3PpJGG7xhm3WtINoWGnlvqAmbqSexoRMbEMJzlwewSIOqPRKPlq452c22xAdEJISVi80r+I7KL9GPUiwFgbg==}
engines: {node: '>=18.12.0'}
hasBin: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-hyperlinks@3.2.0:
resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
engines: {node: '>=14.18'}
svg-tags@1.0.0:
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
snapshots:
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/helper-validator-identifier@7.27.1': {}
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
'@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)':
dependencies:
postcss-selector-parser: 7.1.0
'@dual-bundle/import-meta-resolve@4.1.0': {}
'@keyv/serialize@1.0.3':
dependencies:
buffer: 6.0.3
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.0.6
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
argparse@2.0.1: {}
array-union@2.1.0: {}
astral-regex@2.0.0: {}
balanced-match@2.0.0: {}
base64-js@1.5.1: {}
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
cacheable@1.10.0:
dependencies:
hookified: 1.9.1
keyv: 5.3.4
callsites@3.1.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
colord@2.9.3: {}
cosmiconfig@9.0.0:
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.0
parse-json: 5.2.0
css-functions-list@3.2.3: {}
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
cssesc@3.0.0: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
emoji-regex@8.0.0: {}
env-paths@2.2.1: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
fast-deep-equal@3.1.3: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fast-uri@3.0.6: {}
fastest-levenshtein@1.0.16: {}
fastq@1.19.1:
dependencies:
reusify: 1.1.0
file-entry-cache@10.1.1:
dependencies:
flat-cache: 6.1.10
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
flat-cache@6.1.10:
dependencies:
cacheable: 1.10.0
flatted: 3.3.3
hookified: 1.9.1
flatted@3.3.3: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
global-modules@2.0.0:
dependencies:
global-prefix: 3.0.0
global-prefix@3.0.0:
dependencies:
ini: 1.3.8
kind-of: 6.0.3
which: 1.3.1
globby@11.1.0:
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.3.3
ignore: 5.3.2
merge2: 1.4.1
slash: 3.0.0
globjoin@0.1.4: {}
has-flag@4.0.0: {}
hookified@1.9.1: {}
html-tags@3.3.1: {}
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
imurmurhash@0.1.4: {}
ini@1.3.8: {}
is-arrayish@0.2.1: {}
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
is-plain-object@5.0.0: {}
isexe@2.0.0: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@1.0.0: {}
keyv@5.3.4:
dependencies:
'@keyv/serialize': 1.0.3
kind-of@6.0.3: {}
known-css-properties@0.36.0: {}
known-css-properties@0.37.0: {}
lines-and-columns@1.2.4: {}
lodash.truncate@4.4.2: {}
mathml-tag-names@2.1.3: {}
mdn-data@2.12.2: {}
mdn-data@2.21.0: {}
meow@13.2.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
ms@2.1.3: {}
nanoid@3.3.11: {}
normalize-path@3.0.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.27.1
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
path-type@4.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
postcss-media-query-parser@0.2.3: {}
postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@7.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-scss@4.0.9(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser@7.1.0:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
prettier@3.6.0: {}
queue-microtask@1.2.3: {}
require-from-string@2.0.2: {}
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
reusify@1.1.0: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
signal-exit@4.1.0: {}
slash@3.0.0: {}
slice-ansi@4.0.0:
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
stylelint-config-recommended-scss@15.0.1(postcss@8.5.6)(stylelint@16.21.0):
dependencies:
postcss-scss: 4.0.9(postcss@8.5.6)
stylelint: 16.21.0
stylelint-config-recommended: 16.0.0(stylelint@16.21.0)
stylelint-scss: 6.12.1(stylelint@16.21.0)
optionalDependencies:
postcss: 8.5.6
stylelint-config-recommended@16.0.0(stylelint@16.21.0):
dependencies:
stylelint: 16.21.0
stylelint-config-standard-scss@15.0.1(postcss@8.5.6)(stylelint@16.21.0):
dependencies:
stylelint: 16.21.0
stylelint-config-recommended-scss: 15.0.1(postcss@8.5.6)(stylelint@16.21.0)
stylelint-config-standard: 38.0.0(stylelint@16.21.0)
optionalDependencies:
postcss: 8.5.6
stylelint-config-standard@38.0.0(stylelint@16.21.0):
dependencies:
stylelint: 16.21.0
stylelint-config-recommended: 16.0.0(stylelint@16.21.0)
stylelint-scss@6.12.1(stylelint@16.21.0):
dependencies:
css-tree: 3.1.0
is-plain-object: 5.0.0
known-css-properties: 0.36.0
mdn-data: 2.21.0
postcss-media-query-parser: 0.2.3
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.0
postcss-value-parser: 4.2.0
stylelint: 16.21.0
stylelint@16.21.0:
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0)
'@dual-bundle/import-meta-resolve': 4.1.0
balanced-match: 2.0.0
colord: 2.9.3
cosmiconfig: 9.0.0
css-functions-list: 3.2.3
css-tree: 3.1.0
debug: 4.4.1
fast-glob: 3.3.3
fastest-levenshtein: 1.0.16
file-entry-cache: 10.1.1
global-modules: 2.0.0
globby: 11.1.0
globjoin: 0.1.4
html-tags: 3.3.1
ignore: 7.0.5
imurmurhash: 0.1.4
is-plain-object: 5.0.0
known-css-properties: 0.37.0
mathml-tag-names: 2.1.3
meow: 13.2.0
micromatch: 4.0.8
normalize-path: 3.0.0
picocolors: 1.1.1
postcss: 8.5.6
postcss-resolve-nested-selector: 0.1.6
postcss-safe-parser: 7.0.1(postcss@8.5.6)
postcss-selector-parser: 7.1.0
postcss-value-parser: 4.2.0
resolve-from: 5.0.0
string-width: 4.2.3
supports-hyperlinks: 3.2.0
svg-tags: 1.0.0
table: 6.9.0
write-file-atomic: 5.0.1
transitivePeerDependencies:
- supports-color
- typescript
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-hyperlinks@3.2.0:
dependencies:
has-flag: 4.0.0
supports-color: 7.2.0
svg-tags@1.0.0: {}
table@6.9.0:
dependencies:
ajv: 8.17.1
lodash.truncate: 4.4.2
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
util-deprecate@1.0.2: {}
which@1.3.1:
dependencies:
isexe: 2.0.0
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
signal-exit: 4.1.0

23
pyproject.toml Normal file
View File

@@ -0,0 +1,23 @@
[project]
name = "snikket-web-portal"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"aiohttp~=3.12.13",
"email-validator~=1.3",
"environ-config~=20.0",
"flask-babel~=2.0,<3",
"flask-wtf~=1.1,<1.2",
"hsluv~=5.0",
"mypy>=1.16.1",
"python-dotenv~=0.15",
"types-toml>=0.10.8.20240310",
"typing-extensions>=4.14.0",
"werkzeug~=2.2,<3",
"wtforms~=3.0,<4",
]
[dependency-groups]
dev = ["quart~=0.18,<0.19"]

View File

@@ -1,8 +0,0 @@
aiohttp~=3.6
quart~=0.11
flask-wtf~=0.14
hsluv~=0.0.2
flask-babel~=1.0
email-validator~=1.1
environ-config~=20.0
typing-extensions

View File

@@ -18,10 +18,12 @@ from quart import (
jsonify,
)
import werkzeug.exceptions
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]:
@@ -40,7 +42,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
try:
user_info = await infra.client.get_user_info()
except (aiohttp.ClientError, quart.exceptions.HTTPException):
except (aiohttp.ClientError, werkzeug.exceptions.HTTPException):
user_info = {}
return {
@@ -48,6 +50,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
"text_to_css": colour.text_to_css,
"lang": infra.selected_locale(),
"user_info": user_info,
"is_in_debug_mode": current_app.debug,
}
@@ -104,16 +107,16 @@ async def backend_error_handler(exc: Exception) -> quart.Response:
async def generic_http_error(
exc: quart.exceptions.HTTPException,
exc: werkzeug.exceptions.HTTPException,
) -> quart.Response:
return quart.Response(
await render_template(
"generic_http_error.html",
status=exc.status_code,
status=exc.code,
description=exc.description,
name=exc.name,
),
status=exc.status_code,
status=exc.code,
)
@@ -143,9 +146,38 @@ class AppConfig:
domain = environ.var()
site_name = environ.var("")
avatar_cache_ttl = environ.var(1800, converter=int)
languages = environ.var(["de", "en"], converter=autosplit)
apple_store_url = environ.var("")
languages = environ.var([
# Keep `en` as the first language, because it is used as a fallback
# if the language negotiation cannot find another match. It is more
# likely that users are able to read english (or find a suitable
# online translator) than, for instance, danish.
"en",
"da",
"de",
"fr",
"id",
"it",
"pl",
"ru",
"sv",
"uk",
"zh_Hans_CN",
], converter=autosplit)
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)
tos_uri = environ.var("")
privacy_uri = environ.var("")
abuse_email = environ.var("")
security_email = environ.var("")
admin_contact = environ.var("")
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
@@ -157,7 +189,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
@@ -175,27 +207,37 @@ def create_app() -> quart.Quart:
app.config["SITE_NAME"] = config.site_name or config.domain
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.config["TOS_URI"] = config.tos_uri
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.config["ADMIN_CONTACT"] = config.admin_contact
app.context_processor(proc)
app.register_error_handler(
aiohttp.ClientConnectorError,
backend_error_handler, # type:ignore
backend_error_handler,
)
app.register_error_handler(
quart.exceptions.HTTPException,
werkzeug.exceptions.HTTPException,
generic_http_error, # type:ignore
)
app.register_error_handler(
Exception,
generic_error_handler, # type:ignore
generic_error_handler,
)
@app.route("/")
async def index() -> quart.Response:
async def index() -> typing.Union[str, werkzeug.Response]:
if infra.client.has_session:
return redirect(url_for('user.index'))
return redirect(url_for("user.index"))
return redirect(url_for('main.login'))
return await render_template("home.html")
@app.route("/site.webmanifest")
def site_manifest() -> quart.Response:

View File

@@ -1,5 +1,15 @@
version_info = (0, 1, 0, "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)"

View File

@@ -1,15 +1,17 @@
import asyncio
import json
import resource
import time
import typing
from datetime import datetime
import aiohttp
import werkzeug.exceptions
import quart.flask_patch
import wtforms
import wtforms.fields.html5
from quart import (
Blueprint,
@@ -18,13 +20,14 @@ from quart import (
url_for,
request,
abort,
flash,
current_app,
)
import flask_wtf
from flask_babel import lazy_gettext as _l
from flask_babel import lazy_gettext as _l, _
from . import prosodyclient
from .infra import client, circle_name
from . import prosodyclient, _version
from .infra import client, circle_name, BaseForm
bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -32,11 +35,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(flask_wtf.FlaskForm): # type: ignore
action_create = wtforms.StringField()
class PasswordResetLinkPost(BaseForm):
action_revoke = wtforms.StringField()
@@ -47,15 +53,129 @@ 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,
)
class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
class EditUserForm(BaseForm):
localpart = wtforms.StringField(
_l("Login name"),
)
display_name = wtforms.StringField(
_l("Display name"),
)
role = wtforms.RadioField(
_l("Access Level"),
choices=[
("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"),
)
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_user(localpart: str) -> typing.Union[werkzeug.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_,
))
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,
display_name=form.display_name.data,
role=form.role.data,
)
await flash(
_("User information updated."),
"success",
)
return redirect(url_for(".users"))
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:registered"
return await render_template(
"admin_edit_user.html",
target_user=target_user_info,
form=form,
)
class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField(
_l("Delete user permanently")
)
@@ -63,12 +183,16 @@ class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"])
@client.require_admin_session()
async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
async def delete_user(localpart: str) -> typing.Union[str, werkzeug.Response]:
target_user_info = await client.get_user_by_localpart(localpart)
form = DeleteUserForm()
if form.validate_on_submit():
if form.action_delete.data:
await client.delete_user_by_localpart(localpart)
await flash(
_("User deleted"),
"success",
)
return redirect(url_for(".users"))
return await render_template(
@@ -94,37 +218,47 @@ 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, werkzeug.Response]:
invite_info = await client.get_invite_by_id(
id_,
)
if invite_info.jid is None:
await flash(
_("Password reset link not found"),
"alert",
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
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,
)
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
class InvitesListForm(BaseForm):
action_revoke = wtforms.StringField()
class InvitePost(flask_wtf.FlaskForm): # type:ignore
class InvitePost(BaseForm):
circles = wtforms.SelectMultipleField(
_l("Invite to circle"),
# NOTE: This is for when/if we ever support multi-group invites.
@@ -157,6 +291,20 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
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")
)
@@ -180,7 +328,7 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
@bp.route("/invitations", methods=["GET", "POST"])
@client.require_admin_session()
async def invitations() -> typing.Union[str, quart.Response]:
async def invitations() -> typing.Union[str, werkzeug.Response]:
invites = sorted(
(
invite
@@ -218,7 +366,7 @@ async def invitations() -> typing.Union[str, quart.Response]:
)
class InviteForm(flask_wtf.FlaskForm): # type:ignore
class InviteForm(BaseForm):
action_revoke = wtforms.SubmitField(
_l("Revoke")
)
@@ -226,7 +374,7 @@ class InviteForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/invitation/-/new", methods=["POST"])
@client.require_admin_session()
async def create_invite() -> typing.Union[str, quart.Response]:
async def create_invite() -> typing.Union[str, werkzeug.Response]:
form = InvitePost()
circles = await client.list_groups()
form.circles.choices = [
@@ -236,13 +384,21 @@ async def create_invite() -> typing.Union[str, quart.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"),
"success",
)
return redirect(url_for(".edit_invite", id_=invite.id_))
return await render_template("admin_create_invite.html",
invite_form=form)
@@ -250,12 +406,16 @@ async def create_invite() -> typing.Union[str, quart.Response]:
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
async def edit_invite(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite_info = await client.get_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
abort(404)
await flash(
_("No such invitation exists"),
"alert",
)
return redirect(url_for(".invitations"))
circles = await client.list_groups()
circle_map = {
circle.id_: circle
@@ -266,6 +426,10 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
if form.action_revoke.data:
await client.delete_invite(id_)
await flash(
_("Invitation revoked"),
"success",
)
return redirect(url_for(".invitations"))
return redirect(url_for(".edit_invite", id_=id_))
@@ -278,7 +442,7 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
)
class CirclePost(flask_wtf.FlaskForm): # type:ignore
class CirclePost(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],
@@ -308,12 +472,16 @@ async def circles() -> str:
@bp.route("/circle/-/new", methods=["POST"])
@client.require_admin_session()
async def create_circle() -> typing.Union[str, quart.Response]:
async def create_circle() -> typing.Union[str, werkzeug.Response]:
create_form = CirclePost()
if create_form.validate_on_submit():
circle = await client.create_group(
name=create_form.name.data,
)
await flash(
_("Circle created"),
"success",
)
return redirect(url_for(".edit_circle", id_=circle.id_))
return await render_template(
@@ -322,7 +490,7 @@ async def create_circle() -> typing.Union[str, quart.Response]:
)
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
class EditCircleForm(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],
@@ -337,20 +505,18 @@ class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
_l("Update circle")
)
action_delete = wtforms.SubmitField(
_l("Delete circle permanently")
)
action_remove_user = wtforms.StringField()
action_add_user = wtforms.SubmitField(
_l("Add user")
)
action_remove_group_chat = wtforms.StringField()
@bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
@@ -359,27 +525,27 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
circle_members = await asyncio.gather(*(
client.get_user_by_localpart(
localpart,
session=session,
)
users = {
user.localpart: user
for user in await client.list_users()
}
circle_members = [
(localpart, users.get(localpart))
for localpart in sorted(circle.members)
))
users = await client.list_users()
]
form = EditCircleForm()
form.user_to_add.choices = sorted(
(
(u.localpart, u.localpart)
for u in users
if u.localpart not in circle.members
),
key=lambda x: x[1]
(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]
@@ -396,30 +562,292 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
id_,
new_name=form.name.data,
)
elif form.action_delete.data:
await client.delete_group(id_)
return redirect(url_for(".circles"))
await flash(
_("Circle data updated"),
"success",
)
elif form.action_add_user.data:
if form.user_to_add.data in valid_users:
print("is valid")
await client.add_group_member(
id_,
form.user_to_add.data,
)
await flash(
_("User added to circle"),
"success",
)
elif form.action_remove_user.data:
await client.remove_group_member(
id_,
form.action_remove_user.data,
)
await flash(
_("User removed from circle"),
"success",
)
elif form.action_remove_group_chat.data:
await client.remove_group_chat(
id_,
form.action_remove_group_chat.data,
)
await flash(
_("Chat removed from circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_))
else:
print(form.errors)
return await render_template(
"admin_edit_circle.html",
target_circle=circle,
form=form,
circle_chats=circle.chats,
circle_members=circle_members,
invite_form=invite_form,
)
class DeleteCircleForm(BaseForm):
action_delete = wtforms.SubmitField(
_l("Delete circle permanently")
)
@bp.route("/circle/<id_>/delete", methods=["GET", "POST"])
@client.require_admin_session()
async def delete_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
id_,
session=session,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
form = DeleteCircleForm()
if form.validate_on_submit():
if form.action_delete.data:
await client.delete_group(id_)
await flash(
_("Circle deleted"),
"success",
)
return redirect(url_for(".circles"))
return await render_template(
"admin_delete_circle.html",
target_circle=circle,
form=form,
)
class AddCircleChatForm(BaseForm):
name = wtforms.StringField(
_l("Group chat name"),
validators=[wtforms.validators.InputRequired()],
)
action_save = wtforms.SubmitField(
_l("Create group chat")
)
@bp.route("/circle/<id_>/add_chat", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle_add_chat(
id_: str
) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
id_,
session=session,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
form = AddCircleChatForm()
if form.validate_on_submit():
if form.action_save.data:
await client.add_group_chat(id_, form.name.data)
await flash(
_("New group chat added to circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_))
return await render_template(
"admin_create_circle_chat.html",
target_circle=circle,
group_chat_form=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(
_l("Message contents"),
widget=wtforms.widgets.TextArea(),
validators=[wtforms.validators.DataRequired()],
)
online_only = wtforms.BooleanField(
_l("Only send to online users"),
)
action_post_all = wtforms.SubmitField(
_l("Post to all users"),
)
action_send_preview = wtforms.SubmitField(
_l("Send preview to yourself"),
)
@bp.route("/system/", methods=["GET", "POST"])
@client.require_admin_session()
async def system() -> typing.Union[str, werkzeug.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 werkzeug.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
try:
metrics["prosody_uploads"] = prosody_metrics["uploads"]
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
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,
)

View File

@@ -1,16 +1,22 @@
import base64
import itertools
import math
import secrets
import typing
from datetime import datetime, timedelta, timezone
import quart.flask_patch # noqa:F401
from quart import (
current_app,
request,
g,
)
import flask_babel
from flask_babel import _
import flask_wtf
from flask_babel import lazy_gettext as _l
import flask_babel as _
from . import prosodyclient
@@ -21,11 +27,21 @@ 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:
g.language_header_accessed = True
selected = request.accept_languages.best_match(
current_app.config['LANGUAGES']
)
) or current_app.config['LANGUAGES'][0]
return selected
@@ -37,21 +53,96 @@ 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
def format_bytes(n: float) -> str:
try:
scale = max(math.floor(math.log(n, 1024)), 0)
except ValueError:
scale = 0
try:
unit = BYTE_UNIT_SCALE_MAP[scale]
factor = 1024**scale
except IndexError:
unit = "TiB"
factor = 1024**4
if factor > 1:
return "{:.1f}{}".format(n / factor, unit)
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")
return resp
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)
app.template_filter("format_last_activity")(format_last_activity)
app.context_processor(template_now)
app.after_request(add_vary_language_header)
def generate_error_id() -> str:
return base64.b32encode(secrets.token_bytes(8)).decode(
"ascii"
).rstrip("=")
class BaseForm(flask_wtf.FlaskForm): # type:ignore
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
meta = kwargs["meta"] = dict(kwargs.get("meta", {}))
if "locales" not in meta:
locale = flask_babel.get_locale()
if locale:
meta["locales"] = [str(locale)]
super().__init__(*args, **kwargs)

View File

@@ -10,16 +10,18 @@ from quart import (
current_app,
render_template,
redirect,
request,
url_for,
session as http_session,
)
import werkzeug
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l
from flask_babel import lazy_gettext as _l, gettext
from .infra import client, selected_locale
from .infra import client, selected_locale, BaseForm
bp = Blueprint("invite", __name__)
@@ -27,6 +29,11 @@ bp = Blueprint("invite", __name__)
INVITE_SESSION_JID = "invite-session-jid"
MAX_IMPORT_DATA_SIZE = 5*1024*1024 # 5MB
SUPPORTED_IMPORT_TYPES = ["application/xml", "text/xml"]
EIMPORTTOOBIG = _l("The account data you tried to import is too large to"
" upload. Please contact your Snikket operator.")
# https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1
@@ -40,21 +47,38 @@ def apple_store_badge() -> str:
return url_for("static", filename="img/apple/en.svg")
def play_store_badge() -> str:
locale = selected_locale()
filename = "{}_badge_web_generic.png".format(locale)
static_path = pathlib.Path(__file__).parent / "static" / "img" / "google"
if (static_path / filename).exists():
return url_for("static", filename="img/google/{}".format(filename))
return url_for("static", filename="img/google/en_badge_web_generic.png")
@bp.context_processor
def context() -> typing.Mapping[str, typing.Any]:
def context() -> typing.Dict[str, typing.Any]:
return {
"apple_store_badge": apple_store_badge,
"play_store_badge": play_store_badge,
}
@bp.route("/<id_>")
async def view(id_: str) -> str:
async def view_old(id_: str) -> werkzeug.Response:
return redirect(url_for(".view", id_=id_))
@bp.route("/<id_>/")
async def view(id_: str) -> typing.Union[quart.Response,
typing.Tuple[str, int],
str]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
# invite expired
return await render_template("invite_invalid.html")
return await render_template("invite_invalid.html"), 404
raise
if invite.reset_localpart is not None:
@@ -79,22 +103,33 @@ async def view(id_: str) -> str:
)
apple_store_url = current_app.config["APPLE_STORE_URL"]
return await render_template(
body = await render_template(
"invite_view.html",
invite=invite,
play_store_url=play_store_url,
apple_store_url=apple_store_url,
f_droid_url="market://details?id=org.snikket.android",
invite_id=id_,
)
return quart.Response(
body,
headers={
"Link": "<{}>; rel=\"alternate\"".format(invite.xmpp_uri),
}
)
class RegisterForm(flask_wtf.FlaskForm): # type:ignore
class RegisterForm(BaseForm):
localpart = wtforms.StringField(
_l("Username"),
)
password = wtforms.PasswordField(
_l("Password"),
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.Length(min=10),
],
)
password_confirm = wtforms.PasswordField(
@@ -102,7 +137,7 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"password",
_l("The passwords must match")
_l("The passwords must match.")
)]
)
@@ -112,7 +147,7 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]:
async def register(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -134,15 +169,15 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
except aiohttp.ClientResponseError as exc:
if exc.status == 409:
form.localpart.errors.append(
_l("That username is already taken")
_l("That username is already taken.")
)
elif exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 400:
form.localpart.errors.append(
_l("The username is not valid")
_l("The username is not valid.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
@@ -150,6 +185,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
raise
else:
http_session[INVITE_SESSION_JID] = jid
await client.login(jid, form.password.data)
return redirect(url_for(".success"))
return await render_template(
@@ -159,9 +195,13 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
)
class ResetForm(flask_wtf.FlaskForm): # type:ignore
class ResetForm(BaseForm):
password = wtforms.PasswordField(
_l("Password"),
validators=[
wtforms.validators.InputRequired(),
wtforms.validators.Length(min=10),
],
)
password_confirm = wtforms.PasswordField(
@@ -169,7 +209,7 @@ class ResetForm(flask_wtf.FlaskForm): # type:ignore
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"password",
_l("The passwords must match")
_l("The passwords must match.")
)]
)
@@ -179,7 +219,7 @@ class ResetForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/<id_>/reset", methods=["GET", "POST"])
async def reset(id_: str) -> typing.Union[str, quart.Response]:
async def reset(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -202,7 +242,7 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
except aiohttp.ClientResponseError as exc:
if exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
@@ -219,11 +259,55 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
)
class DataImportForm(BaseForm):
account_data_file = wtforms.FileField(
_l("Account data file")
)
action_import = wtforms.SubmitField(
_l("Import data")
)
@bp.route("/success", methods=["GET", "POST"])
@client.require_session()
async def success() -> str:
form = DataImportForm()
if form.validate_on_submit():
ok = True
file_info = (await request.files).get(form.account_data_file.name)
if file_info is not None:
mimetype = file_info.mimetype
data = file_info.stream.read()
if len(data) > MAX_IMPORT_DATA_SIZE:
form.account_data_file.errors.append(EIMPORTTOOBIG)
ok = False
elif mimetype not in SUPPORTED_IMPORT_TYPES:
form.account_data_file.errors.append(
# not breaking the line here to avoid extract
# translations failing (defensive)
gettext("The account data you tried to import is in an unknown format. Please upload an XML file in XEP-0227 format (provided format: %(mimetype)s).", mimetype=mimetype), # noqa:E501
)
ok = False
elif len(data) > 0:
await client.import_account_data(data)
if ok:
# Re-render success page, this time with no import option
return await render_template(
"invite_success.html",
jid=http_session.get(INVITE_SESSION_JID, ""),
migration_success=True,
)
return await render_template(
"invite_success.html",
jid=http_session.get(INVITE_SESSION_JID, ""),
migration_success=False,
form=form,
max_import_size=MAX_IMPORT_DATA_SIZE,
import_too_big_warning_header=_l("Error"),
import_too_big_warning=EIMPORTTOOBIG,
)
@@ -236,5 +320,5 @@ async def reset_success() -> str:
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))

View File

@@ -15,24 +15,26 @@ from quart import (
render_template,
request,
Response,
flash,
)
import werkzeug.exceptions
import babel
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from . import xmpputil, _version
from .infra import client
from .infra import client, BaseForm
bp = quart.Blueprint("main", __name__)
class LoginForm(flask_wtf.FlaskForm): # type:ignore
address = wtforms.TextField(
class LoginForm(BaseForm):
address = wtforms.StringField(
_l("Address"),
validators=[wtforms.validators.InputRequired()],
)
@@ -48,12 +50,15 @@ class LoginForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
@bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]:
async def login() -> typing.Union[str, werkzeug.Response]:
if client.has_session and (await client.test_session()):
return redirect(url_for('user.index'))
@@ -63,34 +68,55 @@ async def login() -> typing.Union[str, quart.Response]:
localpart, domain, resource = xmpputil.split_jid(jid)
if not localpart:
localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"]
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
form.password.errors.append(
_("Invalid username or password.")
)
if domain != current_app.config["SNIKKET_DOMAIN"]:
# (a) prosody throws a 400 at us and I prefer to catch that here
# and (b) I dont want to pass on this obviously not-for-here
# password further than necessary.
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
return redirect(url_for('user.index'))
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except werkzeug.exceptions.Unauthorized:
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
await flash(
_("Login successful!"),
"success"
)
return redirect(url_for('user.index'))
return await render_template("login.html", form=form)
@bp.route("/meta/about.html")
async def about() -> str:
version = None
core_versions = {}
extra_versions = {}
if current_app.debug or client.is_admin_session:
version = _version.version
try:
core_versions["Prosody"] = await client.get_server_version()
except werkzeug.exceptions.Unauthorized:
core_versions["Prosody"] = "unknown"
if current_app.debug:
extra_versions["Quart"] = quart.__version__
extra_versions["aiohttp"] = aiohttp.__version__
extra_versions["babel"] = babel.__version__
extra_versions["wtforms"] = wtforms.__version__
extra_versions["flask-wtf"] = flask_wtf.__version__
try:
extra_versions["Prosody"] = await client.get_server_version()
except werkzeug.exceptions.Unauthorized:
extra_versions["Prosody"] = "unknown"
return await render_template(
"about.html",
version=_version.version,
version=version,
extra_versions=extra_versions,
core_versions=core_versions,
)
@@ -105,6 +131,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:
@@ -144,3 +171,44 @@ async def avatar(from_: str, code: str) -> quart.Response:
response.set_data(data)
return response
@bp.route("/terms")
async def terms() -> Response:
if not current_app.config["TOS_URI"]:
return Response("", 404)
return Response("", status=303, headers={
"Location": current_app.config["TOS_URI"],
})
@bp.route("/privacy")
async def privacy() -> Response:
if not current_app.config["PRIVACY_URI"]:
return Response("", 404)
return Response("", status=303, headers={
"Location": current_app.config["PRIVACY_URI"],
})
# This is linked from the iOS app and about page
@bp.route("/policies/")
async def policies() -> str:
return await render_template(
"policies.html",
)
@bp.route("/.well-known/security.txt")
async def securitytxt() -> Response:
return Response(
await render_template("security.txt"),
mimetype="text/plain;charset=UTF-8",
)
@bp.route("/_health")
async def health() -> Response:
return Response("STATUS OK", content_type="text/plain")

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -33,13 +33,35 @@ body {
main {
padding: $w-l1;
margin-left: auto;
max-width: 60rem;
margin-right: auto;
}
#mwrap {
flex: 1;
display: flex;
flex-direction: row-reverse;
> .filler, > .flashbox {
flex: 1 1 1rem;
}
> main {
flex: 0 1 60rem;
}
}
@media screen and (max-width: $large-screen-threshold) {
#mwrap {
display: block;
> main {
margin-left: auto;
margin-right: auto;
}
}
}
.flashbox > div.box > :first-child {
margin-top: 0;
}
/* top bar */
@@ -67,6 +89,10 @@ div#topbar {
font-size: $_top-h-size;
line-height: 1.5;
body.debug & {
color: red;
}
@media screen and (max-width: $small-screen-threshold) {
font-size: $_top-h-small-size;
}
@@ -134,22 +160,20 @@ body > footer {
background-color: $gray-100;
color: $gray-800;
padding: 0 $w-l1;
font-size: 92.21079115%;
ul {
display: block;
padding: 0;
margin: 0;
list-style-type: none;
text-align: center;
line-height: 1.6267076567643135;
}
li {
display: inline-block;
margin: $w-l1 0;
}
li:before {
content: '';
padding-right: $w-s2;
display: block;
margin: $w-s1 0;
}
a, a:visited, a:hover, a:active, a:focus {
@@ -235,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;
@@ -251,22 +282,22 @@ div.form.layout-expanded {
}
@each $type in $text-entry-inputs {
input[type=$type] {
input[type=#{$type}] {
width: 100%;
border: none;
border-bottom: $w-s4 solid $primary-500;
margin-bottom: -$w-s4;
}
input[type=$type].has-error {
input[type=#{$type}].has-error {
border-right: $w-s4 solid $alert-500;
}
input[type=$type]:hover {
input[type=#{$type}]:hover {
border-bottom-color: $primary-700;
}
input[type=$type]:focus {
input[type=#{$type}]:focus {
border-bottom-color: $primary-800;
}
}
@@ -330,6 +361,19 @@ 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;
}
.radio-button-ext {
margin-left: 0.5rem;
}
div.select-wrap {
display: block;
border-bottom: $w-s4 solid $primary-500;
@@ -613,69 +657,6 @@ input[type="submit"], button, .button {
/* button, .button {
margin: 0 $w-s2;
}
button.lv-primary, .button.lv-primary {
background-color: $gray-500;
color: $gray-900;
border-radius: $w-s4;
border: $w-s4 solid $gray-400;
@each $type, $values in $colours {
&.c-#{$type} {
border-color: nth($values, 4);
background-color: nth($values, 5);
color: nth($values, 9);
}
&.c-#{$type}:hover {
background-color: nth($values, 4);
}
}
}
button.lv-secondary, .button.lv-secondary {
background-color: $gray-700;
color: $gray-100;
border-radius: $w-s4;
@each $type, $values in $colours {
&.c-#{$type} {
background-color: nth($values, 7);
color: nth($values, 1);
}
}
}
button.lv-tertiary, .button.lv-tertiary {
background-color: inherit;
color: $gray-300;
border-radius: $w-s4;
text-decoration: underline;
@each $type, $values in $colours {
&.c-#{$type} {
color: nth($values, 3);
}
}
}
*/
/*
button.lv-secondary.c-#{$type}, .button.lv-secondary.c-#{$type} {
background-color: nth($values, 7);
color: nth($values, 1);
}
button.lv-tertiary.c-#{$type}, .button.lv-tertiary.c-#{$type} {
color: nth($values, 3);
text-decoration: underline;
background-color: transparent;
}
}*/
/* boxes */
.box {
@@ -738,8 +719,7 @@ button.lv-tertiary, .button.lv-tertiary {
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;
@@ -993,21 +973,37 @@ div.profile-card {
display: none;
}
}
input[type="submit"], button, .button {
&.slimmify {
> svg.icon {
margin-right: 0;
}
> span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
top: -100px;
}
}
}
}
/* clipboard button */
/* 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 {
@@ -1071,7 +1067,7 @@ pre.guru-meditation {
}
@each $type in $text-entry-inputs {
input[type=$type] {
input[type=#{$type}] {
background-color: black;
}
@@ -1081,6 +1077,10 @@ pre.guru-meditation {
}
}
label, legend {
color: $gray-800 !important;
}
.box {
background-color: black;
border-color: $gray-800;
@@ -1215,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 */
@@ -1265,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%;
}
}

194
snikket_web/scss/home.scss Normal file
View File

@@ -0,0 +1,194 @@
/*
* Reset
*/
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
li,
img,
figure,
figcaption,
table,
td,
tr,
thead,
tbody,
details,
summary {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Text related colors */
--copy: #444444;
--copy-darker: #242424;
--copy-darker-2: #121212;
--copy-lighter: #6b6b6b;
--copy-lighter-2: #c4c4c4;
--hyperlink: #155bca;
/* Background related colors */
--bg: #fffaef;
/*
* Colors
*/
--primary-color: #ff94a1;
--primary-color-half-light: #ffc6c8;
--secondary-color: #97aa9b;
--secondary-color-half-light: #e5e6d9;
--secondary-color-light: #cbd2c4;
}
html,
body {
margin: 0;
padding: 0;
font-size: 18px;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
justify-content: center;
align-items: center;
font-family: Verdana, sans-serif;
text-align: left;
word-wrap: break-word;
overflow-wrap: break-word;
background-color: var(--bg);
line-height: 1.5rem;
}
article,
section,
header,
footer {
a {
--transition-property: inherit;
--color: var(--copy);
--hover-color: var(--primary-color);
--hover-thickness: 0.2rem;
--text-decoration: underline;
--text-decoration-color: var(--color);
color: var(--color);
text-decoration: none;
&[href]:not([aria-disabled="true"], [aria-current="page"]) {
text-decoration: var(--text-decoration);
text-decoration-line: var(--text-decoration);
text-decoration-color: var(--text-decoration-color);
text-decoration-thickness: 1px;
text-decoration-skip-ink: none;
transition-property:
text-decoration-thickness, text-decoration-color,
var(--transition-property);
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-timing-function);
&:hover,
&:active,
&:focus {
text-decoration-color: var(--hover-color);
text-decoration-thickness: var(--hover-thickness);
}
}
&[rel~="external"]:not(:has(img))::after {
display: inline-block;
content: "";
width: 12px;
height: 12px;
line-height: inherit;
vertical-align: middle;
background: var(--color);
mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIj4KICAJPHBhdGggZmlsbD0iIzAwMDAwMCIgZD0iTTYgMWg1djVMOC44NiAzLjg1IDQuNyA4IDQgNy4zbDQuMTUtNC4xNnpNMiAzaDJ2MUgydjZoNlY4aDF2MmExIDEgMCAwIDEtMSAxSDJhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTEiLz4KPC9zdmc+");
margin-left: 0.2em;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
> a {
color: var(--copy);
}
}
}
main.home {
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
max-width: 450px;
> header {
> img {
width: 250px;
margin-bottom: 0.3rem;
}
> h1 {
text-align: center;
font-size: 3rem;
line-height: 3rem;
font-weight: 600;
}
}
> section {
text-align: center;
> p {
margin-bottom: 1.25rem;
text-wrap: balance;
&:last-child {
margin-bottom: 0;
}
}
}
> footer {
> nav {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 1rem;
> a {
flex: 1 1 50%;
padding: 0.5rem 0.7rem;
border-radius: 0.2rem 0.5rem;
&[href]:not([aria-disabled="true"], [aria-current="page"]) {
transition-property:
background-color, text-decoration-thickness, text-decoration-color;
&:hover {
background-color: var(--secondary-color-half-light);
}
}
&[aria-current="page"] {
font-weight: 600;
background-color: var(--primary-color);
}
}
}
}
}

View File

@@ -54,6 +54,8 @@ div.install-buttons {
ul {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: $w-l1 0;
padding: 0;
@@ -74,58 +76,8 @@ img.play {
height: $w-l3;
}
.tabbox {
display: flex;
flex-direction: column;
margin: $w-l1 0;
> nav.tabs {
display: flex;
flex-direction: row;
> a {
display: inline-block;
padding: $w-s2;
border-top-left-radius: $w-s4;
border-top-right-radius: $w-s4;
&, &:visited {
color: inherit;
text-decoration: underline;
text-decoration-color: $accent-500;
}
&:hover {
background: $accent-900;
border-color: $accent-800;
color: black;
}
&.active {
text-decoration: none;
background: linear-gradient(0deg, $accent-600, $accent-700);
color: $accent-200;
&:hover, &:focus {
background: linear-gradient(0deg, $accent-700, $accent-800);
}
&:active {
background: $accent-600;
}
}
}
}
> .tab-pane {
display: none;
padding: 0 $w-0;
background: $accent-900;
&.active {
display: block;
}
}
img.fdroid {
height: $w-l3;
}
.qr {

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -37,6 +37,26 @@ 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: 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" />
<path d="M8.65 3.35L5.86 6.14c-.32.31-.1.85.35.85H8V13c0 .55.45 1 1 1s1-.45 1-1V6.99h1.79c.45 0 .67-.54.35-.85L9.35 3.35c-.19-.19-.51-.19-.7 0zM16 17.01V11c0-.55-.45-1-1-1s-1 .45-1 1v6.01h-1.79c-.45 0-.67.54-.35.85l2.79 2.78c.2.19.51.19.71 0l2.79-2.78c.32-.31.09-.85-.35-.85H16z" />
</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 +67,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 +98,26 @@ 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: file/file_download/materialicons/24px.svg -->
<symbol id="icon-download" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
</symbol>
<!-- from: file/file_upload/materialicons/24px.svg -->
<symbol id="icon-upload" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</symbol>
<!-- from: file/folder/materialiconsround/24px.svg -->
<symbol id="icon-folder" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M10.59 4.59C10.21 4.21 9.7 4 9.17 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-1.41-1.41z" />
</symbol>
<!-- from: navigation/arrow_back/materialiconsround/24px.svg -->
<symbol id="icon-back" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
@@ -102,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" />
@@ -137,4 +188,14 @@ 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>
<!-- 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: 14 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ns1="http://sozi.baierouge.fr" xmlns:cc="http://creativecommons.org/ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" id="svg2" xml:space="preserve" viewBox="0 0 39.287 34.67" version="1.1" inkscape:version="0.91 r13725">
<g id="g10" transform="matrix(1.25 0 0 -1.25 0 34.67)">
<g id="g12">
<path id="path14" style="fill-rule:evenodd;fill:#f9f5ea" d="m8.988 22.388 5.516 2.977 0.992-0.442c0.77-0.292 1.543-0.515 2.316-0.66 1.25-0.257 1.985-0.515 2.204-0.773l-4.743-2.535c-0.734 0.218-1.726 0.457-2.976 0.715l-3.309 0.718z" inkscape:connector-curvature="0"/>
<path id="path16" style="fill-rule:evenodd;fill:#e8e1c5" d="m9.375 22.775 3.859-0.551c0.625-0.113 1.414-0.48 2.371-1.105l-0.332-7.332-0.769 0.328-5.129 1.656v7.004z" inkscape:connector-curvature="0"/>
<path id="path18" style="fill-rule:evenodd;fill:#a79e81" d="m15.605 21.119 4.411 2.371v-6.949c-0.477-0.368-1.028-0.735-1.653-1.102-0.59-0.367-1.617-0.918-3.09-1.652l0.332 7.332z" inkscape:connector-curvature="0"/>
<path id="path20" style="fill-rule:evenodd;fill:#c1aa76" d="m12.684 17.259c0.66 0.184 1.121 0.055 1.378-0.386 0.258-0.477-0.019-0.844-0.828-1.102-0.773-0.258-1.195-0.109-1.269 0.442-0.035 0.55 0.203 0.898 0.719 1.046z" inkscape:connector-curvature="0"/>
<path id="path22" style="fill-rule:evenodd;fill:#d9d2b8" d="m15.992 23.877c0.66 0.328 1.117 0.257 1.379-0.223 0.254-0.477 0.164-0.77-0.277-0.883-0.406-0.109-0.809-0.015-1.215 0.277-0.402 0.333-0.367 0.606 0.113 0.829z" inkscape:connector-curvature="0"/>
<path id="path24" style="fill-rule:evenodd;fill:#c9bf9c" d="m17.48 21.119c0.625 0.183 0.918-0.035 0.883-0.66-0.074-0.664-0.441-1.121-1.105-1.379-0.586-0.258-0.77 0-0.551 0.769 0.184 0.735 0.441 1.16 0.773 1.27z" inkscape:connector-curvature="0"/>
<path id="path26" style="fill-rule:evenodd;fill:#c1aa76" d="m10.641 20.404c0.367 0.144 0.589 0 0.664-0.441 0.074-0.442-0.075-0.665-0.442-0.665-0.406 0-0.664 0.129-0.773 0.387-0.11 0.258 0.074 0.496 0.551 0.719z" inkscape:connector-curvature="0"/>
<path id="path28" style="fill-rule:evenodd;fill:#d9d2b8" d="m12.684 23.49c0.328 0.219 0.625 0.332 0.882 0.332 0.258-0.039 0.2-0.258-0.168-0.664-0.367-0.477-0.699-0.625-0.992-0.442-0.332 0.188-0.238 0.442 0.278 0.774z" inkscape:connector-curvature="0"/>
<path id="path30" style="fill-rule:evenodd;fill:#ccc8ab" d="m16.93 7.334-16.93 4.964 16.156 6.614 14.887-5.899-14.113-5.679z" inkscape:connector-curvature="0"/>
<path id="path32" style="fill-rule:evenodd;fill:#f5f5f5" d="m30.492 12.849-15.054 4.024-14.5-5.512-0.938 0.937l15.273 5.676 15.77-4.187c0.258-0.992 0.258-1.547 0-1.656-0.145-0.11-0.273-0.016-0.383 0.277l-0.168 0.441z" inkscape:connector-curvature="0"/>
<path id="path34" style="fill-rule:evenodd;fill:#f9f5ea" d="m17.867 20.127 6.117 1.324 0.828-0.664c0.7-0.512 1.399-0.953 2.094-1.321 1.106-0.554 1.731-0.976 1.875-1.269l-5.293-1.16c-0.625 0.406-1.488 0.902-2.59 1.492l-3.031 1.598z" inkscape:connector-curvature="0"/>
<path id="path36" style="fill-rule:evenodd;fill:#e8e1c5" d="m18.363 20.404 3.528-1.602c0.55-0.293 1.23-0.863 2.039-1.707l-2.371-6.949-0.606 0.551-4.519 2.976 1.929 6.731z" inkscape:connector-curvature="0"/>
<path id="path38" style="fill-rule:evenodd;fill:#a79e81" d="m28.781 18.197-1.871-6.727-5.351-1.324 2.371 6.949 4.851 1.102z" inkscape:connector-curvature="0"/>
<path id="path40" style="fill-rule:evenodd;fill:#c1aa76" d="m20.016 14.17c0.699 0 1.105-0.258 1.214-0.77 0.11-0.477-0.257-0.734-1.101-0.773-0.774-0.036-1.121 0.203-1.051 0.718 0.11 0.551 0.422 0.825 0.938 0.825z" inkscape:connector-curvature="0"/>
<path id="path42" style="fill-rule:evenodd;fill:#d9d2b8" d="m25.035 19.631c0.735 0.148 1.156-0.036 1.266-0.551 0.113-0.551-0.074-0.828-0.551-0.828-0.402 0.039-0.75 0.238-1.047 0.609-0.293 0.402-0.183 0.66 0.332 0.77z" inkscape:connector-curvature="0"/>
<path id="path44" style="fill-rule:evenodd;fill:#c9bf9c" d="m25.695 16.599c0.664 0 0.864-0.297 0.606-0.883-0.219-0.589-0.68-0.937-1.379-1.05-0.625-0.071-0.734 0.222-0.328 0.882 0.402 0.7 0.769 1.051 1.101 1.051z" inkscape:connector-curvature="0"/>
<path id="path46" style="fill-rule:evenodd;fill:#c1aa76" d="m18.914 17.756c0.402 0.074 0.586-0.11 0.551-0.551-0.074-0.477-0.293-0.66-0.664-0.551-0.403 0.074-0.606 0.258-0.606 0.551-0.035 0.258 0.203 0.441 0.719 0.551z" inkscape:connector-curvature="0"/>
<path id="path48" style="fill-rule:evenodd;fill:#d9d2b8" d="m21.727 20.181c0.402 0.149 0.714 0.164 0.937 0.055 0.219-0.074 0.109-0.273-0.332-0.605-0.477-0.333-0.844-0.368-1.102-0.114-0.257 0.262-0.093 0.481 0.497 0.664z" inkscape:connector-curvature="0"/>
<path id="path50" style="fill-rule:evenodd;fill:#f9f5ea" d="m2.535 16.709 4.965 3.804c1.066-0.332 2.223-0.515 3.473-0.55 1.215-0.04 1.965-0.168 2.261-0.387l-4.3-3.309c-0.774 0.11-1.786 0.184-3.036 0.219l-3.363 0.223z" inkscape:connector-curvature="0"/>
<path id="path52" style="fill-rule:evenodd;fill:#e8e1c5" d="m2.867 17.095 3.86 0.11c0.625-0.035 1.468-0.278 2.535-0.715l0.828-7.281-0.77 0.222-5.351 0.77-1.102 6.894z" inkscape:connector-curvature="0"/>
<path id="path54" style="fill-rule:evenodd;fill:#a79e81" d="m9.262 16.486 3.972 3.09 1.157-6.895c-0.442-0.441-0.938-0.883-1.489-1.32-0.515-0.48-1.449-1.195-2.812-2.152l-0.828 7.277z" inkscape:connector-curvature="0"/>
<path id="path56" style="fill-rule:evenodd;fill:#c1aa76" d="m7.004 12.24c0.625 0.297 1.101 0.242 1.434-0.164 0.292-0.406 0.074-0.809-0.665-1.211-0.695-0.371-1.117-0.297-1.265 0.219-0.149 0.515 0.015 0.902 0.496 1.156z" inkscape:connector-curvature="0"/>
<path id="path58" style="fill-rule:evenodd;fill:#d9d2b8" d="m9.207 19.298c0.59 0.442 1.051 0.442 1.379 0 0.332-0.437 0.297-0.753-0.109-0.937-0.403-0.145-0.829-0.109-1.27 0.113-0.441 0.219-0.441 0.496 0 0.824z" inkscape:connector-curvature="0"/>
<path id="path60" style="fill-rule:evenodd;fill:#c9bf9c" d="m11.082 16.818c0.59 0.258 0.922 0.074 0.992-0.551 0.039-0.59-0.238-1.086-0.824-1.488-0.551-0.367-0.793-0.148-0.719 0.66 0.074 0.774 0.258 1.231 0.551 1.379z" inkscape:connector-curvature="0"/>
<path id="path62" style="fill-rule:evenodd;fill:#c1aa76" d="m4.523 14.998c0.329 0.222 0.551 0.109 0.661-0.328 0.148-0.442 0.035-0.7-0.332-0.774-0.403-0.074-0.661 0.02-0.77 0.277-0.148 0.254 0 0.532 0.441 0.825z" inkscape:connector-curvature="0"/>
<path id="path64" style="fill-rule:evenodd;fill:#d9d2b8" d="m6.012 18.361c0.254 0.297 0.511 0.441 0.769 0.441 0.258 0.039 0.258-0.183 0-0.66-0.293-0.515-0.605-0.719-0.937-0.605-0.367 0.144-0.313 0.422 0.168 0.824z" inkscape:connector-curvature="0"/>
<path id="path66" style="fill-rule:evenodd;fill:#f9f5ea" d="m10.863 15.22 5.512 2.977 0.937-0.441c0.813-0.293 1.602-0.516 2.376-0.661 1.21-0.257 1.945-0.515 2.203-0.773l-4.797-2.535c-0.735 0.219-1.707 0.457-2.922 0.715l-3.309 0.718z" inkscape:connector-curvature="0"/>
<path id="path68" style="fill-rule:evenodd;fill:#e8e1c5" d="m11.195 15.603 3.86-0.551c0.625-0.109 1.433-0.476 2.425-1.101l-0.386-7.336-0.715 0.332-5.184 1.656v7z" inkscape:connector-curvature="0"/>
<path id="path70" style="fill-rule:evenodd;fill:#c1aa76" d="m15.824 14.67 1.27-0.883c0.441-0.406 0.293-0.699-0.442-0.883-0.515-0.148-0.882 0.09-1.101 0.715-0.223 0.625-0.129 0.976 0.273 1.051z" inkscape:connector-curvature="0"/>
<path id="path72" style="fill-rule:evenodd;fill:#a79e81" d="m20.238 8.271-3.144-1.656 0.386 7.336 4.411 2.371v-6.949l-1.653-1.102z" inkscape:connector-curvature="0"/>
<path id="path74" style="fill-rule:evenodd;fill:#c9bf9c" d="m20.512 14.006c0.441 0.183 0.664 0.054 0.664-0.387 0-0.403-0.184-0.645-0.551-0.715-0.371-0.074-0.59 0.035-0.664 0.332-0.109 0.293 0.074 0.551 0.551 0.77z" inkscape:connector-curvature="0"/>
<path id="path76" style="fill-rule:evenodd;fill:#c1aa76" d="m14.5 10.091c0.699 0.184 1.16 0.055 1.379-0.386 0.258-0.477-0.016-0.844-0.824-1.102-0.774-0.258-1.18-0.113-1.215 0.442-0.074 0.55 0.148 0.898 0.66 1.046z" inkscape:connector-curvature="0"/>
<path id="path78" style="fill-rule:evenodd;fill:#d9d2b8" d="m17.809 16.709c0.664 0.332 1.121 0.257 1.382-0.223 0.293-0.477 0.2-0.77-0.277-0.883-0.406-0.109-0.809-0.015-1.215 0.278-0.402 0.332-0.367 0.605 0.11 0.828z" inkscape:connector-curvature="0"/>
<path id="path80" style="fill-rule:evenodd;fill:#c9bf9c" d="m19.301 11.634c0.625 0.149 0.918-0.089 0.879-0.714-0.071-0.625-0.422-1.067-1.047-1.325-0.586-0.257-0.789 0-0.606 0.774 0.184 0.734 0.442 1.156 0.774 1.265z" inkscape:connector-curvature="0"/>
<path id="path82" style="fill-rule:evenodd;fill:#c1aa76" d="m12.516 13.236c0.367 0.145 0.57 0 0.609-0.441 0.07-0.446-0.074-0.664-0.441-0.664-0.407 0-0.645 0.128-0.719 0.386-0.11 0.258 0.074 0.496 0.551 0.719z" inkscape:connector-curvature="0"/>
<path id="path84" style="fill-rule:evenodd;fill:#d9d2b8" d="m14.5 16.322c0.332 0.219 0.625 0.332 0.883 0.332 0.258-0.039 0.222-0.258-0.11-0.664-0.367-0.477-0.714-0.625-1.046-0.442-0.332 0.184-0.239 0.442 0.273 0.774z" inkscape:connector-curvature="0"/>
<path id="path86" style="fill-rule:evenodd;fill:#dbdbdb" d="m27.184 4.193-9.153-4.191-14.172 3.308-3.859 8.988 18.195-4.246 13.235 5.735-4.246-9.594z" inkscape:connector-curvature="0"/>
<path id="path88" style="fill-rule:evenodd;fill:#b2b2b2" d="m18.746 6.783 12.297 5.348-3.859-7.938-9.153-4.191 0.715 6.781z" inkscape:connector-curvature="0"/>
<path id="path90" style="fill-rule:evenodd;fill:#f5f5f5" d="m31.43 13.787-0.551-1.875-12.133-5.129-18.582 4.023-0.164 1.492 19.133-4.027 12.297 5.516z" inkscape:connector-curvature="0"/>
<path id="path92" style="fill-rule:evenodd;fill:#e4e4e4" d="m5.293 3.42-0.77 6.781 10.532-2.59-4.028-5.074-5.734 0.883z" inkscape:connector-curvature="0"/>
<path id="path94" style="fill-rule:evenodd;fill:#949494" d="m30.16 11.193-2.754-6.617-9.375-4.574c2.793 2.574 4.926 4.464 6.395 5.679 1.472 1.211 3.383 3.051 5.734 5.512z" inkscape:connector-curvature="0"/>
<path id="path96" style="fill-rule:evenodd;fill:#bdbf02" d="m13.949 17.756c-1.687 2.132-2.719 3.492-3.086 4.082l-2.758 5.898c0.957-1.469 2.059-2.758 3.309-3.859 1.215-1.102 2.059-3.145 2.535-6.121z" inkscape:connector-curvature="0"/>
<path id="path98" style="fill-rule:evenodd;fill:#586436" d="m13.621 26.634c0.219-2.574 0.328-4.171 0.328-4.796v-4.082c-0.734 1.984-1.101 3.234-1.101 3.75l0.773 5.128z" inkscape:connector-curvature="0"/>
<path id="path100" style="fill-rule:evenodd;fill:#90a701" d="m8.438 27.185 2.976-3.308c0.734-0.844 1.285-1.766 1.652-2.758v-0.387l-2.976 2.594c-0.219 0.219-0.57 0.937-1.047 2.148-0.219 0.59-0.422 1.16-0.605 1.711z" inkscape:connector-curvature="0"/>
</g>
</g>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@@ -1,27 +1,37 @@
{% extends "base.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
<title>About Snikket</title>
<title>{% trans %}About Snikket{% endtrans %}</title>
{% endblock %}
{% block body %}
<main>
<div class="box el-2">
<h1>{% trans %}About Snikket{% endtrans %}</h1>
<p>{% trans snikket_url="https://snikket.org" %}To learn more about Snikket, visit the <a href="{{ snikket_url}}">Snikket website</a>.{% endtrans %}</p>
<h2>{% trans %}About this Service{% endtrans %}</h2>
<p>{% trans site_name=config["SITE_NAME"] %}This is the Snikket service <em>{{ site_name }}</em>.{% endtrans %}</p>
<p>{% trans site_name=config["SITE_NAME"] %}This is the Snikket service <em>{{ site_name }}</em>, running open-source software from the Snikket project.{% endtrans %}</p>
<p>{% trans snikket_url="https://snikket.org" %}To learn more about Snikket, visit the <a href="{{ snikket_url}}">Snikket website</a>.{% endtrans %}</p>
<p><a href="/policies/">{% trans %}View service policies{% endtrans %}</a>
<h3>{% trans %}Licenses{% endtrans %}</h3>
<p>{% trans agpl_url="https://www.gnu.org/licenses/agpl.html" %}The web portal software is licensed under the terms of the <a href="{{ agpl_url }}">Affero GNU General Public License, version 3.0 or later</a>. The full terms of the license can be reviewed using the aforementioned link.{% endtrans %}</p>
<p>{% trans source_url="https://github.com/snikket-im/snikket-web-portal/" %}The source code of the web portal can be downloaded and viewed in <a href="{{ source_url }}">its GitHub repository</a>.{% endtrans %}</p>
<p>{% trans source_url="https://material.io/resources/icons/", apache20_url="https://www.apache.org/licenses/LICENSE-2.0.txt" %}The icons used in the web portal are <a href="{{ source_url }}">Googles Material Icons</a>, made available by Google under the terms of the <a href="{{ apache20_url }}">Apache 2.0 License</a>.{% endtrans %}</p>
<h3>{% trans %}Trademarks{% endtrans %}</h3>
<p>{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
<h3>{% trans %}Software Versions{% endtrans %}</h3>
<pre>Snikket Server
Domain: {{ config["SNIKKET_DOMAIN"] }}
Snikket Web Portal ({{ version }})
<pre>Domain: {{ config["SNIKKET_DOMAIN"] }}
Web Portal{% if version %} ({{ version }}){% endif %}
{%- if core_versions -%}
{% for name, version in core_versions.items() %}
{{ name }} ({{ version }}){% endfor %}
{%- endif -%}
{%- if extra_versions -%}
{% for name, version in extra_versions.items() %}
{{ name }} ({{ version }}){% endfor %}
{%- endif -%}</pre>
<p>
{%- call standard_button("back", url_for("index"), class="primary") -%}
{% trans %}Back to the main page{% endtrans %}

View File

@@ -3,7 +3,7 @@
{% block content %}
<h1>{% trans %}Manage circles{% endtrans %}</h1>
<p>{% trans %}<em>Circles</em> aim to help people who are in the same social circle find each other on your service.{% endtrans %}</p>
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle has a group chat where the circle members are included.{% endtrans %}</p>
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle may have group chats where the circle members are included.{% endtrans %}</p>
{%- if circles -%}
<form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}}

View File

@@ -0,0 +1,5 @@
{% extends "admin_app.html" %}
{% block content %}
<h1>{{ target_circle.name }}</h1>
{%- include "admin_create_circle_group_chat_form.html" -%}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% from "library.j2" import form_button, render_errors %}
<form method="POST" action="{{ url_for(".edit_circle_add_chat", id_=target_circle.id_) }}">
{{- group_chat_form.csrf_token -}}
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Create new circle group chat{% endtrans %}</h2>
<p class="form-descr weak">{% trans %}Add a chat to your circle so its members can hold group discussions.{% endtrans %}</p>
<p class="form-descr weak"><strong>{% trans %}Tip:{% endtrans %}</strong> {% trans %}This is only for creating group chats that automatically include <em>all</em> members of the circle. If you want a normal group chat, create it in the Snikket app instead.{% endtrans %}</p>
<div class="f-ebox">
{{ group_chat_form.name.label }}
{{ group_chat_form.name }}
</div>
<div class="f-bbox">
{%- call form_button("add", group_chat_form.action_save, class="primary") %}{% endcall -%}
</div>
</div></form>

View File

@@ -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>

View File

@@ -0,0 +1,21 @@
{% extends "admin_app.html" %}
{% from "library.j2" import box, form_button, standard_button %}
{% block content %}
<h1>{% trans circle_name=target_circle.name %}Delete circle {{ circle_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Delete circle{% endtrans %}</h2>
{{ form.csrf_token }}
<p class="form-descr">{% trans %}Are you sure you want to delete the following circle?{% endtrans %}</p>
<dl>
<dt>{% trans %}Name{% endtrans %}</dt>
<dd>{{ target_circle.name }}</dd>
</dl>
{% call box("alert", _("Danger")) %}
<p>{% trans %}The circle and the corresponding chat will be deleted, 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(".edit_circle", id_=target_circle.id_), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
</div>
</form></div>
{% endblock %}

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button %}
{% 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" %}
@@ -13,13 +13,6 @@
<div class="box hint form layout-expanded">
<header>{% trans %}This is your main circle{% endtrans %}</header>
<p>{% trans %}This circle is managed automatically and cannot be removed or renamed.{% endtrans %}</p>
{%- if target_circle.muc_jid -%}
<div><label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label></div>
<div><input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}"></div>
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- endif -%}
</div>
{%- else -%}
<div class="form layout-expanded">
@@ -28,43 +21,69 @@
{{ form.name.label }}
{{ form.name }}
</div>
<div class="f-ebox">
{%- if target_circle.muc_jid -%}
<label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label>
<input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}">
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- else -%}
<p>{% trans %}This circle has no group chat associated.{% endtrans %}<p>
{%- 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>
<h3 class="form-title">{% trans %}Delete circle{% endtrans %}</h3>
<p class="form-desc">{% trans %}Deleting a circle does not delete any users in the circle.{% endtrans %}</p>
<div class="f-bbox">
{%- call form_button("delete", form.action_delete, class="secondary danger") %}{% endcall -%}
{%- call standard_button("delete", url_for(".delete_circle", id_=target_circle.id_), class="secondary danger") %}{% trans %}Delete circle{% endtrans %}{% endcall -%}
</div>
</div>
{%- endif -%}
<h2 id="chats">{% trans %}Group chats{% endtrans %}</h2>
<p>{% trans %}These group chats will be available to all members of the circle.{% endtrans %}</p>
{%- if circle_chats -%}
<div class="el-2 elevated"><table>
<thead>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</thead>
<tbody>
{%- for chat in circle_chats -%}
<tr>
<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 %}
{%- endcall -%}
</td>
</tr>
{%- endfor -%}
</tbody>
</table></div>
{%- else -%}
<p>{% trans %}This circle currently has no group chats.{% endtrans %}</p>
{%- endif -%}
{%- call standard_button("add", url_for(".edit_circle_add_chat", id_=target_circle.id_), class="secondary") -%}
{% trans %}Add group chat{% endtrans %}
{%- endcall -%}
<h2 id="members">{% trans %}Circle members{% endtrans %}</h2>
<p>{% trans %}All members of the circle will see each other in their contact list.{% endtrans %}</p>
{%- 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>{% trans %}Actions{% endtrans %}</th>
</thead>
<tbody>
{%- for member in circle_members -%}
{%- for localpart, member in circle_members -%}
<tr>
<td>{{ member.localpart }}</td>
<td class="collapsible">{% call value_or_hint(member.display_name) %}{% endcall %}</td>
<td>
{%- if member -%}
{%- 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="nowrap">
{%- call custom_form_button("remove_user", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
{% trans username=member.localpart %}Remove user {{ username }} from circle{% endtrans %}

View File

@@ -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 -#}
@@ -44,10 +45,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>

View File

@@ -0,0 +1,83 @@
{% extends "admin_app.html" %}
{% 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 }}
</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="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="secondary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>
</div></form>
{% endblock %}

View File

@@ -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>

View File

@@ -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 %}Valid until{% 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 -%}

View File

@@ -9,19 +9,19 @@
<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>
<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") -%}
{% 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>

View File

@@ -0,0 +1,112 @@
{% 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 %}Storage used by shared files{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_uploads | default(None) is not none -%}
{{ metrics.prosody_uploads | format_bytes }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Active users{% endtrans %}</dt>
<dd>
<ul>
{%- if metrics.prosody_devices | default(None) is not none -%}
<li>{% trans %}Connected now:{% endtrans %} {{ metrics.prosody_devices }}</li>
{%- else -%}
<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>
{% 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 %}

View File

@@ -1,37 +1,36 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, 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>
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
{{- reset_form.csrf_token -}}
<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>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.localpart }}</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td>
{%- call render_user(user) -%}{%- 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("delete", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Delete user {{ 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 -%}
{%- 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 %}
{%- endcall -%}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table></div>
</form>
{%- include "admin_create_invite_form.html" -%}
{% endblock %}

View File

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

View File

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

View File

@@ -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>

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block head_lead %}
<title>{{ config["SITE_NAME"] }}</title>
{% endblock %}
{% block style %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/home.css') }}">
{% endblock %}
{% block body %}
<main class="home">
<header>
{% trans site_name=config["SITE_NAME"], logo_url=url_for("static", filename="img/tofu.svg") %}
<img src="{{ logo_url }}" alt="{{ site_name }}" />
{% endtrans %}
<h1>{{ config["SITE_NAME"] }}</h1>
</header>
<section class="icons">
<a href="https://compliance.conversations.im/server/{{ config['SNIKKET_DOMAIN'] }}"
><img src="https://compliance.conversations.im/badge/{{ config['SNIKKET_DOMAIN'] }}"></a>
</section>
<section class="intro">
<p>{% trans site_name=config["SITE_NAME"], about_page=url_for('main.about') %}
<em>{{ site_name }}</em> is an XMPP (Jabber) server hosted by
<a href="https://www.davejansen.com/" rel="external" target="_blank">Dave Jansen</a>
using open-source software from <a href="{{ about_page }}">the Snikket project</a>.
{% endtrans %}</p>
{%- if config["ADMIN_CONTACT"] -%}
<p>
{% trans admin_contact=config["ADMIN_CONTACT"] %}
To reach out, send a message to <a href="xmpp:{{ admin_contact }}">{{ admin_contact }}</a> or
<a href="mailto:{{ admin_contact }}">via email</a>.
{% endtrans %}
</p>
{%- endif -%}
</section>
<footer>
<nav>
<a href="https://xmpp.org/getting-started/" rel="external" target="_blank">
{% trans %}Need an XMPP (Jabber) client?{% endtrans %}
</a>
<a href="{{ url_for('main.login') }}">
{% trans site_name=config["SITE_NAME"] %}Need to manage your {{ site_name }} account?{% endtrans %}
</a>
</p>
</footer>
</main>
{% endblock %}

View File

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

View File

@@ -28,12 +28,12 @@
</div>
<div class="f-ebox">
{{ form.password.label }}
{{ form.password }}
{{ form.password(autocomplete="new-password") }}
<p class="field-desc weak">{% trans %}Enter a secure password that you do not use anywhere else.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.password_confirm.label }}
{{ form.password_confirm }}
{{ form.password_confirm(autocomplete="new-password") }}
</div>
<div class="f-bbox">
{%- call form_button("done", form.action_register, class="primary") -%}{%- endcall -%}

View File

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

View File

@@ -1,6 +1,6 @@
{% extends "invite.html" %}
{% set body_id = "invite" %}
{% from "library.j2" import form_button, clipboard_button %}
{% from "library.j2" import form_button, clipboard_button, render_errors %}
{% block head_lead %}
<title>{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }} | Snikket{% endtrans %}</title>
{%- include "copy-snippet.html" -%}
@@ -15,6 +15,47 @@
{% 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>
{% if migration_success %}
<h2>{% trans %}Import successful{% endtrans %}</h2>
<p>{% trans %}Congratulations! Your account data has been successfully imported.{% endtrans %}</p>
{% endif %}
{% if form %}
<h2>{% trans %}Moving to Snikket?{% endtrans %}</h2>
<p>{% trans %}If you are moving from a different Snikket instance or another XMPP-compatible service, you may optionally import the data (contacts, profile information, etc.) from your previous account. When you have exported the data from your previous account, upload it using the form below.{% endtrans %}</p>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
<h3 class="form-title">{% trans %}Upload account data{% endtrans %}</h3>
{{ form.csrf_token }}
{% call render_errors(form) %}{% endcall %}
<div class="f-ebox">
{{ form.account_data_file.label }}
{{ form.account_data_file(accept="application/xml",
data_maxsize=max_import_size,
data_warning_header=import_too_big_warning_header,
data_maxsize_warning=import_too_big_warning) }}
</div>
<div class="f-bbox">
{%- call form_button("upload", form.action_import, class="secondary") %}{% endcall -%}
</div>
<script type="text/javascript">
document.getElementById("{{ form.account_data_file.id }}").onchange = function() {
var maxsize_s = this.dataset.maxsize;
var maxsize = parseInt(maxsize_s);
if (this.files[0].size > maxsize) {
var warning_header = this.dataset.warningHeader;
var warning_text = this.dataset.maxsizeWarning;
this.setCustomValidity(warning_text);
this.reportValidity();
this.value = null;
} else {
this.setCustomValidity("");
}
};
</script>
</form></div>
{% endif %}
</div>
{% endblock %}

View File

@@ -6,6 +6,7 @@
<title>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }} | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/invite-magic.js") }}"></script>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
<link rel="alternate" href="{{ invite.xmpp_uri }}">
{% endblock %}
{% block content %}
<div class="elevated box el-3">
@@ -16,6 +17,13 @@
{%- else -%}
<p>{% trans site_name=config["SITE_NAME"] %}You have been invited to chat on {{ site_name }} using Snikket, a secure, privacy-friendly chat app.{% endtrans %}</p>
{%- endif -%}
{%- if config["TOS_URI"] and config["PRIVACY_URI"] -%}
<p>
{% trans site_name=config["SITE_NAME"], tos_uri=config["TOS_URI"], privacy_uri=config["PRIVACY_URI"] %}By continuing, you agree to the <a href="{{tos_uri}}">Terms of Service</a> and <a href="{{privacy_uri}}">Privacy Policy</a>.{% endtrans %}
</p>
{%- endif -%}
<h2>{% trans %}Get started{% endtrans %}</h2>
{%- if apple_store_url -%}
<p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p>
@@ -24,13 +32,14 @@
{%- endif -%}
<div class="install-buttons">
<ul>
<li><a href="{{ play_store_url }}"><img alt='{% trans %}Get it on Google Play{% endtrans %}' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' class="play"/></a></li>
<li><a href="{{ play_store_url }}"><img alt='{% trans %}Get it on Google Play{% endtrans %}' src='{{ play_store_badge() }}' class="play"/></a></li>
{%- if apple_store_url -%}
<li><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
<li><a href="{{ apple_store_url }}" class="popover" data-popover-id="apple-popover"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
{%- endif -%}
<li><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Get it on F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></li>
</ul>
{%- call standard_button("qrcode", "#qr-modal", class="primary", onclick="open_modal(this); return false;") -%}
{% trans %}Not on mobile?{% endtrans %}
{% trans %}Send to mobile device{% endtrans %}
{%- endcall -%}
</div>
<p>{% trans %}After installation the app should automatically open and prompt you to create an account. If not, simply click the button below.{% endtrans %}</p>
@@ -54,39 +63,83 @@
{%- endcall -%}
</header>
<p>{% trans %}You can transfer this invite to your mobile device by scanning a code with your camera. You can use either a QR scanner app or the Snikket app itself.{% endtrans %}</p>
<div class="tabbox">
{#- -#}
<nav class="tabs" role="tablist">
{#- -#}
<a href="#qr-info-url" class="active" role="tab" aria-controls="qr-info-url" aria-selected="true" onclick="select_tab(this); return false;">{% trans %}Using a QR code scanner{% endtrans %}</a>
{#- -#}
<a href="#qr-info-uri" role="tab" aria-controls="qr-info-uri" aria-selected="false" onclick="select_tab(this); return false;">{% trans %}Using the Snikket app{% endtrans %}</a>
{#- -#}
</nav>
{#- -#}
<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>
{#- -#}
<div id="qr-info-uri" class="tab-pane">
<img class="float-right" id="tutorial-scan" aria-hidden="true" alt="" src="{{ url_for("static", filename="img/tutorial-scan.png") }}">
<p>{% trans %}Install the Snikket app on your mobile device, open it, and tap the 'Scan' button at the top.{% endtrans %}</p>
<p>{% trans %}Your camera will turn on. Point it at the square code below until it is within the highlighted square on your screen, and wait until the app recognises it.{% endtrans %}</p>
<div id="qr-uri" data-qrdata="{{ invite.xmpp_uri }}" class="qr"></div>
</div>
{#- -#}
</div>
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True, _scheme="https") }}" class="qr"></div>
{#- -#}
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="primary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</div>
</div>
{%- if apple_store_url -%}
<div id="apple-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
<header class="modal-title">
{#- -#}
<span>{% trans %}Install on iOS{% endtrans %}</span>
{#- -#}
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
{% 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>
<ol>
<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>
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</p></li>
</ol>
{#- -#}
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</div>
</div>
{%- endif -%}
<div id="fdroid-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
<header class="modal-title">
{#- -#}
<span>{% trans %}Install via F-Droid{% endtrans %}</span>
{#- -#}
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</header>
<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 }}"><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") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</p></li>
</ol>
{#- -#}
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</div>
</div>
<script type="text/javascript">
var catch_popover = function() {
open_modal(this);
return false;
}
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];
a.onclick = catch_popover;
a.href = "#" + a.dataset.popoverId;
}
};
</script>
{% endblock %}

View File

@@ -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,13 +100,31 @@
</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 -%}
<div class="box warning">{#- -#}
<header>{% trans %}Invalid input{% endtrans %}</header>
{%- if error_list | length == 1 -%}
<p>{{ error_list[0] }}.</p>
<p>{{ error_list[0] }}</p>
{%- else -%}
<ul>
{%- for error in 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 %}

View File

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

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
<title>{% trans %}Policies{% endtrans %} - {{ config["SITE_NAME"] }}</title>
{% endblock %}
{% block body %}
<main>
<div class="box el-2">
<h1>{{ config["SITE_NAME"] }}</h1>
<h2>{% trans %}Policies{% endtrans %}</h2>
{% if config["TOS_URI"] or config["PRIVACY_URI"] -%}
<p>{% trans %}Use of this service is subject to the following policies:{% endtrans %}</p>
<ul>
{%- if config["TOS_URI"] %}
<li><a href="{{ config["TOS_URI"] }}">{% trans %}Terms of Service{% endtrans %}</a></li>
{%- endif %}
{%- if config["PRIVACY_URI"] %}
<li><a href="{{ config["PRIVACY_URI"] }}">{% trans %}Privacy Policy{% endtrans %}</a></li>
{%- endif %}
</ul>
{%- else -%}
<p>{% trans %}Please contact the administrator of this instance if you have questions about policies.{% endtrans %}</p>
{% endif -%}
<p>{% trans url="https://snikket.org/app/privacy/" %}Use of the Snikket apps is subject to the <a href="{{url}}">Snikket Apps Privacy Policy</a>.{% endtrans %}</p>
{%- if config["ABUSE_EMAIL"] %}
<p>{% trans email=config["ABUSE_EMAIL"], domain=config["SNIKKET_DOMAIN"] %}To report policy violations or other abuse from this service, please send an email to {{email}}. Specify the domain name of this instance ({{domain}}) and include details of the incident(s).{% endtrans %}</p>
{%- endif %}
<p>
{%- call standard_button("back", url_for("index"), class="primary") -%}
{% trans %}Back to the main page{% endtrans %}
{%- endcall -%}
</p>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,16 @@
# {{ config["SNIKKET_DOMAIN"] }} is running open-source software
# from the Snikket project: https://snikket.org/
{% if config["SECURITY_EMAIL"] -%}
# Security issues related to this service should be addressed to the
# following security contact:
Contact: mailto:{{ config["SECURITY_EMAIL"] }}
{% else -%}
# This service does not have a public security contact. You might find
# more information about the service at the following link:
Contact: https://{{ config["SNIKKET_DOMAIN"] }}/policies/
{%- endif %}
# Please report software defects to the project developers, per the
# instructions at the following link:
Contact: https://snikket.org/security/

View File

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

View File

@@ -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">
@@ -30,6 +35,7 @@
<div>
<div>{% call standard_button("edit", url_for(".profile"), class="primary") %}{% trans %}Edit profile{% endtrans %}{% endcall %}</div>
<div>{% call standard_button("passwd", url_for(".change_pw"), class="secondary") %}{% trans %}Change password{% endtrans %}{% endcall %}</div>
<div>{% call standard_button("folder", url_for(".manage_data"), class="secondary") %}{% trans %}Manage your data{% endtrans %}{% endcall %}</div>
</div>
{#- -#}
</li>

View File

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

View File

@@ -0,0 +1,24 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
{% block content %}
<h1>{% trans %}Manage your data{% endtrans %}</h1>
<nav class="welcome">
<ul>
<li>
<h2>{% trans %}Export account{% endtrans %}</h2>
<p>{% trans %}Download your account data as a file for backup purposes or to move your account to another service.{% endtrans %}</p>
{% call render_errors(form) %}{% endcall %}
<div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
<form method="POST">
{{ form.csrf_token }}
{%- call form_button("download", form.action_export, class="primary") %}{% endcall -%}
</form>
</div>
</li>
</ul>
</nav>
{% endblock %}

View File

@@ -1,8 +1,5 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, custom_form_button, render_errors %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<div class="form layout-expanded"><form method="POST">
<h1 class="form-title">{% trans %}Change your password{% endtrans %}</h1>
@@ -12,22 +9,22 @@
{%- endcall -%}
<div class="f-ebox">
{{ form.current_password.label(class="required") }}
{{ form.current_password(class=("has-error" if form.current_password.name in form.errors else "")) }}
{{ form.current_password(class=("has-error" if form.current_password.name in form.errors else ""), autocomplete="current-password") }}
</div>
<div class="f-ebox">
{{ form.new_password.label(class="required") }}
{{ form.new_password }}
{{ form.new_password(autocomplete="new-password") }}
</div>
<div class="f-ebox">
{{ form.new_password_confirm.label(class="required") }}
{{ form.new_password_confirm(class=("has-error" if form.new_password_confirm.name in form.errors else "")) }}
{{ form.new_password_confirm(class=("has-error" if form.new_password_confirm.name in form.errors else ""), autocomplete="new-password") }}
</div>
<div class="box warning">
<header>{% trans %}Warning{% endtrans %}</header>
<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 -%}

View File

@@ -1,13 +1,11 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button, avatar with context %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
{% block content %}
<h1>{% trans %}Update your profile{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
<h2 class="form-title">{% trans %}Profile{% endtrans %}</h2>
{{ form.csrf_token }}
{% call render_errors(form) %}{% endcall %}
<div class="f-ebox">
{{ form.nickname.label }}
{{ form.nickname(placeholder=user_info.username) }}
@@ -16,7 +14,10 @@
{{ form.avatar.label }}
<div class="avatar-wrap">
{%- call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall -%}
{{ form.avatar }}
{{ form.avatar(accept="image/png",
data_maxsize=max_avatar_size,
data_warning_header=avatar_too_big_warning_header,
data_maxsize_warning=avatar_too_big_warning) }}
</div>
</div>
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3>
@@ -28,8 +29,27 @@
</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">
document.getElementById("{{ form.avatar.id }}").onchange = function() {
var maxsize_s = this.dataset.maxsize;
var maxsize = parseInt(maxsize_s);
var existing_alert = document.getElementById("avatar-alert");
if (existing_alert) {
existing_alert.parentNode.removeChild(existing_alert);
}
if (this.files[0].size > maxsize) {
var warning_header = this.dataset.warningHeader;
var warning_text = this.dataset.maxsizeWarning;
this.setCustomValidity(warning_text);
this.reportValidity();
this.value = null;
} else {
this.setCustomValidity("");
}
};
</script>
</form></div>
{% endblock %}

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

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

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

Some files were not shown because too many files have changed in this diff Show More