Compare commits

..

No commits in common. "a0ee22406bdfe9a604ac74d0f6732e03f30437e2" and "2a866eaaaa2413f6f93e2a6851d5f4e65f189b50" have entirely different histories.

27 changed files with 291 additions and 443 deletions

5
MANIFEST.in Normal file
View file

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

56
relay.spec Normal file
View file

@ -0,0 +1,56 @@
# -*- 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,8 +1,5 @@
import multiprocessing
from relay.manage import main
if __name__ == '__main__':
multiprocessing.freeze_support()
main()

View file

@ -26,10 +26,9 @@ 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, Response
from .misc import Message
# pylint: disable=unsubscriptable-object
@ -37,12 +36,11 @@ if typing.TYPE_CHECKING:
class Application(web.Application):
DEFAULT: Application = None
def __init__(self, cfgpath: str | None, dev: bool = False):
def __init__(self, cfgpath: str | None):
web.Application.__init__(self,
middlewares = [
handle_api_path,
handle_frontend_path,
handle_response_headers
handle_frontend_path
]
)
@ -52,7 +50,6 @@ 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)
@ -258,18 +255,25 @@ 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'
# if not request.app['dev'] and request.path.endswith(('.css', '.js')):
# resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
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
)
)
# else:
# resp.headers['Cache-Control'] = 'no-store'
return resp
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')
)
async def handle_cleanup(app: Application) -> None:

View file

@ -52,11 +52,7 @@ 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.path = Config.get_config_dir()
self.listen = None
self.port = None

View file

@ -1,14 +1,10 @@
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
@ -50,33 +46,25 @@ 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():
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)
cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec']
subprocess.run(cmd, check = False)
@cli.command('run')
@ -106,7 +94,7 @@ def cli_run():
class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py']
cmd = [sys.executable, '-m', 'relay', 'run', '-d']
cmd = [sys.executable, '-m', 'relay', 'run']
def __init__(self):
@ -157,6 +145,7 @@ 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")
%link(rel="stylesheet" type="text/css" href="/style.css?page={{page}}")
-block head
%body
@ -27,7 +27,6 @@
{{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")}}
@ -36,13 +35,8 @@
#container
#header.section
%span#menu-open << &#8286;
%span.title-container
%a.title(href="/") -> =config.name
-if view.request.path not in ["/", "/login"]
.page -> =page
%span#menu-open.button << &#8286;
%a(href="https://{{domain}}/") -> =config.name
.empty
-if error
@ -51,7 +45,7 @@
-if message
.message.section -> =message
#content(class="page-{{page.lower().replace(' ', '_')}}")
#content
-block content
#footer.section

View file

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

View file

@ -3,23 +3,20 @@
-block content
%details.section
%summary << Add Instance
%form(action="/admin/instances" method="POST")
#add-item
%form(target="/admin/instances", method="POST")
#add-instance
%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")
#data-table.section
#instances.section
%table
%thead
%tr

View file

@ -1,48 +1,5 @@
-extends "base.haml"
-set page="Software Bans"
-block content
%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;
.section
UvU

View file

@ -1,44 +0,0 @@
-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,31 +1,5 @@
-extends "base.haml"
-set page="Whitelist"
-block content
%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;
.section
UvU

View file

@ -19,7 +19,7 @@
Note: The whitelist is enabled on this instance. Ask the admin to add your instance
before joining.
#data-table.section
#instances.section
%table
%thead
%tr
@ -29,8 +29,5 @@
%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,12 +1,9 @@
-extends "base.haml"
-set page="Login"
-block content
%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")
%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")

View file

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

View file

@ -0,0 +1,20 @@
#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

@ -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);
}

View file

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

View file

@ -0,0 +1,33 @@
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

@ -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%;
}
}

View file

View file

View file

@ -188,9 +188,8 @@ 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, dev: bool = False) -> None:
def cli_run(ctx: click.Context) -> None:
'Run the relay'
if ctx.obj.config.domain.endswith('example.com') or not ctx.obj.signer:
@ -217,7 +216,6 @@ def cli_run(ctx: click.Context, dev: bool = False) -> 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,10 +11,9 @@ 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 Any, Self
from typing import Self
from ..application import Application
from ..cache import Cache
from ..config import Config

View file

@ -4,7 +4,6 @@ import typing
from aiohttp import web
from argon2.exceptions import VerifyMismatchError
from urllib.parse import urlparse
from .base import View, register_route
@ -14,11 +13,8 @@ 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'
@ -151,22 +147,22 @@ class AdminInstances(View):
async def post(self, request: Request) -> Response:
data = await request.post()
data = {key: value for key, value in (await request.post()).items()}
if not data.get('actor') and not data.get('domain'):
if not data['actor'] and not data['domain']:
return await self.get(request, error = 'Missing actor and/or domain')
if not data.get('domain'):
if not data['domain']:
data['domain'] = urlparse(data['actor']).netloc
if not data.get('software'):
if not data['software']:
nodeinfo = await self.client.fetch_nodeinfo(data['domain'])
data['software'] = nodeinfo.sw_name
if not data.get('actor') and data['software'] in ACTOR_FORMATS:
if not data['actor'] and data['software'] in ACTOR_FORMATS:
data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain'])
if not data.get('inbox') and data['actor']:
if not data['inbox'] and data['actor']:
actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse)
data['inbox'] = actor.shared_inbox
@ -180,7 +176,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)
@ -197,7 +193,7 @@ class AdminWhitelist(View):
with self.database.session() as conn:
context = {
'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all())
'domains': tuple(conn.execute('SELECT * FROM whitelist').all())
}
if error:
@ -210,34 +206,6 @@ 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,
@ -262,12 +230,13 @@ 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 conn.get_domain_ban(data['domain']):
if (ban := conn.get_domain_ban(data['domain'])):
conn.update_domain_ban(
data['domain'],
data.get('reason'),
@ -288,7 +257,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)
@ -298,115 +267,11 @@ class AdminDomainBansDelete(View):
@register_route('/admin/software_bans')
class AdminSoftwareBans(View):
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)
async def get(self, request: Request) -> Response:
data = self.template.render('page/admin-software_bans.haml', self)
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:
@ -443,5 +308,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)
data = self.template.render('style.css', self, page = request.query.getone('page', ""))
return Response.new(data, ctype = 'css')

View file

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