From 7af3b9c20bf7845ab898aa3baf4de3e6238662d1 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Sat, 2 Mar 2024 17:36:44 -0500 Subject: [PATCH] add login/logout and start on admin interface --- relay/application.py | 4 +- relay/database/config.py | 15 +- relay/frontend/base.haml | 60 +++++++- relay/frontend/functions.haml | 1 + relay/frontend/page/admin-config.haml | 5 + relay/frontend/page/admin-domain_bans.haml | 5 + relay/frontend/page/admin-instances.haml | 5 + relay/frontend/page/admin-software_bans.haml | 5 + relay/frontend/page/admin-whitelist.haml | 5 + relay/frontend/page/login.haml | 9 ++ relay/frontend/style.css | 123 ++++++++++++++-- relay/frontend/style/config.css | 0 relay/frontend/style/domain_bans.css | 0 relay/frontend/style/home.css | 16 ++ relay/frontend/style/instances.css | 0 relay/frontend/style/login.css | 18 +++ relay/frontend/style/software_bans.css | 0 relay/frontend/style/whitelist.css | 0 relay/misc.py | 6 + relay/views/frontend.py | 145 ++++++++++++++++++- 20 files changed, 402 insertions(+), 20 deletions(-) create mode 100644 relay/frontend/functions.haml create mode 100644 relay/frontend/page/admin-config.haml create mode 100644 relay/frontend/page/admin-domain_bans.haml create mode 100644 relay/frontend/page/admin-instances.haml create mode 100644 relay/frontend/page/admin-software_bans.haml create mode 100644 relay/frontend/page/admin-whitelist.haml create mode 100644 relay/frontend/page/login.haml create mode 100644 relay/frontend/style/config.css create mode 100644 relay/frontend/style/domain_bans.css create mode 100644 relay/frontend/style/home.css create mode 100644 relay/frontend/style/instances.css create mode 100644 relay/frontend/style/login.css create mode 100644 relay/frontend/style/software_bans.css create mode 100644 relay/frontend/style/whitelist.css diff --git a/relay/application.py b/relay/application.py index 9a49433..6fc09ae 100644 --- a/relay/application.py +++ b/relay/application.py @@ -23,6 +23,7 @@ from .misc import check_open_port, get_resource from .template import Template from .views import VIEWS from .views.api import handle_api_path +from .views.frontend import handle_frontend_path if typing.TYPE_CHECKING: from tinysql import Database, Row @@ -38,7 +39,8 @@ class Application(web.Application): def __init__(self, cfgpath: str | None): web.Application.__init__(self, middlewares = [ - handle_api_path + handle_api_path, + handle_frontend_path ] ) diff --git a/relay/database/config.py b/relay/database/config.py index c961a0c..d49f1b5 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -22,7 +22,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' }, 'pink': { 'text': '#DDD', @@ -34,7 +37,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' }, 'blue': { 'text': '#DDD', @@ -46,7 +52,10 @@ THEMES = { 'border': '#444', 'message-text': '#DDD', 'message-background': '#335', - 'message-border': '#446' + 'message-border': '#446', + 'error-text': '#DDD', + 'error-background': '#533', + 'error-border': '#644' } } diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 2df7899..8fb7e80 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -1,22 +1,80 @@ +-macro menu_item(name, path) + -if view.request.path == path + %a.button(active="true") -> =name + + -else + %a.button(href="{{path}}") -> =name + !!! %html %head %title << {{config.name}}: {{page}} %meta(charset="UTF-8") %meta(name="viewport" content="width=device-width, initial-scale=1") - %link(rel="stylesheet" type="text/css" href="/style.css") + %link(rel="stylesheet" type="text/css" href="/style.css?page={{page}}") -block head %body + #menu.section(visible="false") + .menu-head + %span.menu-title << Menu + %span#menu-close.button << ✖ + + {{menu_item("Home", "/")}} + + -if view.request["user"] + {{menu_item("Instances", "/admin/instances")}} + {{menu_item("Whitelist", "/admin/whitelist")}} + {{menu_item("Domain Bans", "/admin/domain_bans")}} + {{menu_item("Software Bans", "/admin/software_bans")}} + {{menu_item("Config", "/admin/config")}} + {{menu_item("Logout", "/logout")}} + + -else + {{menu_item("Login", "/login")}} + #container #header.section + %span#menu-open.button << ⁞ %a(href="https://{{domain}}/") -> =config.name + .empty + + -if error + .error.section -> =error + + -if message + .message.section -> =message #content -block content #footer.section .col1 + -if not view.request["user"] + %a(href="/login") << Login + + -else + =view.request["user"]["username"] + ( + %a(href="/logout") << Logout + ) + .version %a(href="https://git.pleroma.social/pleroma/relay") ActivityRelay/{{version}} + + %script(type="application/javascript") + const body = document.getElementById("container") + const menu = document.getElementById("menu"); + const menu_open = document.getElementById("menu-open"); + const menu_close = document.getElementById("menu-close"); + + menu_open.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "true"}); + menu_close.addEventListener("click", (event) => {menu.attributes.visible.nodeValue = "false"}); + body.addEventListener("click", (event) => { + if (event.target === menu_open) { + return; + } + + menu.attributes.visible.nodeValue = "false"; + }); diff --git a/relay/frontend/functions.haml b/relay/frontend/functions.haml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/relay/frontend/functions.haml @@ -0,0 +1 @@ + diff --git a/relay/frontend/page/admin-config.haml b/relay/frontend/page/admin-config.haml new file mode 100644 index 0000000..6587dbe --- /dev/null +++ b/relay/frontend/page/admin-config.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Config" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml new file mode 100644 index 0000000..6550244 --- /dev/null +++ b/relay/frontend/page/admin-domain_bans.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Domain Bans" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml new file mode 100644 index 0000000..04c84f9 --- /dev/null +++ b/relay/frontend/page/admin-instances.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Instances" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml new file mode 100644 index 0000000..c215fc8 --- /dev/null +++ b/relay/frontend/page/admin-software_bans.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Software Bans" +-block content + .section + UvU diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml new file mode 100644 index 0000000..083c8bb --- /dev/null +++ b/relay/frontend/page/admin-whitelist.haml @@ -0,0 +1,5 @@ +-extends "base.haml" +-set page="Whitelist" +-block content + .section + UvU diff --git a/relay/frontend/page/login.haml b/relay/frontend/page/login.haml new file mode 100644 index 0000000..ee7d1dc --- /dev/null +++ b/relay/frontend/page/login.haml @@ -0,0 +1,9 @@ +-extends "base.haml" +-set page="Login" +-block content + %form.section(action="/login" method="post") + %label(for="username") << Username + %input(id="username" name="username" placeholder="Username" value="{{username or ''}}") + %label(for="password") << Password + %input(id="password" name="password" placeholder="Password" type="password") + %input(type="submit" value="Login") diff --git a/relay/frontend/style.css b/relay/frontend/style.css index 743b2b5..61c358d 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -9,6 +9,9 @@ --message-text: {{theme["message-text"]}}; --message-background: {{theme["message-background"]}}; --message-border: {{theme["message-border"]}}; + --error-text: {{theme["error-text"]}}; + --error-background: {{theme["error-background"]}}; + --error-border: {{theme["error-border"]}}; --spacing: 10px; } @@ -84,31 +87,76 @@ table tbody td { #container { width: 1024px; margin: 0px auto; + height: 100vh; } #header { - text-align: center; + display: grid; + grid-template-columns: 50px auto 50px; + justify-items: center; + align-items: center; } -#header a { - font-size: 3em; +#header > * { + font-size: 2em; } -#instances table { - width: 100%; +#header > *:nth-child(2) { + font-weight: bold; } -#instances .instance { - width: 100%; -} - -#instances .date { +#menu { + padding: 0px; + position: fixed; + top: 0px; + left: 0px; + margin: 0px; + height: 100%; width: max-content; - text-align: right; + z-index: 1; + font-size: 1.5em; + min-width: 300px; } -#instances thead td { - text-align: center !important; +#menu[visible="false"] { + visibility: hidden; +} + +#menu > a { + margin: var(--spacing); + display: block; + border-radius: 5px; + padding: 5px; +} + +#menu > a[active="true"] { + cursor: default; + background-color: var(--background); + color: var(--primary); + border-color: transparent; +} + +#menu .menu-head { + display: grid; + grid-template-columns: auto max-content; + margin: var(--spacing); +} + +#menu .menu-head > * { + font-weight: bold; +} + +#menu .menu-title { + color: var(--primary); +} + +#menu-open, #menu-close { + cursor: pointer; +} + +#menu-close, #menu-open { + min-width: 35px; + text-align: center; } #footer { @@ -121,6 +169,34 @@ table tbody td { } +.button { + background-color: var(--primary); + border: 1px solid var(--primary); + color: var(--background); + border-radius: 5px; +} + +.button:hover { + background-color: var(--background); + color: var(--primary); + text-decoration: None; +} + +.container { + display: grid; + grid-template-columns: max-content auto; +} + +.error, .message { + text-align: center; +} + +.error{ + color: var(--error-text) !important; + background-color: var(--error-background) !important; + border: 1px solid var(--error-border) !important; +} + .message { color: var(--message-text) !important; background-color: var(--message-background) !important; @@ -143,16 +219,37 @@ table tbody td { } +{% if page %} + {% include "style/" + page.lower().replace(" ", "_") + ".css" %} +{% endif %} + + @media (max-width: 1026px) { body { margin: 0px; } + #menu { + width: 100%; + } + + #menu > a { + text-align: center; + } + #container { width: unset; margin: unset; } + .container { + grid-template-columns: auto; + } + + .content { + grid-template-columns: auto; + } + .section { border-width: 0px; border-radius: 0px; diff --git a/relay/frontend/style/config.css b/relay/frontend/style/config.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/home.css b/relay/frontend/style/home.css new file mode 100644 index 0000000..4cdfaa9 --- /dev/null +++ b/relay/frontend/style/home.css @@ -0,0 +1,16 @@ +#instances table { + width: 100%; +} + +#instances .instance { + width: 100%; +} + +#instances .date { + width: max-content; + text-align: right; +} + +#instances thead td { + text-align: center !important; +} diff --git a/relay/frontend/style/instances.css b/relay/frontend/style/instances.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/login.css b/relay/frontend/style/login.css new file mode 100644 index 0000000..48f623b --- /dev/null +++ b/relay/frontend/style/login.css @@ -0,0 +1,18 @@ +label, input { + margin: 0 auto; + display: block; +} + +label, input:not([type="submit"]) { + width: 50%; +} + +input:not([type="submit"]) { + margin-bottom: var(--spacing); +} + +@media (max-width: 1026px) { + label, input:not([type="submit"]) { + width: 75%; + } +} diff --git a/relay/frontend/style/software_bans.css b/relay/frontend/style/software_bans.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/frontend/style/whitelist.css b/relay/frontend/style/whitelist.css new file mode 100644 index 0000000..e69de29 diff --git a/relay/misc.py b/relay/misc.py index b4d333c..3007863 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -225,6 +225,12 @@ class Response(AiohttpResponse): return cls.new(body=body, status=status, ctype=ctype) + @classmethod + def new_redir(cls: type[Response], path: str) -> Response: + body = f'Redirect to {path}' + return cls.new(body, 302, {'Location': path}) + + @property def location(self) -> str: return self.headers.get('Location') diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 567a44b..7b45011 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -2,6 +2,9 @@ from __future__ import annotations import typing +from aiohttp import web +from argon2.exceptions import VerifyMismatchError + from .base import View, register_route from ..misc import Response @@ -10,6 +13,44 @@ if typing.TYPE_CHECKING: from aiohttp.web import Request +AUTH_ROUTES = { + '/admin', + '/admin/instances', + '/admin/domain_bans', + '/admin/software_bans', + '/admin/whitelist', + '/admin/config', + '/logout' +} + + +UNAUTH_ROUTES = { + '/', + '/login' +} + +ALL_ROUTES = {*AUTH_ROUTES, *UNAUTH_ROUTES} + + +@web.middleware +async def handle_frontend_path(request: web.Request, handler: Coroutine) -> Response: + if request.path in ALL_ROUTES: + request['token'] = request.cookies.get('user-token') + request['user'] = None + + if request['token']: + with request.app.database.session(False) as conn: + request['user'] = conn.get_user_by_token(request['token']) + + if request['user'] and request.path == '/login': + return Response.new('', 302, {'Location': '/'}) + + if not request['user'] and request.path.startswith('/admin'): + return Response.new('', 302, {'Location': f'/login?redir={request.path}'}) + + return await handler(request) + + # pylint: disable=unused-argument @register_route('/') @@ -25,12 +66,112 @@ class HomeView(View): # targets = '
'.join(inbox['domain'] for inbox in inboxes) # ) - data = self.template.render('page/home.haml', instances = instances) + data = self.template.render('page/home.haml', self, instances = instances) return Response.new(data, ctype='html') +@register_route('/login') +class Login(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/login.haml', self) + return Response.new(data, ctype = 'html') + + + async def post(self, request: Request) -> Response: + form = await request.post() + params = {} + + with self.database.session(True) as conn: + if not (user := conn.get_user(form['username'])): + params = { + 'username': form['username'], + 'error': 'User not found' + } + + else: + try: + conn.hasher.verify(user['hash'], form['password']) + + except VerifyMismatchError: + params = { + 'username': form['username'], + 'error': 'Invalid password' + } + + if params: + data = self.template.render('page/login.haml', self, **params) + return Response.new(data, ctype = 'html') + + token = conn.put_token(user['username']) + resp = Response.new_redir(request.query.getone('redir', '/')) + resp.set_cookie( + 'user-token', + token['code'], + max_age = 60 * 60 * 24 * 365, + domain = self.config.domain, + path = '/', + secure = True, + httponly = True, + samesite = 'Strict' + ) + + return resp + + +@register_route('/logout') +class Logout(View): + async def get(self, request: Request) -> Response: + with self.database.session(True) as conn: + conn.del_token(request['token']) + + resp = Response.new_redir('/') + resp.del_cookie('user-token', domain = self.config.domain, path = '/') + return resp + + +@register_route('/admin') +class Admin(View): + async def get(self, request: Request) -> Response: + return Response.new('', 302, {'Location': '/admin/instances'}) + + +@register_route('/admin/instances') +class AdminInstances(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-instances.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/whitelist') +class AdminWhitelist(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-whitelist.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/domain_bans') +class AdminDomainBans(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-domain_bans.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/software_bans') +class AdminSoftwareBans(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-software_bans.haml', self) + return Response.new(data, ctype = 'html') + + +@register_route('/admin/config') +class AdminConfig(View): + async def get(self, request: Request) -> Response: + data = self.template.render('page/admin-config.haml', self) + return Response.new(data, ctype = 'html') + + @register_route('/style.css') class StyleCss(View): async def get(self, request: Request) -> Response: - data = self.template.render('style.css') + data = self.template.render('style.css', self, page = request.query.getone('page', "")) return Response.new(data, ctype = 'css')