Compare commits

..

13 commits

Author SHA1 Message Date
Izalia Mae a0ee22406b don't use spec file when building bin 2024-03-04 05:39:21 -05:00
Izalia Mae 0af6a33b69 use darker text color for active menu item 2024-03-04 02:34:28 -05:00
Izalia Mae 01a491f272 replace target with action in forms 2024-03-04 02:32:57 -05:00
Izalia Mae 5f156401c7 fix linter warnings 2024-03-04 02:30:54 -05:00
Izalia Mae 4cbf83a7b7 simplify css 2024-03-04 02:19:26 -05:00
Izalia Mae 33102f9e4e disable caching for now 2024-03-04 01:42:27 -05:00
Izalia Mae b73fdece95 create admin users page 2024-03-04 01:40:51 -05:00
Izalia Mae 2223695d15 remove commas in element attributes 2024-03-04 01:14:17 -05:00
Izalia Mae d44498b966 fix dev mode 2024-03-04 01:09:40 -05:00
Izalia Mae 5dcf375247 create admin whitelist page 2024-03-04 01:00:57 -05:00
Izalia Mae a163f2baab create admin software bans page 2024-03-04 00:43:12 -05:00
Izalia Mae d7cfa12145 add Cache-Control header 2024-03-04 00:32:13 -05:00
Izalia Mae 4639d8a78d minor frontend tweaks 2024-03-03 18:28:49 -05:00
27 changed files with 443 additions and 291 deletions

View file

@ -1,5 +0,0 @@
include frontend/base.haml
include frontend/style.css
include data/statements.sql
include data/swagger.yaml
include frontend/page/home.haml

View file

@ -1,56 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import importlib
from pathlib import Path
block_cipher = None
aiohttp_swagger_path = Path(importlib.import_module('aiohttp_swagger').__file__).parent
a = Analysis(
['relay/__main__.py'],
pathex=[],
binaries=[],
datas=[
('relay/data', 'relay/data'),
('relay/frontend', 'relay/frontend'),
(aiohttp_swagger_path, 'aiohttp_swagger')
],
hiddenimports=[
'pg8000',
'sqlite3'
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='activityrelay',
icon=None,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -1,5 +1,8 @@
import multiprocessing
from relay.manage import main
if __name__ == '__main__':
multiprocessing.freeze_support()
main()

View file

@ -26,9 +26,10 @@ from .views.api import handle_api_path
from .views.frontend import handle_frontend_path
if typing.TYPE_CHECKING:
from collections.abc import Coroutine
from tinysql import Database, Row
from .cache import Cache
from .misc import Message
from .misc import Message, Response
# pylint: disable=unsubscriptable-object
@ -36,11 +37,12 @@ if typing.TYPE_CHECKING:
class Application(web.Application):
DEFAULT: Application = None
def __init__(self, cfgpath: str | None):
def __init__(self, cfgpath: str | None, dev: bool = False):
web.Application.__init__(self,
middlewares = [
handle_api_path,
handle_frontend_path
handle_frontend_path,
handle_response_headers
]
)
@ -50,6 +52,7 @@ class Application(web.Application):
self['signer'] = None
self['start_time'] = None
self['cleanup_thread'] = None
self['dev'] = dev
self['config'] = Config(cfgpath, load = True)
self['database'] = get_database(self.config)
@ -255,25 +258,18 @@ class PushWorker(multiprocessing.Process):
await client.close()
@web.middleware
async def handle_response_headers(request: web.Request, handler: Coroutine) -> Response:
resp = await handler(request)
resp.headers['Server'] = 'ActivityRelay'
async def handle_access_log(request: web.Request, response: web.Response) -> None:
address = request.headers.get(
'X-Forwarded-For',
request.headers.get(
'X-Real-Ip',
request.remote
)
)
# if not request.app['dev'] and request.path.endswith(('.css', '.js')):
# resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
logging.info(
'%s "%s %s" %i %i "%s"',
address,
request.method,
request.path,
response.status,
response.content_length or 0,
request.headers.get('User-Agent', 'n/a')
)
# else:
# resp.headers['Cache-Control'] = 'no-store'
return resp
async def handle_cleanup(app: Application) -> None:

View file

@ -52,6 +52,10 @@ if IS_DOCKER:
class Config:
def __init__(self, path: str, load: bool = False):
if path:
self.path = Path(path).expanduser().resolve()
else:
self.path = Config.get_config_dir()
self.listen = None

View file

@ -1,10 +1,14 @@
import click
import platform
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from . import __version__
try:
from watchdog.observers import Observer
@ -46,24 +50,32 @@ def cli_lint(path):
subprocess.run([sys.executable, '-m', 'pylint', path], check = False)
@cli.command('manifest-gen')
def cli_manifest_install():
paths = []
for path in SCRIPT.rglob('*'):
if path.suffix.lower() in IGNORE_EXT or not path.is_file():
continue
paths.append(path)
with REPO.joinpath('MANIFEST.in').open('w', encoding = 'utf-8') as fd:
for path in paths:
fd.write(f'include {str(path.relative_to(SCRIPT))}\n')
@cli.command('build')
def cli_build():
cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec']
with TemporaryDirectory() as tmp:
arch = 'amd64' if sys.maxsize >= 2**32 else 'i386'
cmd = [
sys.executable, '-m', 'PyInstaller',
'--collect-data', 'relay',
'--collect-data', 'aiohttp_swagger',
'--hidden-import', 'pg8000',
'--hidden-import', 'sqlite3',
'--name', f'activityrelay-{__version__}-{platform.system().lower()}-{arch}',
'--workpath', tmp,
'--onefile', 'relay/__main__.py',
]
if platform.system() == 'Windows':
cmd.append('--console')
# putting the spec path on a different drive than the source dir breaks
if str(SCRIPT)[0] == tmp[0]:
cmd.extend(['--specpath', tmp])
else:
cmd.append('--strip')
cmd.extend(['--specpath', tmp])
subprocess.run(cmd, check = False)
@ -94,7 +106,7 @@ def cli_run():
class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py']
cmd = [sys.executable, '-m', 'relay', 'run']
cmd = [sys.executable, '-m', 'relay', 'run', '-d']
def __init__(self):
@ -145,7 +157,6 @@ class WatchHandler(PatternMatchingEventHandler):
if event.event_type not in ['modified', 'created', 'deleted']:
return
print(event.src_path)
self.run_proc(restart = True)

View file

@ -11,7 +11,7 @@
%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?page={{page}}")
%link(rel="stylesheet" type="text/css" href="/style.css")
-block head
%body
@ -27,6 +27,7 @@
{{menu_item("Whitelist", "/admin/whitelist")}}
{{menu_item("Domain Bans", "/admin/domain_bans")}}
{{menu_item("Software Bans", "/admin/software_bans")}}
{{menu_item("Users", "/admin/users")}}
{{menu_item("Config", "/admin/config")}}
{{menu_item("Logout", "/logout")}}
@ -35,8 +36,13 @@
#container
#header.section
%span#menu-open.button << &#8286;
%a(href="https://{{domain}}/") -> =config.name
%span#menu-open << &#8286;
%span.title-container
%a.title(href="/") -> =config.name
-if view.request.path not in ["/", "/login"]
.page -> =page
.empty
-if error
@ -45,7 +51,7 @@
-if message
.message.section -> =message
#content
#content(class="page-{{page.lower().replace(' ', '_')}}")
-block content
#footer.section

View file

@ -2,7 +2,7 @@
-set page="Config"
-block content
%form.section(action="/admin/config" method="POST")
#config-options
.grid-2col
%label(for="name") << Name
%input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}")

View file

@ -3,8 +3,8 @@
-block content
%details.section
%summary << Ban Domain
%form(action="/admin/domain_bans", method="POST")
#add-domain
%form(action="/admin/domain_bans" method="POST")
#add-item
%label(for="domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain")
@ -16,12 +16,12 @@
%input(type="submit" value="Ban Domain")
#domains.section
#data-table.section
%table
%thead
%tr
%td.domain << Instance
%td.date << Joined
%td << Date
%td.remove
%tbody
@ -31,14 +31,14 @@
%details
%summary -> =ban.domain
%form(action="/admin/domain_bans" method="POST")
.items
.grid-2col
.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="hidden" name="domain" value="{{ban.domain}}")
%input(type="submit" value="Update")
%td.date

View file

@ -3,20 +3,23 @@
-block content
%details.section
%summary << Add Instance
%form(target="/admin/instances", method="POST")
#add-instance
%form(action="/admin/instances" method="POST")
#add-item
%label(for="domain") << Domain
%input(type="domain", id="domain" name="domain", placeholder="Domain")
%input(type="domain" id="domain" name="domain" placeholder="Domain")
%label(for="actor") << Actor URL
%input(type="url", id="actor" name="actor", placeholder="Actor URL")
%input(type="url" id="actor" name="actor" placeholder="Actor URL")
%label(for="inbox") << Inbox URL
%input(type="url", id="inbox" name="inbox", placeholder="Inbox URL")
%input(type="url" id="inbox" name="inbox" placeholder="Inbox URL")
%label(for="software") << Software
%input(name="software", id="software" placeholder="software")
%input(name="software" id="software" placeholder="software")
%input(type="submit" value="Add Instance")
#instances.section
#data-table.section
%table
%thead
%tr

View file

@ -1,5 +1,48 @@
-extends "base.haml"
-set page="Software Bans"
-block content
.section
UvU
%details.section
%summary << Ban Software
%form(action="/admin/software_bans" method="POST")
#add-item
%label(for="name") << Name
%input(id="name" name="name" placeholder="Name")
%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 Software")
#data-table.section
%table
%thead
%tr
%td.name << Instance
%td << Date
%td.remove
%tbody
-for ban in bans
%tr
%td.name
%details
%summary -> =ban.name
%form(action="/admin/software_bans" method="POST")
.grid-2col
.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="name" value="{{ban.name}}")
%input(type="submit" value="Update")
%td.date
=ban.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/software_bans/delete/{{ban.name}}" title="Unban software") << &#10006;

View file

@ -0,0 +1,44 @@
-extends "base.haml"
-set page="Users"
-block content
%details.section
%summary << Add User
%form(action="/admin/users", method="POST")
#add-item
%label(for="username") << Username
%input(id="username" name="username" placeholder="Username")
%label(for="password") << Password
%input(type="password" id="password" name="password" placeholder="Password")
%label(for="password2") << Password Again
%input(type="password" id="password2" name="password2" placeholder="Password Again")
%label(for="handle") << Handle
%input(type="email" name="handle" id="handle" placeholder="handle")
%input(type="submit" value="Add User")
#data-table.section
%table
%thead
%tr
%td.username << Username
%td.handle << Handle
%td.date << Joined
%td.remove
%tbody
-for user in users
%tr
%td.username
=user.username
%td.handle
=user.handle or "n/a"
%td.date
=user.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/users/delete/{{user.username}}" title="Remove User") << &#10006;

View file

@ -1,5 +1,31 @@
-extends "base.haml"
-set page="Whitelist"
-block content
.section
UvU
%details.section
%summary << Add Domain
%form(action="/admin/whitelist" method="POST")
#add-item
%label(for="domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain")
%input(type="submit" value="Add Domain")
#data-table.section
%table
%thead
%tr
%td.domain << Domain
%td.date << Added
%td.remove
%tbody
-for item in whitelist
%tr
%td.domain
=item.domain
%td.date
=item.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/whitelist/delete/{{item.domain}}" title="Remove whitlisted domain") << &#10006;

View file

@ -19,7 +19,7 @@
Note: The whitelist is enabled on this instance. Ask the admin to add your instance
before joining.
#instances.section
#data-table.section
%table
%thead
%tr
@ -29,5 +29,8 @@
%tbody
-for instance in instances
%tr
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain
%td.date -> =instance.created.strftime("%Y-%m-%d")
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new")
=instance.domain
%td.date
=instance.created.strftime("%Y-%m-%d")

View file

@ -1,9 +1,12 @@
-extends "base.haml"
-set page="Login"
-block content
%form.section(action="/login" method="post")
%form.section(action="/login" method="POST")
.grid-2col
%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")

View file

@ -15,13 +15,6 @@
--spacing: 10px;
}
body {
color: var(--text);
background-color: #222;
margin: var(--spacing);
font-family: sans serif;
}
a {
color: var(--primary);
text-decoration: none;
@ -32,10 +25,26 @@ a:hover {
text-decoration: underline;
}
body {
color: var(--text);
background-color: #222;
margin: var(--spacing);
font-family: sans serif;
}
details *:nth-child(2) {
margin-top: 5px;
}
details summary {
cursor: pointer;
}
form input[type="submit"] {
display: block;
margin: 0 auto;
}
p {
line-height: 1em;
margin: 0px;
@ -53,6 +62,7 @@ table {
border: 1px solid var(--primary);
border-radius: 5px;
border-spacing: 0px;
width: 100%;
}
table tbody tr:nth-child(even) td {
@ -81,13 +91,18 @@ table td {
table thead td {
background-color: var(--primary);
color: var(--table-background)
color: var(--background);
text-align: center;
}
table tbody td {
background-color: var(--table-background);
}
textarea {
height: calc(5em);
}
#container {
width: 1024px;
margin: 0px auto;
@ -105,8 +120,17 @@ table tbody td {
font-size: 2em;
}
#header > *:nth-child(2) {
#header .title-container {
text-align: center;
}
#header .title {
font-weight: bold;
line-height: 0.75em;
}
#header .page {
font-size: 0.5em;
}
#menu {
@ -135,7 +159,6 @@ table tbody td {
#menu > a[active="true"]:not(:hover) {
background-color: var(--primary-hover);
color: var(--primary);
border-color: transparent;
}
@ -153,11 +176,19 @@ table tbody td {
color: var(--primary);
}
#menu-open {
color: var(--primary);
}
#menu-open:hover {
color: var(--primary-hover);
}
#menu-open, #menu-close {
cursor: pointer;
}
#menu-close, #menu-open {
#menu-open, #menu-close {
min-width: 35px;
text-align: center;
}
@ -171,6 +202,23 @@ table tbody td {
text-align: right
}
#add-item {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
#data-table td:first-child {
width: 100%;
}
#data-table .date {
width: max-content;
text-align: right;
}
.button {
background-color: var(--primary);
@ -200,6 +248,15 @@ table tbody td {
border: 1px solid var(--error-border) !important;
}
.grid-2col {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
.message {
color: var(--message-text) !important;
background-color: var(--message-background) !important;
@ -222,9 +279,10 @@ table tbody td {
}
{% if page %}
{% include "style/" + page.lower().replace(" ", "_") + ".css" %}
{% endif %}
/* config */
#content.page-config input[type="checkbox"] {
justify-self: left;
}
@media (max-width: 1026px) {
@ -254,7 +312,8 @@ table tbody td {
}
.section {
border-width: 0px;
border-left-width: 0px;
border-right-width: 0px;
border-radius: 0px;
}
}

View file

@ -1,20 +0,0 @@
#config-options {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
form input[type="submit"] {
display: block;
margin: 0 auto;
}
form input[type="checkbox"] {
justify-self: left;
}
textarea {
height: 4em;
}

View file

@ -1,40 +0,0 @@
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);
}

View file

@ -1,16 +0,0 @@
#instances table {
width: 100%;
}
#instances .instance {
width: 100%;
}
#instances .date {
width: max-content;
text-align: right;
}
#instances thead td {
text-align: center !important;
}

View file

@ -1,33 +0,0 @@
form input[type="submit"] {
display: block;
margin: 0 auto;
}
#instances table {
width: 100%;
}
#instances .instance {
width: 100%;
}
#instances .software {
text-align: center;
}
#instances .date {
width: max-content;
text-align: right;
}
#instances thead td {
text-align: center !important;
}
#add-instance {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
margin-bottom: var(--spacing);
}

View file

@ -1,18 +0,0 @@
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%;
}
}

View file

@ -188,8 +188,9 @@ def cli_setup(ctx: click.Context) -> None:
@cli.command('run')
@click.option('--dev', '-d', is_flag=True, help='Enable developer mode')
@click.pass_context
def cli_run(ctx: click.Context) -> None:
def cli_run(ctx: click.Context, dev: bool = False) -> None:
'Run the relay'
if ctx.obj.config.domain.endswith('example.com') or not ctx.obj.signer:
@ -216,6 +217,7 @@ def cli_run(ctx: click.Context) -> None:
click.echo(pip_command)
return
ctx.obj['dev'] = dev
ctx.obj.run()
# todo: figure out why the relay doesn't quit properly without this

View file

@ -11,9 +11,10 @@ from json.decoder import JSONDecodeError
from ..misc import Response
if typing.TYPE_CHECKING:
from aiohttp.web import Request
from collections.abc import Callable, Coroutine, Generator
from bsql import Database
from typing import Self
from typing import Any, Self
from ..application import Application
from ..cache import Cache
from ..config import Config

View file

@ -4,6 +4,7 @@ import typing
from aiohttp import web
from argon2.exceptions import VerifyMismatchError
from urllib.parse import urlparse
from .base import View, register_route
@ -13,8 +14,11 @@ from ..misc import ACTOR_FORMATS, Message, Response
if typing.TYPE_CHECKING:
from aiohttp.web import Request
from collections.abc import Coroutine
# pylint: disable=no-self-use
UNAUTH_ROUTES = {
'/',
'/login'
@ -147,22 +151,22 @@ class AdminInstances(View):
async def post(self, request: Request) -> Response:
data = {key: value for key, value in (await request.post()).items()}
data = await request.post()
if not data['actor'] and not data['domain']:
if not data.get('actor') and not data.get('domain'):
return await self.get(request, error = 'Missing actor and/or domain')
if not data['domain']:
if not data.get('domain'):
data['domain'] = urlparse(data['actor']).netloc
if not data['software']:
if not data.get('software'):
nodeinfo = await self.client.fetch_nodeinfo(data['domain'])
data['software'] = nodeinfo.sw_name
if not data['actor'] and data['software'] in ACTOR_FORMATS:
if not data.get('actor') and data['software'] in ACTOR_FORMATS:
data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain'])
if not data['inbox'] and data['actor']:
if not data.get('inbox') and data['actor']:
actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse)
data['inbox'] = actor.shared_inbox
@ -176,7 +180,7 @@ class AdminInstances(View):
class AdminInstancesDelete(View):
async def get(self, request: Request, domain: str) -> Response:
with self.database.session() as conn:
if not (conn.get_inbox(domain)):
if not conn.get_inbox(domain):
return await AdminInstances(request).get(request, message = 'Instance not found')
conn.del_inbox(domain)
@ -193,7 +197,7 @@ class AdminWhitelist(View):
with self.database.session() as conn:
context = {
'domains': tuple(conn.execute('SELECT * FROM whitelist').all())
'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all())
}
if error:
@ -206,6 +210,34 @@ class AdminWhitelist(View):
return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
if not data['domain']:
return await self.get(request, error = 'Missing domain')
with self.database.session(True) as conn:
if conn.get_domain_whitelist(data['domain']):
return await self.get(request, message = "Domain already in whitelist")
conn.put_domain_whitelist(data['domain'])
return await self.get(request, message = "Added/updated domain ban")
@register_route('/admin/whitelist/delete/{domain}')
class AdminWhitlistDelete(View):
async def get(self, request: Request, domain: str) -> Response:
with self.database.session() as conn:
if not conn.get_domain_whitelist(domain):
msg = 'Whitelisted domain not found'
return await AdminWhitelist.run("GET", request, message = msg)
conn.del_domain_whitelist(domain)
return await AdminWhitelist.run("GET", request, message = 'Removed domain from whitelist')
@register_route('/admin/domain_bans')
class AdminDomainBans(View):
async def get(self,
@ -230,13 +262,12 @@ class AdminDomainBans(View):
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'])):
if conn.get_domain_ban(data['domain']):
conn.update_domain_ban(
data['domain'],
data.get('reason'),
@ -257,7 +288,7 @@ class AdminDomainBans(View):
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)):
if not conn.get_domain_ban(domain):
return await AdminDomainBans.run("GET", request, message = 'Domain ban not found')
conn.del_domain_ban(domain)
@ -267,11 +298,115 @@ class AdminDomainBansDelete(View):
@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)
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 software_bans ORDER BY name ASC').all())
}
if error:
context['error'] = error
if message:
context['message'] = message
data = self.template.render('page/admin-software_bans.haml', self, **context)
return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
if not data['name']:
return await self.get(request, error = 'Missing name')
with self.database.session(True) as conn:
if conn.get_software_ban(data['name']):
conn.update_software_ban(
data['name'],
data.get('reason'),
data.get('note')
)
else:
conn.put_software_ban(
data['name'],
data.get('reason'),
data.get('note')
)
return await self.get(request, message = "Added/updated software ban")
@register_route('/admin/software_bans/delete/{name}')
class AdminSoftwareBansDelete(View):
async def get(self, request: Request, name: str) -> Response:
with self.database.session() as conn:
if not conn.get_software_ban(name):
return await AdminSoftwareBans.run("GET", request, message = 'Software ban not found')
conn.del_software_ban(name)
return await AdminSoftwareBans.run("GET", request, message = 'Unbanned software')
@register_route('/admin/users')
class AdminUsers(View):
async def get(self,
request: Request,
error: str | None = None,
message: str | None = None) -> Response:
with self.database.session() as conn:
context = {
'users': tuple(conn.execute('SELECT * FROM users').all())
}
if error:
context['error'] = error
if message:
context['message'] = message
data = self.template.render('page/admin-users.haml', self, **context)
return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
required_fields = {'username', 'password', 'password2'}
if not all(data.get(field) for field in required_fields):
return await self.get(request, error = 'Missing username and/or password')
if data['password'] != data['password2']:
return await self.get(request, error = 'Passwords do not match')
with self.database.session(True) as conn:
if conn.get_user(data['username']):
return await self.get(request, message = "User already exists")
conn.put_user(data['username'], data['password'], data['handle'])
return await self.get(request, message = "Added user")
@register_route('/admin/users/delete/{name}')
class AdminUsersDelete(View):
async def get(self, request: Request, name: str) -> Response:
with self.database.session() as conn:
if not conn.get_user(name):
return await AdminUsers.run("GET", request, message = 'User not found')
conn.del_user(name)
return await AdminUsers.run("GET", request, message = 'User deleted')
@register_route('/admin/config')
class AdminConfig(View):
async def get(self, request: Request, message: str | None = None) -> Response:
@ -308,5 +443,5 @@ class AdminConfig(View):
@register_route('/style.css')
class StyleCss(View):
async def get(self, request: Request) -> Response:
data = self.template.render('style.css', self, page = request.query.getone('page', ""))
data = self.template.render('style.css', self)
return Response.new(data, ctype = 'css')

View file

@ -34,8 +34,9 @@ dev = file: dev-requirements.txt
[options.package_data]
relay =
data/statements.sql
data/swagger.yaml
data/*
frontend/*
frontend/page/*
[options.entry_points]
console_scripts =