diff --git a/relay/database/connection.py b/relay/database/connection.py index f2247aa..2792111 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -266,10 +266,10 @@ class Connection(SqlConnection): params = {} - if reason: + if reason is not None: params['reason'] = reason - if note: + if note is not None: params['note'] = note statement = Update('domain_bans', params) @@ -321,10 +321,10 @@ class Connection(SqlConnection): params = {} - if reason: + if reason is not None: params['reason'] = reason - if note: + if note is not None: params['note'] = note statement = Update('software_bans', params) diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 8fb7e80..13b83e9 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -1,6 +1,6 @@ -macro menu_item(name, path) - -if view.request.path == path - %a.button(active="true") -> =name + -if view.request.path == path or (path != "/" and view.request.path.startswith(path)) + %a.button(href="{{path}}" active="true") -> =name -else %a.button(href="{{path}}") -> =name @@ -69,8 +69,15 @@ 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"}); + menu_open.addEventListener("click", (event) => { + var new_value = menu.attributes.visible.nodeValue === "true" ? "false" : "true"; + menu.attributes.visible.nodeValue = new_value; + }); + + menu_close.addEventListener("click", (event) => { + menu.attributes.visible.nodeValue = "false" + }); + body.addEventListener("click", (event) => { if (event.target === menu_open) { return; diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index 6550244..e2b3e5a 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -1,5 +1,48 @@ -extends "base.haml" -set page="Domain Bans" -block content - .section - UvU + %details.section + %summary << Ban Domain + %form(action="/admin/domain_bans", method="POST") + #add-domain + %label(for="domain") << Domain + %input(type="domain" id="domain" name="domain" placeholder="Domain") + + %label(for="reason") << Ban Reason + %textarea(id="reason" name="reason") << {{""}} + + %label(for="note") << Admin Note + %textarea(id="note" name="note") << {{""}} + + %input(type="submit" value="Ban Domain") + + #domains.section + %table + %thead + %tr + %td.domain << Instance + %td.date << Joined + %td.remove + + %tbody + -for ban in bans + %tr + %td.domain + %details + %summary -> =ban.domain + %form(action="/admin/domain_bans" method="POST") + .items + .reason << Reason + %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} + + .note << Note + %textarea.note(id="note" name="note") << {{ban.note or ""}} + + %input(type="hidden" name="domain", value="{{ban.domain}}") + %input(type="submit" value="Update") + + %td.date + =ban.created.strftime("%Y-%m-%d") + + %td.remove + %a(href="/admin/domain_bans/delete/{{ban.domain}}" title="Unban domain") << ✖ diff --git a/relay/frontend/style.css b/relay/frontend/style.css index cc4818f..986426f 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -133,9 +133,8 @@ table tbody td { padding: 5px; } -#menu > a[active="true"] { - cursor: default; - background-color: var(--background); +#menu > a[active="true"]:not(:hover) { + background-color: var(--primary-hover); color: var(--primary); border-color: transparent; } diff --git a/relay/frontend/style/domain_bans.css b/relay/frontend/style/domain_bans.css index e69de29..437ab98 100644 --- a/relay/frontend/style/domain_bans.css +++ b/relay/frontend/style/domain_bans.css @@ -0,0 +1,40 @@ +form input[type="submit"] { + display: block; + margin: 0 auto; +} + +textarea { + height: calc(5em); +} + +table .items { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); +} + +#domains table { + width: 100%; +} + +#domains .domain { + width: 100%; +} + +#domains .date { + width: max-content; + text-align: right; +} + +#domains thead td { + text-align: center !important; +} + +#add-domain { + display: grid; + grid-template-columns: max-content auto; + grid-gap: var(--spacing); + margin-top: var(--spacing); + margin-bottom: var(--spacing); +} diff --git a/relay/views/base.py b/relay/views/base.py index 8d6d1ff..65859a5 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -12,7 +12,8 @@ from ..misc import Response if typing.TYPE_CHECKING: from collections.abc import Callable, Coroutine, Generator - from tinysql import Database + from bsql import Database + from typing import Self from ..application import Application from ..cache import Cache from ..config import Config @@ -28,7 +29,7 @@ def register_route(*paths: str) -> Callable: for path in paths: VIEWS.append([path, view]) - return View + return view return wrapper @@ -43,8 +44,14 @@ class View(AbstractView): return self._run_handler(handler).__await__() - async def _run_handler(self, handler: Coroutine) -> Response: - return await handler(self.request, **self.request.match_info) + @classmethod + async def run(cls: type[Self], method: str, request: Request, **kwargs: Any) -> Self: + view = cls(request) + return await view.handlers[method](request, **kwargs) + + + async def _run_handler(self, handler: Coroutine, **kwargs: Any) -> Response: + return await handler(self.request, **self.request.match_info, **kwargs) @cached_property diff --git a/relay/views/frontend.py b/relay/views/frontend.py index 616017c..3f90234 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -186,18 +186,85 @@ class AdminInstancesDelete(View): @register_route('/admin/whitelist') class AdminWhitelist(View): - async def get(self, request: Request) -> Response: - data = self.template.render('page/admin-whitelist.haml', self) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'domains': tuple(conn.execute('SELECT * FROM whitelist').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-whitelist.haml', self, **context) 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) + async def get(self, + request: Request, + error: str | None = None, + message: str | None = None) -> Response: + + with self.database.session() as conn: + context = { + 'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC').all()) + } + + if error: + context['error'] = error + + if message: + context['message'] = message + + data = self.template.render('page/admin-domain_bans.haml', self, **context) return Response.new(data, ctype = 'html') + async def post(self, request: Request) -> Response: + data = await request.post() + print(data) + + if not data['domain']: + return await self.get(request, error = 'Missing domain') + + with self.database.session(True) as conn: + if (ban := conn.get_domain_ban(data['domain'])): + conn.update_domain_ban( + data['domain'], + data.get('reason'), + data.get('note') + ) + + else: + conn.put_domain_ban( + data['domain'], + data.get('reason'), + data.get('note') + ) + + return await self.get(request, message = "Added/updated domain ban") + + +@register_route('/admin/domain_bans/delete/{domain}') +class AdminDomainBansDelete(View): + async def get(self, request: Request, domain: str) -> Response: + with self.database.session() as conn: + if not (conn.get_domain_ban(domain)): + return await AdminDomainBans.run("GET", request, message = 'Domain ban not found') + + conn.del_domain_ban(domain) + + return await AdminDomainBans.run("GET", request, message = 'Unbanned domain') + + @register_route('/admin/software_bans') class AdminSoftwareBans(View): async def get(self, request: Request) -> Response: