Compare commits

..

10 commits

Author SHA1 Message Date
Izalia Mae b068f4f91e move request method out of client class 2024-03-15 06:55:52 -04:00
Izalia Mae 0f3b72830b use cookie auth for frontend 2024-03-15 06:50:08 -04:00
Izalia Mae 50c323ba1e use api for users admin page 2024-03-15 06:46:55 -04:00
Izalia Mae 1ffc609058 add user management api endpoints and allow cookie for api auth 2024-03-15 06:46:31 -04:00
Izalia Mae 08f4f0e72d use api for admin config page 2024-03-15 04:53:43 -04:00
Izalia Mae 31f5decc4a create append_table_row js function 2024-03-15 00:30:04 -04:00
Izalia Mae ad17fb64f1 use api for whitelist admin page 2024-03-15 00:02:24 -04:00
Izalia Mae f775335e80 use api for instances admin page 2024-03-14 23:48:22 -04:00
Izalia Mae aeb84d7a72 use api for software bans admin page 2024-03-14 21:58:40 -04:00
Izalia Mae 17690268bc add ability to update domain bans and re-add domain ban api endpoints 2024-03-14 21:36:47 -04:00
18 changed files with 769 additions and 214 deletions

View file

@ -140,25 +140,6 @@ class Application(web.Application):
return '; '.join(data) + ';' 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))
@ -305,6 +286,7 @@ 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'
# Still have to figure out how csp headers work
# if resp.content_type == 'text/html': # if resp.content_type == 'text/html':
# resp.headers['Content-Security-Policy'] = Application.DEFAULT.get_csp(request) # resp.headers['Content-Security-Policy'] = Application.DEFAULT.get_csp(request)

View file

@ -13,6 +13,10 @@ schemes:
- https - https
securityDefinitions: securityDefinitions:
Cookie:
type: apiKey
in: cookie
name: user-token
Bearer: Bearer:
type: apiKey type: apiKey
name: Authorization name: Authorization
@ -549,6 +553,104 @@ paths:
schema: schema:
$ref: "#/definitions/Error" $ref: "#/definitions/Error"
/v1/user:
get:
tags:
- User
description: Get a list of all local users
produces:
- application/json
responses:
"200":
description: List of users
schema:
type: array
items:
$ref: "#/definitions/User"
post:
tags:
- User
description: Create a new user
parameters:
- in: formData
name: username
required: true
type: string
- in: formData
name: password
required: true
type: string
format: password
- in: formData
name: handle
required: false
type: string
format: email
produces:
- application/json
responses:
"200":
description: Newly created user
schema:
$ref: "#/definitions/User"
"404":
description: User already exists
schema:
$ref: "#/definitions/Error"
patch:
tags:
- User
description: Update a user's password or handle
parameters:
- in: formData
name: username
required: true
type: string
- in: formData
name: password
required: false
type: string
format: password
- in: formData
name: handle
required: false
type: string
format: email
produces:
- application/json
responses:
"200":
description: Updated user data
schema:
$ref: "#/definitions/User"
"404":
description: User does not exist
schema:
$ref: "#/definitions/Error"
delete:
tags:
- User
description: Delete a user
parameters:
- in: formData
name: username
required: true
type: string
produces:
- application/json
responses:
"202":
description: Successfully deleted user
schema:
$ref: "#/definitions/Message"
"404":
description: User not found
schema:
$ref: "#/definitions/Error"
/v1/whitelist: /v1/whitelist:
get: get:
tags: tags:
@ -748,6 +850,21 @@ definitions:
description: Character string used for authenticating with the api description: Character string used for authenticating with the api
type: string type: string
User:
type: object
properties:
username:
description: Username of the account
type: string
handle:
description: Fediverse handle associated with the account
type: string
format: email
created:
description: Date the account was created
type: string
format: date-time
Whitelist: Whitelist:
type: object type: object
properties: properties:

View file

@ -190,7 +190,22 @@ class Connection(SqlConnection):
return cur.one() # type: ignore return cur.one() # type: ignore
def put_user(self, username: str, password: str, handle: str | None = None) -> Row: def put_user(self, username: str, password: str | None, handle: str | None = None) -> Row:
if self.get_user(username):
data = {
'username': username
}
if password:
data['password'] = password
if handle:
data['handler'] = handle
else:
if password is None:
raise ValueError('Password cannot be empty')
data = { data = {
'username': username, 'username': username,
'hash': self.hasher.hash(password), 'hash': self.hasher.hash(password),

View file

@ -1,10 +1,14 @@
-extends "base.haml" -extends "base.haml"
-set page="Config" -set page="Config"
-block head
%script(type="application/javascript" src="/static/config.js" nonce="{{view.request['hash']}}" defer)
-import "functions.haml" as func -import "functions.haml" as func
-block content -block content
%fieldset.section %fieldset.section
%legend << Config %legend << Config
%form(action="/admin/config" method="POST")
.grid-2col .grid-2col
%label(for="name") << Name %label(for="name") << Name
%input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}") %input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}")
@ -23,5 +27,3 @@
%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")

View file

@ -2,20 +2,20 @@
-set page="Domain Bans" -set page="Domain Bans"
-block head -block head
%script(type="application/javascript" src="/static/domain_ban.js" nonce="{{view.request['hash']}}", defer) %script(type="application/javascript" src="/static/domain_ban.js" nonce="{{view.request['hash']}}")
-block content -block content
%details.section %details.section
%summary << Ban Domain %summary << Ban Domain
#add-item #add-item
%label(for="new-domain") << Domain %label(for="new-domain") << Domain
%input(type="domain" id="new-domain" name="domain" placeholder="Domain") %input(type="domain" id="new-domain" placeholder="Domain")
%label(for="new-reason") << Ban Reason %label(for="new-reason") << Ban Reason
%textarea(id="new-reason" name="new") << {{""}} %textarea(id="new-reason") << {{""}}
%label(for="new-note") << Admin Note %label(for="new-note") << Admin Note
%textarea(id="new-note" name="note") << {{""}} %textarea(id="new-note") << {{""}}
%input(type="button" value="Ban Domain" onclick="ban();") %input(type="button" value="Ban Domain" onclick="ban();")
@ -23,7 +23,7 @@
%legend << Domain Bans %legend << Domain Bans
.data-table .data-table
%table#table %table
%thead %thead
%tr %tr
%td.domain << Domain %td.domain << Domain
@ -38,11 +38,11 @@
%summary -> =ban.domain %summary -> =ban.domain
.grid-2col .grid-2col
.reason << Reason %label.reason(for="{{ban.domain}}-reason") << Reason
%textarea.reason(id="{{ban.domain}}-reason" name="reason") << {{ban.reason or ""}} %textarea.reason(id="{{ban.domain}}-reason") << {{ban.reason or ""}}
.note << Note %label.note(for="{{ban.domain}}-note") << Note
%textarea.note(id="{{ban.domain}}-note" name="note") << {{ban.note or ""}} %textarea.note(id="{{ban.domain}}-note") << {{ban.note or ""}}
%input(type="button" value="Update" onclick="update_ban('{{ban.domain}}')") %input(type="button" value="Update" onclick="update_ban('{{ban.domain}}')")
@ -50,4 +50,4 @@
=ban.created.strftime("%Y-%m-%d") =ban.created.strftime("%Y-%m-%d")
%td.remove %td.remove
%a(href="#", onclick="unban('{{ban.domain}}')" title="Unban domain") << &#10006; %a(href="#" onclick="unban('{{ban.domain}}')" title="Unban domain") << &#10006;

View file

@ -1,29 +1,32 @@
-extends "base.haml" -extends "base.haml"
-set page="Instances" -set page="Instances"
-block head
%script(type="application/javascript" src="/static/instance.js" nonce="{{view.request['hash']}}")
-block content -block content
%details.section %details.section
%summary << Add Instance %summary << Add Instance
%form(action="/admin/instances" method="POST")
#add-item #add-item
%label(for="domain") << Domain %label(for="new-actor") << Actor
%input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="url" id="new-actor" placeholder="Actor URL")
%label(for="actor") << Actor URL %label(for="new-inbox") << Inbox
%input(type="url" id="actor" name="actor" placeholder="Actor URL") %input(type="url" id="new-inbox" placeholder="Inbox URL")
%label(for="inbox") << Inbox URL %label(for="new-followid") << Follow ID
%input(type="url" id="inbox" name="inbox" placeholder="Inbox URL") %input(type="url" id="new-followid" placeholder="Follow ID URL")
%label(for="software") << Software %label(for="new-software") << Software
%input(name="software" id="software" placeholder="software") %input(id="new-software" placeholder="software")
%input(type="submit" value="Add Instance") %input(type="button" value="Add Instance", onclick="add_instance()")
-if requests -if requests
%fieldset.section %fieldset.section.requests
%legend << Follow Requests %legend << Follow Requests
.data-table .data-table
%table %table#requests
%thead %thead
%tr %tr
%td.instance << Instance %td.instance << Instance
@ -34,7 +37,7 @@
%tbody %tbody
-for request in requests -for request in requests
%tr %tr(id="{{request.domain}}")
%td.instance %td.instance
%a(href="https://{{request.domain}}" target="_new") -> =request.domain %a(href="https://{{request.domain}}" target="_new") -> =request.domain
@ -45,16 +48,16 @@
=request.created.strftime("%Y-%m-%d") =request.created.strftime("%Y-%m-%d")
%td.approve %td.approve
%a(href="/admin/instances/approve/{{request.domain}}" title="Approve Request") << &check; %a(href="#" onclick="req_response('{{request.domain}}', true)" title="Approve Request") << &check;
%td.deny %td.deny
%a(href="/admin/instances/deny/{{request.domain}}" title="Deny Request") << &#10006; %a(href="#" onclick="req_response('{{request.domain}}', false)" title="Deny Request") << &#10006;
%fieldset.section %fieldset.section.instances
%legend << Instances %legend << Instances
.data-table .data-table
%table %table#instances
%thead %thead
%tr %tr
%td.instance << Instance %td.instance << Instance
@ -64,7 +67,7 @@
%tbody %tbody
-for instance in instances -for instance in instances
%tr %tr(id="{{instance.domain}}")
%td.instance %td.instance
%a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain
@ -75,4 +78,4 @@
=instance.created.strftime("%Y-%m-%d") =instance.created.strftime("%Y-%m-%d")
%td.remove %td.remove
%a(href="/admin/instances/delete/{{instance.domain}}" title="Remove Instance") << &#10006; %a(href="#" onclick="del_instance('{{instance.domain}}')" title="Remove Instance") << &#10006;

View file

@ -1,20 +1,23 @@
-extends "base.haml" -extends "base.haml"
-set page="Software Bans" -set page="Software Bans"
-block head
%script(type="application/javascript" src="/static/software_ban.js" nonce="{{view.request['hash']}}")
-block content -block content
%details.section %details.section
%summary << Ban Software %summary << Ban Software
%form(action="/admin/software_bans" method="POST")
#add-item #add-item
%label(for="name") << Name %label(for="new-name") << Domain
%input(id="name" name="name" placeholder="Name") %input(type="name" id="new-name" placeholder="Domain")
%label(for="reason") << Ban Reason %label(for="new-reason") << Ban Reason
%textarea(id="reason" name="reason") << {{""}} %textarea(id="new-reason") << {{""}}
%label(for="note") << Admin Note %label(for="new-note") << Admin Note
%textarea(id="note" name="note") << {{""}} %textarea(id="new-note") << {{""}}
%input(type="submit" value="Ban Software") %input(type="submit" value="Ban Software" onclick="ban()")
%fieldset.section %fieldset.section
%legend << Software Bans %legend << Software Bans
@ -29,23 +32,22 @@
%tbody %tbody
-for ban in bans -for ban in bans
%tr %tr(id="{{ban.name}}")
%td.name %td.name
%details %details
%summary -> =ban.name %summary -> =ban.name
%form(action="/admin/software_bans" method="POST")
.grid-2col .grid-2col
.reason << Reason %label.reason(for="{{ban.name}}-reason") << Reason
%textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} %textarea.reason(id="{{ban.name}}-reason") << {{ban.reason or ""}}
.note << Note %label.note(for="{{ban.name}}-note") << Note
%textarea.note(id="note" name="note") << {{ban.note or ""}} %textarea.note(id="{{ban.name}}-note") << {{ban.note or ""}}
%input(type="hidden" name="name" value="{{ban.name}}") %input(type="button" value="Update" onclick="update_ban('{{ban.name}}')")
%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/software_bans/delete/{{ban.name}}" title="Unban software") << &#10006; %a(href="#" onclick="unban('{{ban.name}}')" title="Unban name") << &#10006;

View file

@ -1,29 +1,32 @@
-extends "base.haml" -extends "base.haml"
-set page="Users" -set page="Users"
-block head
%script(type="application/javascript" src="/static/user.js" nonce="{{view.request['hash']}}")
-block content -block content
%details.section %details.section
%summary << Add User %summary << Add User
%form(action="/admin/users", method="POST")
#add-item #add-item
%label(for="username") << Username %label(for="new-username") << Username
%input(id="username" name="username" placeholder="Username") %input(id="new-username" name="username" placeholder="Username")
%label(for="password") << Password %label(for="new-password") << Password
%input(type="password" id="password" name="password" placeholder="Password") %input(id="new-password" type="password" placeholder="Password")
%label(for="password2") << Password Again %label(for="new-password2") << Password Again
%input(type="password" id="password2" name="password2" placeholder="Password Again") %input(id="new-password2" type="password" placeholder="Password Again")
%label(for="handle") << Handle %label(for="new-handle") << Handle
%input(type="email" name="handle" id="handle" placeholder="handle") %input(id="new-handle" type="email" placeholder="handle")
%input(type="submit" value="Add User") %input(type="button" value="Add User" onclick="add_user()")
%fieldset.section %fieldset.section
%legend << Users %legend << Users
.data-table .data-table
%table %table#users
%thead %thead
%tr %tr
%td.username << Username %td.username << Username
@ -33,7 +36,7 @@
%tbody %tbody
-for user in users -for user in users
%tr %tr(id="{{user.username}}")
%td.username %td.username
=user.username =user.username
@ -44,4 +47,4 @@
=user.created.strftime("%Y-%m-%d") =user.created.strftime("%Y-%m-%d")
%td.remove %td.remove
%a(href="/admin/users/delete/{{user.username}}" title="Remove User") << &#10006; %a(href="#" onclick="del_user('{{user.username}}')" title="Remove User") << &#10006;

View file

@ -1,19 +1,22 @@
-extends "base.haml" -extends "base.haml"
-set page="Whitelist" -set page="Whitelist"
-block head
%script(type="application/javascript" src="/static/whitelist.js" nonce="{{view.request['hash']}}")
-block content -block content
%details.section %details.section
%summary << Add Domain %summary << Add Domain
%form(action="/admin/whitelist" method="POST")
#add-item #add-item
%label(for="domain") << Domain %label(for="new-domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="domain" id="new-domain" placeholder="Domain")
%input(type="submit" value="Add Domain") %input(type="button" value="Add Domain", onclick="add_whitelist()")
%fieldset.data-table.section %fieldset.data-table.section
%legend << Whitelist %legend << Whitelist
%table %table#whitelist
%thead %thead
%tr %tr
%td.domain << Domain %td.domain << Domain
@ -22,7 +25,7 @@
%tbody %tbody
-for item in whitelist -for item in whitelist
%tr %tr(id="{{item.domain}}")
%td.domain %td.domain
=item.domain =item.domain
@ -30,4 +33,4 @@
=item.created.strftime("%Y-%m-%d") =item.created.strftime("%Y-%m-%d")
%td.remove %td.remove
%a(href="/admin/whitelist/delete/{{item.domain}}" title="Remove whitlisted domain") << &#10006; %a(href="#" onclick="del_whitelist('{{item.domain}}')" title="Remove whitlisted domain") << &#10006;

View file

@ -1,15 +1,3 @@
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) { function get_date_string(date) {
var year = date.getFullYear().toString(); var year = date.getFullYear().toString();
var month = date.getMonth().toString(); var month = date.getMonth().toString();
@ -27,13 +15,25 @@ function get_date_string(date) {
} }
class Client { function append_table_row(table, row_name, row) {
constructor() { var table_row = table.insertRow(-1);
this.token = get_cookie("user-token"); table_row.id = row_name;
index = 0;
for (var prop in row) {
if (Object.prototype.hasOwnProperty.call(row, prop)) {
var cell = table_row.insertCell(index);
cell.className = prop;
cell.innerHTML = row[prop];
index += 1;
} }
}
}
async request(method, path, body = null) { async function request(method, path, body = null) {
var headers = { var headers = {
"Accept": "application/json" "Accept": "application/json"
} }
@ -43,10 +43,6 @@ class Client {
body = JSON.stringify(body) body = JSON.stringify(body)
} }
if (this.token !== null) {
headers["Authorization"] = "Bearer " + this.token;
}
const response = await fetch("/api/" + path, { const response = await fetch("/api/" + path, {
method: method, method: method,
mode: "cors", mode: "cors",
@ -62,29 +58,18 @@ class Client {
throw new Error(message.error); throw new Error(message.error);
} }
if (Array.isArray(message)) {
message.forEach((msg) => {
if (Object.hasOwn(msg, "created")) {
msg.created = new Date(msg.created);
}
});
} else {
if (Object.hasOwn(message, "created")) { if (Object.hasOwn(message, "created")) {
message.created = new Date(message.created); message.created = new Date(message.created);
} }
}
return message; 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();

View file

@ -0,0 +1,34 @@
const elems = [
document.querySelector("#name"),
document.querySelector("#description"),
document.querySelector("#theme"),
document.querySelector("#log-level"),
document.querySelector("#whitelist-enabled"),
document.querySelector("#approval-required")
]
async function handle_config_change(event) {
params = {
key: event.target.id,
value: event.target.type === "checkbox" ? event.target.checked : event.target.value
}
try {
await request("POST", "v1/config", params);
} catch (error) {
alert(error);
return;
}
if (params.key === "name") {
document.querySelector("#header .title").innerHTML = params.value;
document.querySelector("title").innerHTML = params.value;
}
}
for (const elem of elems) {
elem.addEventListener("change", handle_config_change);
}

View file

@ -3,9 +3,9 @@ function create_ban_object(domain, reason, note) {
text += `<summary>${domain}</summary>\n`; text += `<summary>${domain}</summary>\n`;
text += '<div class="grid-2col">\n'; text += '<div class="grid-2col">\n';
text += `<label for="${domain}-reason" class="reason">Reason</label>\n`; text += `<label for="${domain}-reason" class="reason">Reason</label>\n`;
text += `<textarea id="${domain}-reason" class="reason" name="reason">${reason}</textarea>\n`; text += `<textarea id="${domain}-reason" class="reason">${reason}</textarea>\n`;
text += `<label for="${domain}-note" class="note">Note</label>\n`; text += `<label for="${domain}-note" class="note">Note</label>\n`;
text += `<textarea id="${domain}-note" class="note" name="note">${note}</textarea>\n`; text += `<textarea id="${domain}-note" class="note">${note}</textarea>\n`;
text += `<input type="button" value="Update" onclick="update_ban(\"${domain}\"")">`; text += `<input type="button" value="Update" onclick="update_ban(\"${domain}\"")">`;
text += '</details>'; text += '</details>';
@ -14,9 +14,7 @@ function create_ban_object(domain, reason, note) {
async function ban() { async function ban() {
var table = document.getElementById("table"); var table = document.querySelector("table");
var row = table.insertRow(-1);
var elems = { var elems = {
domain: document.getElementById("new-domain"), domain: document.getElementById("new-domain"),
reason: document.getElementById("new-reason"), reason: document.getElementById("new-reason"),
@ -35,49 +33,59 @@ async function ban() {
} }
try { try {
var ban = await client.ban(values.domain, values.reason, values.note); var ban = await request("POST", "v1/domain_ban", values);
} catch (err) { } catch (err) {
alert(err); alert(err);
return return
} }
row.id = ban.domain; append_table_row(document.getElementById("instances"), ban.domain, {
var new_domain = row.insertCell(0); domain: create_ban_object(ban.domain, ban.reason, ban.note),
var new_date = row.insertCell(1); date: get_date_string(ban.created),
var new_remove = row.insertCell(2); remove: `<a href="#" onclick="unban('${ban.domain}')" title="Unban domain">&#10006;</a>`
});
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">&#10006;</a>`;
elems.domain.value = null; elems.domain.value = null;
elems.reason.value = null; elems.reason.value = null;
elems.note.value = null; elems.note.value = null;
document.querySelectorAll("details.section").forEach((elem) => { document.querySelector("details.section").open = false;
elem.open = false;
});
} }
async function update_ban(domain) { async function update_ban(domain) {
var row = document.getElementById(domain); var row = document.getElementById(domain);
var elems = {
"reason": row.querySelector("textarea.reason"),
"note": row.querySelector("textarea.note")
}
var values = {
"domain": domain,
"reason": elems.reason.value,
"note": elems.note.value
}
try {
await request("PATCH", "v1/domain_ban", values)
} catch (error) {
alert(error);
return;
}
row.querySelector("details").open = false;
} }
async function unban(domain) { async function unban(domain) {
console.log(domain);
try { try {
await client.unban(domain); await request("DELETE", "v1/domain_ban", {"domain": domain});
} catch (err) { } catch (error) {
alert(err); alert(error);
return; return;
} }

View file

@ -0,0 +1,93 @@
async function add_instance() {
var elems = {
actor: document.getElementById("new-actor"),
inbox: document.getElementById("new-inbox"),
followid: document.getElementById("new-followid"),
software: document.getElementById("new-software")
}
var values = {
actor: elems.actor.value.trim(),
inbox: elems.inbox.value.trim(),
followid: elems.followid.value.trim(),
software: elems.software.value.trim()
}
if (values.actor === "") {
alert("Actor is required");
return;
}
try {
var instance = await request("POST", "v1/instance", values);
} catch (err) {
alert(err);
return
}
append_table_row(document.getElementById("instances"), instance.domain, {
domain: `<a href="https://${instance.domain}/" target="_new">${instance.domain}</a>`,
software: instance.software,
date: get_date_string(instance.created),
remove: `<a href="#" onclick="del_instance('${instance.domain}')" title="Remove Instance">&#10006;</a>`
});
elems.actor.value = null;
elems.inbox.value = null;
elems.followid.value = null;
elems.software.value = null;
document.querySelector("details.section").open = false;
}
async function del_instance(domain) {
try {
await request("DELETE", "v1/instance", {"domain": domain});
} catch (error) {
alert(error);
return;
}
document.getElementById(domain).remove();
}
async function req_response(domain, accept) {
params = {
"domain": domain,
"accept": accept
}
try {
await request("POST", "v1/request", params);
} catch (error) {
alert(error);
return;
}
document.getElementById(domain).remove();
if (document.getElementById("requests").rows.length < 2) {
document.querySelector("fieldset.requests").remove()
}
if (!accept) {
return;
}
instances = await request("GET", `v1/instance`, null);
instances.forEach((instance) => {
if (instance.domain === domain) {
append_table_row(document.getElementById("instances"), instance.domain, {
domain: `<a href="https://${instance.domain}/" target="_new">${instance.domain}</a>`,
software: instance.software,
date: get_date_string(instance.created),
remove: `<a href="#" onclick="del_instance('${instance.domain}')" title="Remove Instance">&#10006;</a>`
});
}
});
}

View file

@ -0,0 +1,95 @@
function create_ban_object(name, reason, note) {
var text = '<details>\n';
text += `<summary>${name}</summary>\n`;
text += '<div class="grid-2col">\n';
text += `<label for="${name}-reason" class="reason">Reason</label>\n`;
text += `<textarea id="${name}-reason" class="reason">${reason}</textarea>\n`;
text += `<label for="${name}-note" class="note">Note</label>\n`;
text += `<textarea id="${name}-note" class="note">${note}</textarea>\n`;
text += `<input type="button" value="Update" onclick="update_ban(\"${name}\"")">`;
text += '</details>';
return text;
}
async function ban() {
var table = document.querySelector("table");
var row = table.insertRow(-1);
var elems = {
name: document.getElementById("new-name"),
reason: document.getElementById("new-reason"),
note: document.getElementById("new-note")
}
var values = {
name: elems.name.value.trim(),
reason: elems.reason.value,
note: elems.note.value
}
if (values.name === "") {
alert("Domain is required");
return;
}
try {
var ban = await request("POST", "v1/software_ban", values);
} catch (err) {
alert(err);
return
}
append_table_row(document.getElementById("instances"), ban.name, {
name: create_ban_object(ban.name, ban.reason, ban.note),
date: get_date_string(ban.created),
remove: `<a href="#" onclick="unban('${ban.domain}')" title="Unban software">&#10006;</a>`
});
elems.name.value = null;
elems.reason.value = null;
elems.note.value = null;
document.querySelector("details.section").open = false;
}
async function update_ban(name) {
var row = document.getElementById(name);
var elems = {
"reason": row.querySelector("textarea.reason"),
"note": row.querySelector("textarea.note")
}
var values = {
"name": name,
"reason": elems.reason.value,
"note": elems.note.value
}
try {
await request("PATCH", "v1/software_ban", values)
} catch (error) {
alert(error);
return;
}
row.querySelector("details").open = false;
}
async function unban(name) {
try {
await request("DELETE", "v1/software_ban", {"name": name});
} catch (error) {
alert(error);
return;
}
document.getElementById(name).remove();
}

View file

@ -0,0 +1,60 @@
async function add_user() {
var elems = {
username: document.getElementById("new-username"),
password: document.getElementById("new-password"),
password2: document.getElementById("new-password2"),
handle: document.getElementById("new-handle")
}
var values = {
username: elems.username.value.trim(),
password: elems.password.value.trim(),
password2: elems.password2.value.trim(),
handle: elems.handle.value.trim()
}
if (values.username === "" | values.password === "" | values.password2 === "") {
alert("Username, password, and password2 are required");
return;
}
if (values.password !== values.password2) {
alert("Passwords do not match");
return;
}
try {
var user = await request("POST", "v1/user", values);
} catch (err) {
alert(err);
return
}
append_table_row(document.getElementById("users"), user.username, {
domain: user.username,
handle: user.handle,
date: get_date_string(user.created),
remove: `<a href="#" onclick="del_user('${user.username}')" title="Delete User">&#10006;</a>`
});
elems.username.value = null;
elems.password.value = null;
elems.password2.value = null;
elems.handle.value = null;
document.querySelector("details.section").open = false;
}
async function del_user(username) {
try {
await request("DELETE", "v1/user", {"username": username});
} catch (error) {
alert(error);
return;
}
document.getElementById(username).remove();
}

View file

@ -0,0 +1,39 @@
async function add_whitelist() {
var domain_elem = document.getElementById("new-domain");
var domain = domain_elem.value.trim();
if (domain === "") {
alert("Domain is required");
return;
}
try {
var item = await request("POST", "v1/whitelist", {"domain": domain});
} catch (err) {
alert(err);
return
}
append_table_row(document.getElementById("whitelist"), item.domain, {
domain: item.domain,
date: get_date_string(item.created),
remove: `<a href="#" onclick="del_whitelist('${item.domain}')" title="Remove whitelisted domain">&#10006;</a>`
});
domain_elem.value = null;
document.querySelector("details.section").open = false;
}
async function del_whitelist(domain) {
try {
await request("DELETE", "v1/whitelist", {"domain": domain});
} catch (error) {
alert(error);
return;
}
document.getElementById(domain).remove();
}

View file

@ -10,7 +10,7 @@ from .base import View, register_route
from .. import __version__ from .. import __version__
from ..database import ConfigData from ..database import ConfigData
from ..misc import Message, Response, get_app from ..misc import Message, Response, boolean, get_app
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from aiohttp.web import Request from aiohttp.web import Request
@ -34,6 +34,10 @@ def check_api_path(method: str, path: str) -> bool:
@web.middleware @web.middleware
async def handle_api_path(request: Request, handler: Callable) -> Response: async def handle_api_path(request: Request, handler: Callable) -> Response:
try: try:
if (token := request.cookies.get('user-token')):
request['token'] = token
else:
request['token'] = request.headers['Authorization'].replace('Bearer', '').strip() request['token'] = request.headers['Authorization'].replace('Bearer', '').strip()
with get_app().database.session() as conn: with get_app().database.session() as conn:
@ -133,6 +137,8 @@ class Config(View):
if isinstance(data, Response): if isinstance(data, Response):
return data return data
data['key'] = data['key'].replace('-', '_');
if data['key'] not in ConfigData.USER_KEYS(): if data['key'] not in ConfigData.USER_KEYS():
return Response.new_error(400, 'Invalid key', 'json') return Response.new_error(400, 'Invalid key', 'json')
@ -161,7 +167,7 @@ class Config(View):
class Inbox(View): class Inbox(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
data = tuple(conn.execute('SELECT * FROM inboxes').all()) data = conn.get_inboxes()
return Response.new(data, ctype = 'json') return Response.new(data, ctype = 'json')
@ -186,6 +192,12 @@ class Inbox(View):
data['inbox'] = actor_data.shared_inbox data['inbox'] = actor_data.shared_inbox
if not data.get('software'):
nodeinfo = await self.client.fetch_nodeinfo(data['domain'])
if nodeinfo is not None:
data['software'] = nodeinfo.sw_name
row = conn.put_inbox(**data) row = conn.put_inbox(**data)
return Response.new(row, ctype = 'json') return Response.new(row, ctype = 'json')
@ -206,7 +218,7 @@ class Inbox(View):
return Response.new(instance, ctype = 'json') return Response.new(instance, ctype = 'json')
async def delete(self, request: Request, domain: str) -> Response: async def delete(self, request: Request) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
data = await self.get_api_data(['domain'], []) data = await self.get_api_data(['domain'], [])
@ -232,10 +244,7 @@ class RequestView(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await self.get_api_data(['domain', 'accept'], []) data = await self.get_api_data(['domain', 'accept'], [])
data['accept'] = boolean(data['accept'])
if not isinstance(data['accept'], bool):
atype = type(data['accept']).__name__
return Response.new_error(400, f'Invalid type for "accept": {atype}', 'json')
try: try:
with self.database.session(True) as conn: with self.database.session(True) as conn:
@ -274,6 +283,54 @@ 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)
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:
@ -311,7 +368,7 @@ class SoftwareBan(View):
if not any([data.get('note'), data.get('reason')]): if not any([data.get('note'), data.get('reason')]):
return Response.new_error(400, 'Must include note and/or reason parameters', 'json') return Response.new_error(400, 'Must include note and/or reason parameters', 'json')
ban = conn.update_software_ban(data['name'], **data) ban = conn.update_software_ban(**data)
return Response.new(ban, ctype = 'json') return Response.new(ban, ctype = 'json')
@ -331,6 +388,63 @@ class SoftwareBan(View):
return Response.new({'message': 'Unbanned software'}, ctype = 'json') return Response.new({'message': 'Unbanned software'}, ctype = 'json')
@register_route('/api/v1/user')
class User(View):
async def get(self, request: Request) -> Response:
with self.database.session() as conn:
items = []
for row in conn.execute('SELECT * FROM users'):
del row['hash']
items.append(row)
return Response.new(items, ctype = 'json')
async def post(self, request: Request) -> Response:
data = await self.get_api_data(['username', 'password'], ['handle'])
if isinstance(data, Response):
return data
with self.database.session() as conn:
if conn.get_user(data['username']):
return Response.new_error(404, 'User already exists', 'json')
user = conn.put_user(**data)
del user['hash']
return Response.new(user, ctype = 'json')
async def patch(self, request: Request) -> Response:
data = await self.get_api_data(['username'], ['password', ['handle']])
if isinstance(data, Response):
return data
with self.database.session(True) as conn:
user = conn.put_user(**data)
del user['hash']
return Response.new(user, ctype = 'json')
async def delete(self, request: Request) -> Response:
data = await self.get_api_data(['username'], [])
if isinstance(data, Response):
return data
with self.database.session(True) as conn:
if not conn.get_user(data['username']):
return Response.new_error(404, 'User does not exist', 'json')
conn.del_user(data['username'])
return Response.new({'message': 'Deleted user'}, ctype = 'json')
@register_route('/api/v1/whitelist') @register_route('/api/v1/whitelist')
class Whitelist(View): class Whitelist(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:

View file

@ -126,7 +126,7 @@ class View(AbstractView):
return Response.new_error(400, 'Invalid JSON data', 'json') return Response.new_error(400, 'Invalid JSON data', 'json')
else: else:
post_data = convert_data(await self.request.query) # type: ignore post_data = convert_data(self.request.query) # type: ignore
data = {} data = {}