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