mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-22 14:38:00 +00:00
minor frontend tweaks and use javascript for managing domain bans
This commit is contained in:
parent
49917fcc4e
commit
10ba039938
|
@ -68,6 +68,8 @@ class Application(web.Application):
|
||||||
for path, view in VIEWS:
|
for path, view in VIEWS:
|
||||||
self.router.add_view(path, view)
|
self.router.add_view(path, view)
|
||||||
|
|
||||||
|
self.add_routes([web.static('/static', get_resource('frontend/static'))])
|
||||||
|
|
||||||
setup_swagger(
|
setup_swagger(
|
||||||
self,
|
self,
|
||||||
ui_version = 3,
|
ui_version = 3,
|
||||||
|
@ -124,6 +126,40 @@ class Application(web.Application):
|
||||||
return timedelta(seconds=uptime.seconds)
|
return timedelta(seconds=uptime.seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def get_csp(self, request: Request) -> str:
|
||||||
|
data = [
|
||||||
|
"default-src 'none'",
|
||||||
|
f"script-src 'nonce-{request['hash']}'",
|
||||||
|
f"style-src 'nonce-{request['hash']}'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"connect-src 'self'",
|
||||||
|
"img-src 'self'",
|
||||||
|
"object-src 'none'",
|
||||||
|
"frame-ancestors 'none'"
|
||||||
|
]
|
||||||
|
|
||||||
|
return '; '.join(data) + ';'
|
||||||
|
|
||||||
|
# data = {
|
||||||
|
# 'base-uri': '\'none\'',
|
||||||
|
# 'default-src': '\'none\'',
|
||||||
|
# 'frame-ancestors': '\'none\'',
|
||||||
|
# 'font-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'img-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'style-src': f'\'self\' https://{self.config.domain} \'nonce-randomstringhere\'',
|
||||||
|
# 'media-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'frame-src': f'\'self\' https:',
|
||||||
|
# 'manifest-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'form-action': f'\'self\'',
|
||||||
|
# 'child-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'worker-src': f'\'self\' https://{self.config.domain}',
|
||||||
|
# 'connect-src': f'\'self\' https://{self.config.domain} wss://{self.config.domain}',
|
||||||
|
# 'script-src': f'\'self\' https://{self.config.domain}'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# return '; '.join(f'{key} {value}' for key, value in data.items()) + ';'
|
||||||
|
|
||||||
|
|
||||||
def push_message(self, inbox: str, message: Message, instance: Row) -> None:
|
def push_message(self, inbox: str, message: Message, instance: Row) -> None:
|
||||||
self['push_queue'].put((inbox, message, instance))
|
self['push_queue'].put((inbox, message, instance))
|
||||||
|
|
||||||
|
@ -269,6 +305,9 @@ async def handle_response_headers(request: web.Request, handler: Callable) -> Re
|
||||||
resp = await handler(request)
|
resp = await handler(request)
|
||||||
resp.headers['Server'] = 'ActivityRelay'
|
resp.headers['Server'] = 'ActivityRelay'
|
||||||
|
|
||||||
|
# if resp.content_type == 'text/html':
|
||||||
|
# resp.headers['Content-Security-Policy'] = Application.DEFAULT.get_csp(request)
|
||||||
|
|
||||||
if not request.app['dev'] and request.path.endswith(('.css', '.js')):
|
if not request.app['dev'] and request.path.endswith(('.css', '.js')):
|
||||||
# cache for 2 weeks
|
# cache for 2 weeks
|
||||||
resp.headers['Cache-Control'] = 'public,max-age=1209600,immutable'
|
resp.headers['Cache-Control'] = 'public,max-age=1209600,immutable'
|
||||||
|
|
|
@ -11,8 +11,10 @@
|
||||||
%title << {{config.name}}: {{page}}
|
%title << {{config.name}}: {{page}}
|
||||||
%meta(charset="UTF-8")
|
%meta(charset="UTF-8")
|
||||||
%meta(name="viewport" content="width=device-width, initial-scale=1")
|
%meta(name="viewport" content="width=device-width, initial-scale=1")
|
||||||
%link(rel="stylesheet" type="text/css" href="/theme/{{config.theme}}.css")
|
%link(rel="stylesheet" type="text/css" href="/theme/{{config.theme}}.css" nonce="{{view.request['hash']}}")
|
||||||
%link(rel="stylesheet" type="text/css" href="/style.css")
|
%link(rel="stylesheet" type="text/css" href="/static/style.css" nonce="{{view.request['hash']}}")
|
||||||
|
%script(type="application/javascript" src="/static/menu.js" nonce="{{view.request['hash']}}", defer)
|
||||||
|
%script(type="application/javascript" src="/static/api.js" nonce="{{view.request['hash']}}", defer)
|
||||||
-block head
|
-block head
|
||||||
|
|
||||||
%body
|
%body
|
||||||
|
@ -38,19 +40,18 @@
|
||||||
#container
|
#container
|
||||||
#header.section
|
#header.section
|
||||||
%span#menu-open << ⁞
|
%span#menu-open << ⁞
|
||||||
%span.title-container
|
%a.title(href="/") -> =config.name
|
||||||
%a.title(href="/") -> =config.name
|
|
||||||
|
|
||||||
-if view.request.path not in ["/", "/login"]
|
|
||||||
.page -> =page
|
|
||||||
|
|
||||||
.empty
|
.empty
|
||||||
|
|
||||||
-if error
|
-if error
|
||||||
.error.section -> =error
|
%fieldset.error.section
|
||||||
|
%legend << Error
|
||||||
|
=error
|
||||||
|
|
||||||
-if message
|
-if message
|
||||||
.message.section -> =message
|
%fieldset.message.section
|
||||||
|
%legend << Message
|
||||||
|
=message
|
||||||
|
|
||||||
#content(class="page-{{page.lower().replace(' ', '_')}}")
|
#content(class="page-{{page.lower().replace(' ', '_')}}")
|
||||||
-block content
|
-block content
|
||||||
|
@ -69,26 +70,3 @@
|
||||||
.version
|
.version
|
||||||
%a(href="https://git.pleroma.social/pleroma/relay")
|
%a(href="https://git.pleroma.social/pleroma/relay")
|
||||||
ActivityRelay/{{version}}
|
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) => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.attributes.visible.nodeValue = "false";
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,24 +2,26 @@
|
||||||
-set page="Config"
|
-set page="Config"
|
||||||
-import "functions.haml" as func
|
-import "functions.haml" as func
|
||||||
-block content
|
-block content
|
||||||
%form.section(action="/admin/config" method="POST")
|
%fieldset.section
|
||||||
.grid-2col
|
%legend << Config
|
||||||
%label(for="name") << Name
|
%form(action="/admin/config" method="POST")
|
||||||
%input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}")
|
.grid-2col
|
||||||
|
%label(for="name") << Name
|
||||||
|
%input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}")
|
||||||
|
|
||||||
%label(for="description") << Description
|
%label(for="description") << Description
|
||||||
%textarea(id="description" name="note" value="{{config.note or ''}}") << {{config.note}}
|
%textarea(id="description" name="note" value="{{config.note or ''}}") << {{config.note}}
|
||||||
|
|
||||||
%label(for="theme") << Color Theme
|
%label(for="theme") << Color Theme
|
||||||
=func.new_select("theme", config.theme, themes)
|
=func.new_select("theme", config.theme, themes)
|
||||||
|
|
||||||
%label(for="log-level") << Log Level
|
%label(for="log-level") << Log Level
|
||||||
=func.new_select("log-level", config.log_level.name, levels)
|
=func.new_select("log-level", config.log_level.name, levels)
|
||||||
|
|
||||||
%label(for="whitelist-enabled") << Whitelist
|
%label(for="whitelist-enabled") << Whitelist
|
||||||
=func.new_checkbox("whitelist-enabled", config.whitelist_enabled)
|
=func.new_checkbox("whitelist-enabled", config.whitelist_enabled)
|
||||||
|
|
||||||
%label(for="approval-required") << Approval Required
|
%label(for="approval-required") << Approval Required
|
||||||
=func.new_checkbox("approval-required", config.approval_required)
|
=func.new_checkbox("approval-required", config.approval_required)
|
||||||
|
|
||||||
%input(type="submit" value="Save")
|
%input(type="submit" value="Save")
|
||||||
|
|
|
@ -1,48 +1,53 @@
|
||||||
-extends "base.haml"
|
-extends "base.haml"
|
||||||
-set page="Domain Bans"
|
-set page="Domain Bans"
|
||||||
|
|
||||||
|
-block head
|
||||||
|
%script(type="application/javascript" src="/static/domain_ban.js" nonce="{{view.request['hash']}}", defer)
|
||||||
|
|
||||||
-block content
|
-block content
|
||||||
%details.section
|
%details.section
|
||||||
%summary << Ban Domain
|
%summary << Ban Domain
|
||||||
%form(action="/admin/domain_bans" method="POST")
|
#add-item
|
||||||
#add-item
|
%label(for="new-domain") << Domain
|
||||||
%label(for="domain") << Domain
|
%input(type="domain" id="new-domain" name="domain" placeholder="Domain")
|
||||||
%input(type="domain" id="domain" name="domain" placeholder="Domain")
|
|
||||||
|
|
||||||
%label(for="reason") << Ban Reason
|
%label(for="new-reason") << Ban Reason
|
||||||
%textarea(id="reason" name="reason") << {{""}}
|
%textarea(id="new-reason" name="new") << {{""}}
|
||||||
|
|
||||||
%label(for="note") << Admin Note
|
%label(for="new-note") << Admin Note
|
||||||
%textarea(id="note" name="note") << {{""}}
|
%textarea(id="new-note" name="note") << {{""}}
|
||||||
|
|
||||||
%input(type="submit" value="Ban Domain")
|
%input(type="button" value="Ban Domain" onclick="ban();")
|
||||||
|
|
||||||
.data-table.section
|
%fieldset.section
|
||||||
%table
|
%legend << Domain Bans
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%td.domain << Instance
|
|
||||||
%td << Date
|
|
||||||
%td.remove
|
|
||||||
|
|
||||||
%tbody
|
.data-table
|
||||||
-for ban in bans
|
%table#table
|
||||||
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%td.domain
|
%td.domain << Domain
|
||||||
%details
|
%td << Date
|
||||||
%summary -> =ban.domain
|
%td.remove
|
||||||
%form(action="/admin/domain_bans" method="POST")
|
|
||||||
|
%tbody
|
||||||
|
-for ban in bans
|
||||||
|
%tr(id="{{ban.domain}}")
|
||||||
|
%td.domain
|
||||||
|
%details
|
||||||
|
%summary -> =ban.domain
|
||||||
|
|
||||||
.grid-2col
|
.grid-2col
|
||||||
.reason << Reason
|
.reason << Reason
|
||||||
%textarea.reason(id="reason" name="reason") << {{ban.reason or ""}}
|
%textarea.reason(id="{{ban.domain}}-reason" name="reason") << {{ban.reason or ""}}
|
||||||
|
|
||||||
.note << Note
|
.note << Note
|
||||||
%textarea.note(id="note" name="note") << {{ban.note or ""}}
|
%textarea.note(id="{{ban.domain}}-note" name="note") << {{ban.note or ""}}
|
||||||
|
|
||||||
%input(type="hidden" name="domain" value="{{ban.domain}}")
|
%input(type="button" value="Update" onclick="update_ban('{{ban.domain}}')")
|
||||||
%input(type="submit" value="Update")
|
|
||||||
|
|
||||||
%td.date
|
%td.date
|
||||||
=ban.created.strftime("%Y-%m-%d")
|
=ban.created.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
%td.remove
|
%td.remove
|
||||||
%a(href="/admin/domain_bans/delete/{{ban.domain}}" title="Unban domain") << ✖
|
%a(href="#", onclick="unban('{{ban.domain}}')" title="Unban domain") << ✖
|
||||||
|
|
|
@ -20,56 +20,59 @@
|
||||||
%input(type="submit" value="Add Instance")
|
%input(type="submit" value="Add Instance")
|
||||||
|
|
||||||
-if requests
|
-if requests
|
||||||
.data-table.section
|
%fieldset.section
|
||||||
.title << Requests
|
%legend << Follow Requests
|
||||||
|
.data-table
|
||||||
|
%table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%td.instance << Instance
|
||||||
|
%td.software << Software
|
||||||
|
%td.date << Joined
|
||||||
|
%td.approve
|
||||||
|
%td.deny
|
||||||
|
|
||||||
|
%tbody
|
||||||
|
-for request in requests
|
||||||
|
%tr
|
||||||
|
%td.instance
|
||||||
|
%a(href="https://{{request.domain}}" target="_new") -> =request.domain
|
||||||
|
|
||||||
|
%td.software
|
||||||
|
=request.software or "n/a"
|
||||||
|
|
||||||
|
%td.date
|
||||||
|
=request.created.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
%td.approve
|
||||||
|
%a(href="/admin/instances/approve/{{request.domain}}" title="Approve Request") << ✓
|
||||||
|
|
||||||
|
%td.deny
|
||||||
|
%a(href="/admin/instances/deny/{{request.domain}}" title="Deny Request") << ✖
|
||||||
|
|
||||||
|
%fieldset.section
|
||||||
|
%legend << Instances
|
||||||
|
|
||||||
|
.data-table
|
||||||
%table
|
%table
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%td.instance << Instance
|
%td.instance << Instance
|
||||||
%td.software << Software
|
%td.software << Software
|
||||||
%td.date << Joined
|
%td.date << Joined
|
||||||
%td.approve
|
%td.remove
|
||||||
%td.deny
|
|
||||||
|
|
||||||
%tbody
|
%tbody
|
||||||
-for request in requests
|
-for instance in instances
|
||||||
%tr
|
%tr
|
||||||
%td.instance
|
%td.instance
|
||||||
%a(href="https://{{request.domain}}" target="_new") -> =request.domain
|
%a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain
|
||||||
|
|
||||||
%td.software
|
%td.software
|
||||||
=request.software or "n/a"
|
=instance.software or "n/a"
|
||||||
|
|
||||||
%td.date
|
%td.date
|
||||||
=request.created.strftime("%Y-%m-%d")
|
=instance.created.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
%td.approve
|
%td.remove
|
||||||
%a(href="/admin/instances/approve/{{request.domain}}" title="Approve Request") << ✓
|
%a(href="/admin/instances/delete/{{instance.domain}}" title="Remove Instance") << ✖
|
||||||
|
|
||||||
%td.deny
|
|
||||||
%a(href="/admin/instances/deny/{{request.domain}}" title="Deny Request") << ✖
|
|
||||||
|
|
||||||
.data-table.section
|
|
||||||
.title << Instances
|
|
||||||
%table
|
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%td.instance << Instance
|
|
||||||
%td.software << Software
|
|
||||||
%td.date << Joined
|
|
||||||
%td.remove
|
|
||||||
|
|
||||||
%tbody
|
|
||||||
-for instance in instances
|
|
||||||
%tr
|
|
||||||
%td.instance
|
|
||||||
%a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain
|
|
||||||
|
|
||||||
%td.software
|
|
||||||
=instance.software or "n/a"
|
|
||||||
|
|
||||||
%td.date
|
|
||||||
=instance.created.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
%td.remove
|
|
||||||
%a(href="/admin/instances/delete/{{instance.domain}}" title="Remove Instance") << ✖
|
|
||||||
|
|
|
@ -16,33 +16,36 @@
|
||||||
|
|
||||||
%input(type="submit" value="Ban Software")
|
%input(type="submit" value="Ban Software")
|
||||||
|
|
||||||
.data-table.section
|
%fieldset.section
|
||||||
%table
|
%legend << Software Bans
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%td.name << Instance
|
|
||||||
%td << Date
|
|
||||||
%td.remove
|
|
||||||
|
|
||||||
%tbody
|
.data-table
|
||||||
-for ban in bans
|
%table
|
||||||
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%td.name
|
%td.name << Name
|
||||||
%details
|
%td << Date
|
||||||
%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
|
%td.remove
|
||||||
%a(href="/admin/software_bans/delete/{{ban.name}}" title="Unban software") << ✖
|
|
||||||
|
%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") << ✖
|
||||||
|
|
|
@ -19,26 +19,29 @@
|
||||||
|
|
||||||
%input(type="submit" value="Add User")
|
%input(type="submit" value="Add User")
|
||||||
|
|
||||||
.data-table.section
|
%fieldset.section
|
||||||
%table
|
%legend << Users
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%td.username << Username
|
|
||||||
%td.handle << Handle
|
|
||||||
%td.date << Joined
|
|
||||||
%td.remove
|
|
||||||
|
|
||||||
%tbody
|
.data-table
|
||||||
-for user in users
|
%table
|
||||||
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%td.username
|
%td.username << Username
|
||||||
=user.username
|
%td.handle << Handle
|
||||||
|
%td.date << Joined
|
||||||
%td.handle
|
|
||||||
=user.handle or "n/a"
|
|
||||||
|
|
||||||
%td.date
|
|
||||||
=user.created.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
%td.remove
|
%td.remove
|
||||||
%a(href="/admin/users/delete/{{user.username}}" title="Remove User") << ✖
|
|
||||||
|
%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") << ✖
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
|
|
||||||
%input(type="submit" value="Add Domain")
|
%input(type="submit" value="Add Domain")
|
||||||
|
|
||||||
.data-table.section
|
%fieldset.data-table.section
|
||||||
|
%legend << Whitelist
|
||||||
|
|
||||||
%table
|
%table
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
|
|
|
@ -14,27 +14,34 @@
|
||||||
%a(href="https://{{domain}}/actor") << https://{{domain}}/actor</a>
|
%a(href="https://{{domain}}/actor") << https://{{domain}}/actor</a>
|
||||||
|
|
||||||
-if config.approval_required
|
-if config.approval_required
|
||||||
%p.section.message
|
%fieldset.section.message
|
||||||
|
%legend << Require Approval
|
||||||
|
|
||||||
Follow requests require approval. You will need to wait for an admin to accept or deny
|
Follow requests require approval. You will need to wait for an admin to accept or deny
|
||||||
your request.
|
your request.
|
||||||
|
|
||||||
-elif config.whitelist_enabled
|
-elif config.whitelist_enabled
|
||||||
%p.section.message
|
%fieldset.section.message
|
||||||
|
%legend << Whitelist Enabled
|
||||||
|
|
||||||
The whitelist is enabled on this instance. Ask the admin to add your instance before
|
The whitelist is enabled on this instance. Ask the admin to add your instance before
|
||||||
joining.
|
joining.
|
||||||
|
|
||||||
.data-table.section
|
%fieldset.section
|
||||||
%table
|
%legend << Instances
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%td.instance << Instance
|
|
||||||
%td.date << Joined
|
|
||||||
|
|
||||||
%tbody
|
.data-table
|
||||||
-for instance in instances
|
%table
|
||||||
|
%thead
|
||||||
%tr
|
%tr
|
||||||
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new")
|
%td.instance << Instance
|
||||||
=instance.domain
|
%td.date << Joined
|
||||||
|
|
||||||
%td.date
|
%tbody
|
||||||
=instance.created.strftime("%Y-%m-%d")
|
-for instance in instances
|
||||||
|
%tr
|
||||||
|
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new")
|
||||||
|
=instance.domain
|
||||||
|
|
||||||
|
%td.date
|
||||||
|
=instance.created.strftime("%Y-%m-%d")
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
-extends "base.haml"
|
-extends "base.haml"
|
||||||
-set page="Login"
|
-set page="Login"
|
||||||
-block content
|
-block content
|
||||||
%form.section(action="/login" method="POST")
|
%fieldset.section
|
||||||
.grid-2col
|
%legend << Login
|
||||||
%label(for="username") << Username
|
|
||||||
%input(id="username" name="username" placeholder="Username" value="{{username or ''}}")
|
|
||||||
|
|
||||||
%label(for="password") << Password
|
%form(action="/login" method="POST")
|
||||||
%input(id="password" name="password" placeholder="Password" type="password")
|
.grid-2col
|
||||||
|
%label(for="username") << Username
|
||||||
|
%input(id="username" name="username" placeholder="Username" value="{{username or ''}}")
|
||||||
|
|
||||||
%input(type="submit" value="Login")
|
%label(for="password") << Password
|
||||||
|
%input(id="password" name="password" placeholder="Password" type="password")
|
||||||
|
|
||||||
|
%input(type="submit" value="Login")
|
||||||
|
|
90
relay/frontend/static/api.js
Normal file
90
relay/frontend/static/api.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
function get_cookie(name) {
|
||||||
|
const regex = new RegExp(`(^| )` + name + `=([^;]+)`);
|
||||||
|
const match = document.cookie.match(regex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function get_date_string(date) {
|
||||||
|
var year = date.getFullYear().toString();
|
||||||
|
var month = date.getMonth().toString();
|
||||||
|
var day = date.getDay().toString();
|
||||||
|
|
||||||
|
if (month.length === 1) {
|
||||||
|
month = "0" + month;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day.length === 1) {
|
||||||
|
day = "0" + day
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
constructor() {
|
||||||
|
this.token = get_cookie("user-token");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async request(method, path, body = null) {
|
||||||
|
var headers = {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body !== null) {
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.token !== null) {
|
||||||
|
headers["Authorization"] = "Bearer " + this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/" + path, {
|
||||||
|
method: method,
|
||||||
|
mode: "cors",
|
||||||
|
cache: "no-store",
|
||||||
|
redirect: "follow",
|
||||||
|
body: body,
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await response.json();
|
||||||
|
|
||||||
|
if (Object.hasOwn(message, "error")) {
|
||||||
|
throw new Error(message.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.hasOwn(message, "created")) {
|
||||||
|
message.created = new Date(message.created);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ban(domain, reason, note) {
|
||||||
|
const params = {
|
||||||
|
"domain": domain,
|
||||||
|
"reason": reason,
|
||||||
|
"note": note
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.request("POST", "v1/domain_ban", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async unban(domain) {
|
||||||
|
const params = {"domain": domain}
|
||||||
|
return await this.request("DELETE", "v1/domain_ban", params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
client = new Client();
|
85
relay/frontend/static/domain_ban.js
Normal file
85
relay/frontend/static/domain_ban.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
function create_ban_object(domain, reason, note) {
|
||||||
|
var text = '<details>\n';
|
||||||
|
text += `<summary>${domain}</summary>\n`;
|
||||||
|
text += '<div class="grid-2col">\n';
|
||||||
|
text += `<label for="${domain}-reason" class="reason">Reason</label>\n`;
|
||||||
|
text += `<textarea id="${domain}-reason" class="reason" name="reason">${reason}</textarea>\n`;
|
||||||
|
text += `<label for="${domain}-note" class="note">Note</label>\n`;
|
||||||
|
text += `<textarea id="${domain}-note" class="note" name="note">${note}</textarea>\n`;
|
||||||
|
text += `<input type="button" value="Update" onclick="update_ban(\"${domain}\"")">`;
|
||||||
|
text += '</details>';
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function ban() {
|
||||||
|
var table = document.getElementById("table");
|
||||||
|
var row = table.insertRow(-1);
|
||||||
|
|
||||||
|
var elems = {
|
||||||
|
domain: document.getElementById("new-domain"),
|
||||||
|
reason: document.getElementById("new-reason"),
|
||||||
|
note: document.getElementById("new-note")
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = {
|
||||||
|
domain: elems.domain.value.trim(),
|
||||||
|
reason: elems.reason.value,
|
||||||
|
note: elems.note.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.domain === "") {
|
||||||
|
alert("Domain is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ban = await client.ban(values.domain, values.reason, values.note);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alert(err);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
row.id = ban.domain;
|
||||||
|
var new_domain = row.insertCell(0);
|
||||||
|
var new_date = row.insertCell(1);
|
||||||
|
var new_remove = row.insertCell(2);
|
||||||
|
|
||||||
|
new_domain.className = "domain";
|
||||||
|
new_date.className = "date";
|
||||||
|
new_remove.className = "remove";
|
||||||
|
|
||||||
|
new_domain.innerHTML = create_ban_object(ban.domain, ban.reason, ban.note);
|
||||||
|
new_date.innerHTML = get_date_string(ban.created);
|
||||||
|
new_remove.innerHTML = `<a href="#" onclick="unban('${ban.domain}')" title="Unban domain">✖</a>`;
|
||||||
|
|
||||||
|
elems.domain.value = null;
|
||||||
|
elems.reason.value = null;
|
||||||
|
elems.note.value = null;
|
||||||
|
|
||||||
|
document.querySelectorAll("details.section").forEach((elem) => {
|
||||||
|
elem.open = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function update_ban(domain) {
|
||||||
|
var row = document.getElementById(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function unban(domain) {
|
||||||
|
console.log(domain);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.unban(domain);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alert(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(domain).remove();
|
||||||
|
}
|
21
relay/frontend/static/menu.js
Normal file
21
relay/frontend/static/menu.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.attributes.visible.nodeValue = "false";
|
||||||
|
});
|
|
@ -23,17 +23,27 @@ details summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset > *:nth-child(2) {
|
||||||
|
margin-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
form input[type="submit"] {
|
form input[type="submit"] {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
background-color: var(--section-background);
|
background-color: var(--table-background);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
|
@ -274,54 +274,6 @@ class DomainBan(View):
|
||||||
return Response.new(bans, ctype = 'json')
|
return Response.new(bans, ctype = 'json')
|
||||||
|
|
||||||
|
|
||||||
async def post(self, request: Request) -> Response:
|
|
||||||
data = await self.get_api_data(['domain'], ['note', 'reason'])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
|
||||||
return data
|
|
||||||
|
|
||||||
with self.database.session() as conn:
|
|
||||||
if conn.get_domain_ban(data['domain']):
|
|
||||||
return Response.new_error(400, 'Domain already banned', 'json')
|
|
||||||
|
|
||||||
ban = conn.put_domain_ban(**data)
|
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
|
||||||
|
|
||||||
|
|
||||||
async def patch(self, request: Request) -> Response:
|
|
||||||
with self.database.session() as conn:
|
|
||||||
data = await self.get_api_data(['domain'], ['note', 'reason'])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
|
||||||
return data
|
|
||||||
|
|
||||||
if not conn.get_domain_ban(data['domain']):
|
|
||||||
return Response.new_error(404, 'Domain not banned', 'json')
|
|
||||||
|
|
||||||
if not any([data.get('note'), data.get('reason')]):
|
|
||||||
return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
|
|
||||||
|
|
||||||
ban = conn.update_domain_ban(data['domain'], **data)
|
|
||||||
|
|
||||||
return Response.new(ban, ctype = 'json')
|
|
||||||
|
|
||||||
|
|
||||||
async def delete(self, request: Request) -> Response:
|
|
||||||
with self.database.session() as conn:
|
|
||||||
data = await self.get_api_data(['domain'], [])
|
|
||||||
|
|
||||||
if isinstance(data, Response):
|
|
||||||
return data
|
|
||||||
|
|
||||||
if not conn.get_domain_ban(data['domain']):
|
|
||||||
return Response.new_error(404, 'Domain not banned', 'json')
|
|
||||||
|
|
||||||
conn.del_domain_ban(data['domain'])
|
|
||||||
|
|
||||||
return Response.new({'message': 'Unbanned domain'}, ctype = 'json')
|
|
||||||
|
|
||||||
|
|
||||||
@register_route('/api/v1/software_ban')
|
@register_route('/api/v1/software_ban')
|
||||||
class SoftwareBan(View):
|
class SoftwareBan(View):
|
||||||
async def get(self, request: Request) -> Response:
|
async def get(self, request: Request) -> Response:
|
||||||
|
|
|
@ -2,9 +2,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from Crypto.Random import get_random_bytes
|
||||||
from aiohttp.abc import AbstractView
|
from aiohttp.abc import AbstractView
|
||||||
from aiohttp.hdrs import METH_ALL as METHODS
|
from aiohttp.hdrs import METH_ALL as METHODS
|
||||||
from aiohttp.web import HTTPMethodNotAllowed
|
from aiohttp.web import HTTPMethodNotAllowed
|
||||||
|
from base64 import b64encode
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
|
@ -56,6 +58,7 @@ class View(AbstractView):
|
||||||
|
|
||||||
|
|
||||||
async def _run_handler(self, handler: Callable[..., Any], **kwargs: Any) -> Response:
|
async def _run_handler(self, handler: Callable[..., Any], **kwargs: Any) -> Response:
|
||||||
|
self.request['hash'] = b64encode(get_random_bytes(16)).decode('ascii')
|
||||||
return await handler(self.request, **self.request.match_info, **kwargs)
|
return await handler(self.request, **self.request.match_info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,21 +26,31 @@ UNAUTH_ROUTES = {
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def handle_frontend_path(request: web.Request, handler: Callable) -> Response:
|
async def handle_frontend_path(request: web.Request, handler: Callable) -> Response:
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
if request.path in UNAUTH_ROUTES or request.path.startswith('/admin'):
|
if request.path in UNAUTH_ROUTES or request.path.startswith('/admin'):
|
||||||
request['token'] = request.cookies.get('user-token')
|
request['token'] = request.cookies.get('user-token')
|
||||||
request['user'] = None
|
request['user'] = None
|
||||||
|
|
||||||
if request['token']:
|
if request['token']:
|
||||||
with get_app().database.session(False) as conn:
|
with app.database.session(False) as conn:
|
||||||
request['user'] = conn.get_user_by_token(request['token'])
|
request['user'] = conn.get_user_by_token(request['token'])
|
||||||
|
|
||||||
if request['user'] and request.path == '/login':
|
if request['user'] and request.path == '/login':
|
||||||
return Response.new('', 302, {'Location': '/'})
|
return Response.new('', 302, {'Location': '/'})
|
||||||
|
|
||||||
if not request['user'] and request.path.startswith('/admin'):
|
if not request['user'] and request.path.startswith('/admin'):
|
||||||
return Response.new('', 302, {'Location': f'/login?redir={request.path}'})
|
response = Response.new('', 302, {'Location': f'/login?redir={request.path}'})
|
||||||
|
response.del_cookie('user-token')
|
||||||
|
return response
|
||||||
|
|
||||||
return await handler(request)
|
response = await handler(request)
|
||||||
|
|
||||||
|
if not request['user'] and request['token']:
|
||||||
|
print("del token")
|
||||||
|
response.del_cookie('user-token')
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@register_route('/')
|
@register_route('/')
|
||||||
|
@ -96,8 +106,8 @@ class Login(View):
|
||||||
domain = self.config.domain,
|
domain = self.config.domain,
|
||||||
path = '/',
|
path = '/',
|
||||||
secure = True,
|
secure = True,
|
||||||
httponly = True,
|
httponly = False,
|
||||||
samesite = 'Strict'
|
samesite = 'lax'
|
||||||
)
|
)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
@ -271,7 +281,7 @@ class AdminWhitelist(View):
|
||||||
|
|
||||||
with self.database.session(True) as conn:
|
with self.database.session(True) as conn:
|
||||||
if conn.get_domain_whitelist(data['domain']):
|
if conn.get_domain_whitelist(data['domain']):
|
||||||
return await self.get(request, message = "Domain already in whitelist")
|
return await self.get(request, error = "Domain already in whitelist")
|
||||||
|
|
||||||
conn.put_domain_whitelist(data['domain'])
|
conn.put_domain_whitelist(data['domain'])
|
||||||
|
|
||||||
|
@ -284,7 +294,7 @@ class AdminWhitlistDelete(View):
|
||||||
with self.database.session() as conn:
|
with self.database.session() as conn:
|
||||||
if not conn.get_domain_whitelist(domain):
|
if not conn.get_domain_whitelist(domain):
|
||||||
msg = 'Whitelisted domain not found'
|
msg = 'Whitelisted domain not found'
|
||||||
return await AdminWhitelist.run("GET", request, message = msg)
|
return await AdminWhitelist.run("GET", request, error = msg)
|
||||||
|
|
||||||
conn.del_domain_whitelist(domain)
|
conn.del_domain_whitelist(domain)
|
||||||
|
|
||||||
|
@ -342,7 +352,7 @@ class AdminDomainBansDelete(View):
|
||||||
async def get(self, request: Request, domain: str) -> Response:
|
async def get(self, request: Request, domain: str) -> Response:
|
||||||
with self.database.session() as conn:
|
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')
|
return await AdminDomainBans.run("GET", request, error = 'Domain ban not found')
|
||||||
|
|
||||||
conn.del_domain_ban(domain)
|
conn.del_domain_ban(domain)
|
||||||
|
|
||||||
|
@ -400,7 +410,7 @@ class AdminSoftwareBansDelete(View):
|
||||||
async def get(self, request: Request, name: str) -> Response:
|
async def get(self, request: Request, name: str) -> Response:
|
||||||
with self.database.session() as conn:
|
with self.database.session() as conn:
|
||||||
if not conn.get_software_ban(name):
|
if not conn.get_software_ban(name):
|
||||||
return await AdminSoftwareBans.run("GET", request, message = 'Software ban not found')
|
return await AdminSoftwareBans.run("GET", request, error = 'Software ban not found')
|
||||||
|
|
||||||
conn.del_software_ban(name)
|
conn.del_software_ban(name)
|
||||||
|
|
||||||
|
@ -441,7 +451,7 @@ class AdminUsers(View):
|
||||||
|
|
||||||
with self.database.session(True) as conn:
|
with self.database.session(True) as conn:
|
||||||
if conn.get_user(data['username']):
|
if conn.get_user(data['username']):
|
||||||
return await self.get(request, message = "User already exists")
|
return await self.get(request, error = "User already exists")
|
||||||
|
|
||||||
conn.put_user(data['username'], data['password'], data['handle'])
|
conn.put_user(data['username'], data['password'], data['handle'])
|
||||||
|
|
||||||
|
@ -453,7 +463,7 @@ class AdminUsersDelete(View):
|
||||||
async def get(self, request: Request, name: str) -> Response:
|
async def get(self, request: Request, name: str) -> Response:
|
||||||
with self.database.session() as conn:
|
with self.database.session() as conn:
|
||||||
if not conn.get_user(name):
|
if not conn.get_user(name):
|
||||||
return await AdminUsers.run("GET", request, message = 'User not found')
|
return await AdminUsers.run("GET", request, error = 'User not found')
|
||||||
|
|
||||||
conn.del_user(name)
|
conn.del_user(name)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue