You've already forked snikket-web-portal
Compare commits
8 Commits
beta.20210
...
alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1da45395c2 | ||
|
|
aa04320d70 | ||
|
|
7dde3a1128 | ||
|
|
934976c114 | ||
|
|
13b2a76c3d | ||
|
|
28e01c336d | ||
|
|
5fb0b91178 | ||
|
|
b007afc901 |
@@ -1,5 +1,11 @@
|
||||
# Snikket Web Portal
|
||||
|
||||
This is the web component of a [Snikket service](https://snikket.org/service/)
|
||||
that allows users to manage accounts, and administrators to manage the
|
||||
service. For general setup, see the [Snikket install
|
||||
guide](https://snikket.org/service/quickstart/). For developers working on
|
||||
Snikket, see the development quickstart below.
|
||||
|
||||

|
||||
|
||||
## Development quickstart
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiohttp~=3.6
|
||||
quart~=0.15
|
||||
quart~=0.11,<0.15
|
||||
flask-wtf~=0.14
|
||||
hsluv~=0.0.2
|
||||
flask-babel~=1.0
|
||||
|
||||
@@ -17,7 +17,6 @@ from quart import (
|
||||
redirect,
|
||||
jsonify,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
|
||||
import environ
|
||||
|
||||
@@ -41,7 +40,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
|
||||
|
||||
try:
|
||||
user_info = await infra.client.get_user_info()
|
||||
except (aiohttp.ClientError, werkzeug.exceptions.HTTPException):
|
||||
except (aiohttp.ClientError, quart.exceptions.HTTPException):
|
||||
user_info = {}
|
||||
|
||||
return {
|
||||
@@ -106,16 +105,16 @@ async def backend_error_handler(exc: Exception) -> quart.Response:
|
||||
|
||||
|
||||
async def generic_http_error(
|
||||
exc: werkzeug.exceptions.HTTPException,
|
||||
exc: quart.exceptions.HTTPException,
|
||||
) -> quart.Response:
|
||||
return quart.Response(
|
||||
await render_template(
|
||||
"generic_http_error.html",
|
||||
status=exc.code,
|
||||
status=exc.status_code,
|
||||
description=exc.description,
|
||||
name=exc.name,
|
||||
),
|
||||
status=exc.code,
|
||||
status=exc.status_code,
|
||||
)
|
||||
|
||||
|
||||
@@ -196,25 +195,25 @@ def create_app() -> quart.Quart:
|
||||
app.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
aiohttp.ClientConnectorError,
|
||||
backend_error_handler,
|
||||
backend_error_handler, # type:ignore
|
||||
)
|
||||
app.register_error_handler(
|
||||
werkzeug.exceptions.HTTPException,
|
||||
generic_http_error,
|
||||
quart.exceptions.HTTPException,
|
||||
generic_http_error, # type:ignore
|
||||
)
|
||||
app.register_error_handler(
|
||||
Exception,
|
||||
generic_error_handler,
|
||||
generic_error_handler, # type:ignore
|
||||
)
|
||||
|
||||
@app.route("/") # type: ignore
|
||||
@app.route("/")
|
||||
async def index() -> quart.Response:
|
||||
if infra.client.has_session:
|
||||
return redirect(url_for('user.index'))
|
||||
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
@app.route("/site.webmanifest") # type: ignore
|
||||
@app.route("/site.webmanifest")
|
||||
def site_manifest() -> quart.Response:
|
||||
# this is needed for icons
|
||||
return jsonify(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version_info = (0, 1, 2, "a0")
|
||||
version_info = (0, 2, 2, None)
|
||||
version = (
|
||||
".".join(map(str, version_info[:3])) +
|
||||
(f"-{version_info[3]}" if version_info[3] else "")
|
||||
|
||||
@@ -28,7 +28,7 @@ from .infra import client, circle_name, BaseForm
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
|
||||
@bp.route("/") # type:ignore
|
||||
@bp.route("/")
|
||||
@client.require_admin_session()
|
||||
async def index() -> str:
|
||||
return await render_template("admin_home.html")
|
||||
@@ -38,7 +38,7 @@ class PasswordResetLinkPost(BaseForm):
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
@bp.route("/users") # type:ignore
|
||||
@bp.route("/users")
|
||||
@client.require_admin_session()
|
||||
async def users() -> str:
|
||||
users = sorted(
|
||||
@@ -88,7 +88,7 @@ class EditUserForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
@@ -143,7 +143,7 @@ class DeleteUserForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"]) # 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]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
@@ -164,7 +164,7 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/debug") # type:ignore
|
||||
@bp.route("/user/<localpart>/debug")
|
||||
@client.require_admin_session()
|
||||
async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
@@ -180,7 +180,7 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/users/password-reset/<id_>", methods=["GET", "POST"]) # type:ignore # noqa:E501
|
||||
@bp.route("/users/password-reset/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def user_password_reset_link(
|
||||
id_: str,
|
||||
@@ -274,7 +274,7 @@ class InvitePost(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/invitations", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/invitations", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def invitations() -> typing.Union[str, quart.Response]:
|
||||
invites = sorted(
|
||||
@@ -320,7 +320,7 @@ class InviteForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/invitation/-/new", methods=["POST"]) # type:ignore
|
||||
@bp.route("/invitation/-/new", methods=["POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
form = InvitePost()
|
||||
@@ -348,7 +348,7 @@ async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
invite_form=form)
|
||||
|
||||
|
||||
@bp.route("/invitation/<id_>", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
@@ -397,7 +397,7 @@ class CirclePost(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circles") # type:ignore
|
||||
@bp.route("/circles")
|
||||
@client.require_admin_session()
|
||||
async def circles() -> str:
|
||||
circles = sorted(
|
||||
@@ -414,7 +414,7 @@ async def circles() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circle/-/new", methods=["POST"]) # type:ignore
|
||||
@bp.route("/circle/-/new", methods=["POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
create_form = CirclePost()
|
||||
@@ -460,7 +460,7 @@ class EditCircleForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circle/<id_>", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/circle/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
async with client.authenticated_session() as session:
|
||||
|
||||
@@ -46,12 +46,12 @@ def context() -> typing.Mapping[str, typing.Any]:
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/<id_>") # type:ignore
|
||||
@bp.route("/<id_>")
|
||||
async def view_old(id_: str) -> quart.Response:
|
||||
return redirect(url_for(".view", id_=id_))
|
||||
|
||||
|
||||
@bp.route("/<id_>/") # type:ignore
|
||||
@bp.route("/<id_>/")
|
||||
async def view(id_: str) -> typing.Union[quart.Response,
|
||||
typing.Tuple[str, int],
|
||||
str]:
|
||||
@@ -124,7 +124,7 @@ class RegisterForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<id_>/register", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/<id_>/register", methods=["GET", "POST"])
|
||||
async def register(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
invite = await client.get_public_invite_by_id(id_)
|
||||
@@ -191,7 +191,7 @@ class ResetForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<id_>/reset", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/<id_>/reset", methods=["GET", "POST"])
|
||||
async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
invite = await client.get_public_invite_by_id(id_)
|
||||
@@ -232,7 +232,7 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/success", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/success", methods=["GET", "POST"])
|
||||
async def success() -> str:
|
||||
return await render_template(
|
||||
"invite_success.html",
|
||||
@@ -240,7 +240,7 @@ async def success() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/success/reset", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/success/reset", methods=["GET", "POST"])
|
||||
async def reset_success() -> str:
|
||||
return await render_template(
|
||||
"invite_reset_success.html",
|
||||
@@ -248,6 +248,6 @@ async def reset_success() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/-") # type:ignore
|
||||
@bp.route("/-")
|
||||
async def index() -> quart.Response:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@@ -17,7 +17,6 @@ from quart import (
|
||||
Response,
|
||||
flash,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
|
||||
import babel
|
||||
import wtforms
|
||||
@@ -48,7 +47,7 @@ class LoginForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/-") # type:ignore
|
||||
@bp.route("/-")
|
||||
async def index() -> quart.Response:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@@ -56,7 +55,7 @@ async def index() -> quart.Response:
|
||||
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
async def login() -> typing.Union[str, quart.Response]:
|
||||
if client.has_session and (await client.test_session()):
|
||||
return redirect(url_for('user.index'))
|
||||
@@ -77,7 +76,7 @@ async def login() -> typing.Union[str, quart.Response]:
|
||||
password = form.password.data
|
||||
try:
|
||||
await client.login(jid, password)
|
||||
except werkzeug.exceptions.Unauthorized:
|
||||
except quart.exceptions.Unauthorized:
|
||||
form.password.errors.append(ERR_CREDENTIALS_INVALID)
|
||||
else:
|
||||
await flash(
|
||||
@@ -89,7 +88,7 @@ async def login() -> typing.Union[str, quart.Response]:
|
||||
return await render_template("login.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/meta/about.html") # type:ignore
|
||||
@bp.route("/meta/about.html")
|
||||
async def about() -> str:
|
||||
version = None
|
||||
extra_versions = {}
|
||||
@@ -103,7 +102,7 @@ async def about() -> str:
|
||||
extra_versions["flask-wtf"] = flask_wtf.__version__
|
||||
try:
|
||||
extra_versions["Prosody"] = await client.get_server_version()
|
||||
except werkzeug.exceptions.Unauthorized:
|
||||
except quart.exceptions.Unauthorized:
|
||||
extra_versions["Prosody"] = "unknown"
|
||||
|
||||
return await render_template(
|
||||
@@ -113,7 +112,7 @@ async def about() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/meta/demo.html") # type:ignore
|
||||
@bp.route("/meta/demo.html")
|
||||
async def demo() -> str:
|
||||
return await render_template("demo.html")
|
||||
|
||||
@@ -122,7 +121,7 @@ def repad(s: str) -> str:
|
||||
return s + "=" * (4 - len(s) % 4)
|
||||
|
||||
|
||||
@bp.route("/avatar/<from_>/<code>") # type:ignore
|
||||
@bp.route("/avatar/<from_>/<code>")
|
||||
async def avatar(from_: str, code: str) -> quart.Response:
|
||||
etag: typing.Optional[str]
|
||||
try:
|
||||
@@ -166,6 +165,6 @@ async def avatar(from_: str, code: str) -> quart.Response:
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/_health") # type:ignore
|
||||
@bp.route("/_health")
|
||||
async def health() -> Response:
|
||||
return Response("STATUS OK", content_type="text/plain")
|
||||
|
||||
@@ -19,8 +19,7 @@ from quart import (
|
||||
current_app, _app_ctx_stack, session as http_session, abort, redirect,
|
||||
url_for,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
import quart
|
||||
import quart.exceptions
|
||||
|
||||
from . import xmpputil
|
||||
from .xmpputil import split_jid
|
||||
@@ -272,9 +271,8 @@ class ProsodyClient:
|
||||
|
||||
def init_app(self, app: quart.Quart) -> None:
|
||||
app.config[self.CONFIG_ENDPOINT]
|
||||
# the type annotation in quart seems to be wrong here
|
||||
app.teardown_appcontext(self._plain_session.teardown) # type:ignore
|
||||
app.teardown_appcontext(self._auth_session.teardown) # type:ignore
|
||||
app.teardown_appcontext(self._plain_session.teardown)
|
||||
app.teardown_appcontext(self._auth_session.teardown)
|
||||
|
||||
@property
|
||||
def _endpoint_base(self) -> str:
|
||||
@@ -491,7 +489,7 @@ class ProsodyClient:
|
||||
session=session,
|
||||
)
|
||||
avatar_hash = avatar_info["sha1"]
|
||||
except werkzeug.exceptions.HTTPException:
|
||||
except quart.exceptions.HTTPException:
|
||||
avatar_hash = None
|
||||
|
||||
return {
|
||||
@@ -643,7 +641,7 @@ class ProsodyClient:
|
||||
new_access_model,
|
||||
)
|
||||
))
|
||||
except werkzeug.exceptions.NotFound:
|
||||
except quart.exceptions.NotFound:
|
||||
if ignore_not_found:
|
||||
return
|
||||
raise
|
||||
@@ -773,7 +771,7 @@ class ProsodyClient:
|
||||
session: aiohttp.ClientSession,
|
||||
) -> str:
|
||||
access_models = filter(
|
||||
lambda x: not isinstance(x, werkzeug.exceptions.NotFound),
|
||||
lambda x: not isinstance(x, quart.exceptions.NotFound),
|
||||
await asyncio.gather(
|
||||
self.get_avatar_access_model(session=session),
|
||||
self.get_nickname_access_model(session=session),
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<p>{% trans %}After installing Snikket via F-Droid, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<ol>
|
||||
<li><p>{% trans %}First install Snikket from F-Droid using the button below:{% endtrans %}</p>
|
||||
<p><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<p><a href="{{ f_droid_url }}"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
|
||||
<p>
|
||||
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import typing
|
||||
|
||||
import quart
|
||||
import quart.flask_patch
|
||||
from quart import (
|
||||
Blueprint,
|
||||
@@ -12,7 +11,7 @@ from quart import (
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
import quart.exceptions
|
||||
|
||||
import wtforms
|
||||
|
||||
@@ -76,14 +75,14 @@ class ProfileForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/") # type:ignore
|
||||
@bp.route("/")
|
||||
@client.require_session()
|
||||
async def index() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
return await render_template("user_home.html", user_info=user_info)
|
||||
|
||||
|
||||
@bp.route('/passwd', methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route('/passwd', methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def change_pw() -> typing.Union[str, quart.Response]:
|
||||
form = ChangePasswordForm()
|
||||
@@ -93,8 +92,8 @@ async def change_pw() -> typing.Union[str, quart.Response]:
|
||||
form.current_password.data,
|
||||
form.new_password.data,
|
||||
)
|
||||
except (werkzeug.exceptions.Unauthorized,
|
||||
werkzeug.exceptions.Forbidden):
|
||||
except (quart.exceptions.Unauthorized,
|
||||
quart.exceptions.Forbidden):
|
||||
# server refused current password, set an appropriate error
|
||||
form.current_password.errors.append(
|
||||
_("Incorrect password."),
|
||||
@@ -115,7 +114,7 @@ EAVATARTOOBIG = _l(
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/profile", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/profile", methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def profile() -> typing.Union[str, quart.Response]:
|
||||
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
|
||||
@@ -169,7 +168,7 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
avatar_too_big_warning=EAVATARTOOBIG)
|
||||
|
||||
|
||||
@bp.route("/logout", methods=["GET", "POST"]) # type:ignore
|
||||
@bp.route("/logout", methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def logout() -> typing.Union[quart.Response, str]:
|
||||
form = LogoutForm()
|
||||
|
||||
@@ -4,7 +4,7 @@ import typing
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from quart import abort
|
||||
import werkzeug.exceptions
|
||||
import quart.exceptions
|
||||
|
||||
|
||||
TAG_XMPP_ERROR = "error"
|
||||
@@ -234,7 +234,7 @@ def extract_pubsub_item_get_reply(
|
||||
) -> typing.Optional[ET.Element]:
|
||||
try:
|
||||
pubsub = extract_iq_reply(iq_tree, TAG_PUBSUB)
|
||||
except werkzeug.exceptions.NotFound:
|
||||
except quart.exceptions.NotFound:
|
||||
return None
|
||||
|
||||
if pubsub is None:
|
||||
|
||||
Reference in New Issue
Block a user