diff --git a/README.md b/README.md index 7f91e3e..854b54b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,16 @@ # toolshed -## Development +## Installation / Development ``` bash git clone https://github.com/gr4yj3d1/toolshed.git ``` - or - ``` bash git clone https://git.neulandlabor.de/j3d1/toolshed.git ``` -all following development mode commands support auto-reloading and hot-reloading where applicable, they do not need to bw -restarted after changes. - -### Backend only +### Backend ``` bash cd toolshed/backend @@ -25,11 +20,9 @@ pip install -r requirements.txt python configure.py python manage.py runserver 0.0.0.0:8000 --insecure ``` +to run this in properly in production, you need to configure a webserver to serve the static files and proxy the requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag. -to run this in properly in production, you need to configure a webserver to serve the static files and proxy the -requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag. - -### Frontend only +### Frontend ``` bash cd toolshed/frontend @@ -37,44 +30,14 @@ npm install npm run dev ``` -### Docs only +### Docs ``` bash cd toolshed/docs mkdocs serve ``` -### Full stack -``` bash -cd toolshed -docker-compose -f deploy/docker-compose.override.yml up --build -``` - -## Deployment - -### Requirements - -- python3 -- python3-pip -- python3-venv -- wget -- unzip -- nginx -- uwsgi - -### Installation - -* Get the latest release from -`https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip` or -`https://github.com/gr4yj3d1/toolshed/archive/refs/tags/.zip`. -* Unpack it to `/var/www` or wherever you want to install toolshed. -* Create a virtual environment and install the requirements. -* Then run the configuration script. -* Configure your webserver to serve the static files and proxy the requests to the backend. -* Configure your webserver to run the backend with uwsgi. - -for detailed instructions see [docs](/docs/deployment.md). ## CLI Client diff --git a/backend/configure.py b/backend/configure.py index e027805..ee26cac 100755 --- a/backend/configure.py +++ b/backend/configure.py @@ -8,41 +8,32 @@ import dotenv from django.db import transaction, IntegrityError -class CmdCtx: +def yesno(prompt, default=False): + if not sys.stdin.isatty(): + return default + yes = {'yes', 'y', 'ye'} + no = {'no', 'n'} - def __init__(self, args): - self.args = args + if default: + yes.add('') + else: + no.add('') - def yesno(self, prompt, default=False): - if not sys.stdin.isatty() or self.args.noninteractive: - return default - elif self.args.yes: + hint = ' [Y/n] ' if default else ' [y/N] ' + + while True: + choice = input(prompt + hint).lower() + if choice in yes: return True - elif self.args.no: + elif choice in no: return False - yes = {'yes', 'y', 'ye'} - no = {'no', 'n'} - - if default: - yes.add('') else: - no.add('') - - hint = ' [Y/n] ' if default else ' [y/N] ' - - while True: - choice = input(prompt + hint).lower() - if choice in yes: - return True - elif choice in no: - return False - else: - print('Please respond with "yes" or "no"') + print('Please respond with "yes" or "no"') -def configure(ctx): +def configure(): if not os.path.exists('.env'): - if not ctx.yesno("the .env file does not exist, do you want to create it?", default=True): + if not yesno("the .env file does not exist, do you want to create it?", default=True): print('Aborting') exit(0) if not os.path.exists('.env.dist'): @@ -65,7 +56,7 @@ def configure(ctx): current_hosts = os.getenv('ALLOWED_HOSTS') print('Current ALLOWED_HOSTS: {}'.format(current_hosts)) - if ctx.yesno("Do you want to add ALLOWED_HOSTS?"): + if yesno("Do you want to add ALLOWED_HOSTS?"): hosts = input("Enter a comma-separated list of allowed hosts: ") joined_hosts = current_hosts + ',' + hosts if current_hosts else hosts dotenv.set_key('.env', 'ALLOWED_HOSTS', joined_hosts) @@ -76,21 +67,20 @@ def configure(ctx): django.setup() if not os.path.exists('db.sqlite3'): - if not ctx.yesno("No database found, do you want to create one?", default=True): + if not yesno("No database found, do you want to create one?", default=True): print('Aborting') exit(0) from django.core.management import call_command call_command('migrate') - if ctx.yesno("Do you want to create a superuser?"): + if yesno("Do you want to create a superuser?"): from django.core.management import call_command call_command('createsuperuser') call_command('collectstatic', '--no-input') - if ctx.yesno("Do you want to import all categories, properties and tags contained in this repository?", - default=True): + if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True): from hostadmin.serializers import CategorySerializer, PropertySerializer, TagSerializer from hostadmin.models import ImportedIdentifierSets from hashlib import sha256 @@ -206,7 +196,6 @@ def main(): parser = ArgumentParser(description='Toolshed Server Configuration') parser.add_argument('--yes', '-y', help='Answer yes to all questions', action='store_true') parser.add_argument('--no', '-n', help='Answer no to all questions', action='store_true') - parser.add_argument('--noninteractive', '-x', help="Run in noninteractive mode", action='store_true') parser.add_argument('cmd', help='Command', default='configure', nargs='?') args = parser.parse_args() @@ -214,10 +203,8 @@ def main(): print('Error: --yes and --no are mutually exclusive') exit(1) - ctx = CmdCtx(args) - if args.cmd == 'configure': - configure(ctx) + configure() elif args.cmd == 'reset': reset() elif args.cmd == 'testdata': diff --git a/cli-client/toolshed-client.py b/cli-client/toolshed-client.py new file mode 100755 index 0000000..2c5d434 --- /dev/null +++ b/cli-client/toolshed-client.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import argparse +import os +import requests +from nacl.signing import SigningKey +from json import dumps + + +class ToolshedApi: + user = None + host = None + signing_key = None + + def __init__(self, user, host, key): + if host is None: + raise ValueError("TOOLSHED_HOST environment variable not set") + + if user is None: + raise ValueError("TOOLSHED_USER environment variable not set") + + if key is None: + raise ValueError("TOOLSHED_KEY environment variable not set") + + if len(key) != 64: + raise ValueError("TOOLSHED_KEY must be 64 hex characters") + + signing_key = SigningKey(bytes.fromhex(key)) + + self.user = user + self.host = host + self.signing_key = signing_key + + def get(self, target): + url = "http://" + self.host + target + signed = self.signing_key.sign(url.encode('utf-8')) + signature = signed.signature.hex() + response = requests.get(url, headers={"Authorization": "Signature " + self.user + ":" + signature}) + return response.json() + + def post(self, target, data): + url = "http://" + self.host + target + json = dumps(data) + signed = self.signing_key.sign(url.encode('utf-8') + json.encode('utf-8')) + signature = signed.signature.hex() + response = requests.post(url, headers={"Authorization": "Signature " + self.user + ":" + signature}, json=data) + return response.json() + + +def main(): + host = os.environ.get('TOOLSHED_HOST') + user = os.environ.get('TOOLSHED_USER') + key = os.environ.get('TOOLSHED_KEY') + + parser = argparse.ArgumentParser(description='Toolshed API client') + parser.add_argument('--host', help='Toolshed host') + parser.add_argument('--user', help='Toolshed user') + parser.add_argument('--key', help='Toolshed key') + parser.add_argument('cmd', help='Command') + args = parser.parse_args() + + if args.host is not None: + host = args.host + + if args.user is not None: + user = args.user + + if args.key is not None: + key = args.key + + api = ToolshedApi(user, host, key) + + if args.cmd == 'getinventory': + inv = api.get("/api/inventory_items/") + print(inv) + elif args.cmd == 'additem': + inv = api.post("/api/inventory_items/", {"name": "test"}) + print(inv) + else: + print("Unknown command: " + args.cmd) + + +if __name__ == '__main__': + main() diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 9262ebe..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,102 +0,0 @@ -# Deployment - -## Native - -### Requirements - -- python3 -- python3-pip -- python3-venv -- wget -- unzip -- nginx -- uwsgi -- certbot - -### Installation - -Get the latest release: - -``` bash -cd /var/www # or wherever you want to install toolshed -wget https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip -``` -or from github: -``` bash -cd /var/www # or wherever you want to install toolshed -wget https://github.com/gr4yj3d1/toolshed/archive/refs/tags/.zip -O toolshed.zip -``` - -Extract and configure the backend: - -``` bash -unzip toolshed.zip -cd toolshed/backend -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt -python configure.py -``` - -Configure uWSGI to serve the backend locally: - -``` bash -cd /var/www/toolshed/backend -cp toolshed.ini /etc/uwsgi/apps-available/ -ln -s /etc/uwsgi/apps-available/toolshed.ini /etc/uwsgi/apps-enabled/ -systemctl restart uwsgi -``` - -Configure nginx to serve the static files and proxy the requests to the backend: - -``` bash -cd /var/www/toolshed/backend -cp toolshed.nginx /etc/nginx/sites-available/toolshed -ln -s /etc/nginx/sites-available/toolsheed /etc/nginx/sites-enabled/ -systemctl restart nginx -``` - -Configure certbot to get a certificate for the domain: - -``` bash -certbot --nginx -d -``` - -### Update - -``` bash -cd /var/www -wget https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip -unzip toolshed.zip -cd toolshed/backend -source venv/bin/activate -pip install -r requirements.txt -python configure.py -systemctl restart uwsgi -``` - -## Docker - -### Requirements - -- docker -- docker-compose -- git - -### Installation - -``` bash -git clone https://git.neulandlabor.de/j3d1/toolshed.git -# or -git clone https://github.com/gr4yj3d1/toolshed.git -cd toolshed -docker-compose -f deploy/docker-compose.prod.yml up -d --build -``` - -### Update - -``` bash -toolshed -git pull -docker-compose -f deploy/docker-compose.prod.yml up -d --build -``` \ No newline at end of file diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 3fcd229..0000000 --- a/docs/development.md +++ /dev/null @@ -1,105 +0,0 @@ -# Development - -``` bash -git clone https://github.com/gr4yj3d1/toolshed.git -``` - -or - -``` bash -git clone https://git.neulandlabor.de/j3d1/toolshed.git -``` - -## Native - -To a certain extent, the frontend and backend can be developed independently. The frontend is a Vue.js project and the -backend is a DRF (Django-Rest-Framework) project. If you want to develop the frontend, you can do so without the backend -and vice -versa. However, especially for the frontend, it is recommended to use the backend as well, as the frontend does not have -a lot of 'offline' functionality. -If you want to run the fullstack application, it is recommended to use the [docker-compose](#docker) method. - -### Frontend - -install `node.js` and `npm` - -on Debian* for example: `sudo apt install npm` - -``` bash -cd toolshed/frontend -npm install -npm run dev -``` - -### Backend - -Install `python3`, `pip` and `virtualenv` - -on Debian* for example: `sudo apt install python3 python3-pip python3-venv` - -Prepare backend environment - -``` bash -cd toolshed/backend -python -m venv venv -source venv/bin/activate -pip install -r requirements.txt -``` - -Run the test suite: - -``` bash -python manage.py test -``` - -optionally with coverage: - -``` bash -coverage run manage.py test -coverage report -``` - -Start the backend in development mode: - -``` bash -python manage.py migrate -cp .env.dist .env -echo "DEBUG = True" >> .env -python manage.py runserver 0.0.0.0:8000 -``` - -provides the api docs at `http://localhost:8000/docs/` - -### Docs (Wiki) - -Install `mkdocs` - -on Debian* for example: `sudo apt install mkdocs` - -Start the docs server: - -``` bash -cd toolshed/docs -mkdocs serve -a 0.0.0.0:8080 -``` - -## Docker - -### Fullstack - -Install `docker` and `docker-compose` - -on Debian* for example: `sudo apt install docker.io docker-compose` - -Start the fullstack application: - -``` bash -docker-compose -f deploy/docker-compose.override.yml up --build -``` - -This will start an instance of the frontend and wiki, a limited DoH (DNS over HTTPS) server and **two** instances of the backend. -The two backend instances are set up to use the domains `a.localhost` and `b.localhost`, the local DoH -server is used to direct the frontend to the correct backend instance. -The frontend is configured to act as if it was served from the domain `a.localhost`. -Access the frontend at `http://localhost:8080/`, backend at `http://localhost:8080/api/`, api docs -at `http://localhost:8080/docs/` and the wiki at `http://localhost:8080/wiki/`. \ No newline at end of file diff --git a/docs/federation.md b/docs/federation.md deleted file mode 100644 index d242677..0000000 --- a/docs/federation.md +++ /dev/null @@ -1,23 +0,0 @@ -# Federation - -This section will cover how federation works in Toolshed. - -## What is Federation? - -Since user of Toolshed you can search and interact the inventory of all their 'friends' that are potentially on -different servers there is a need for a way to communicate between servers. We don't want to rely on a central server that -stores all the data and we don't want to have a central server that handles all the communication between servers. This -is where federation comes in. Toolshed uses a protocol that can not only exchange data with the server where the user -is registered but also with the servers where their friends are registered. - -## How does it work? - -Any user can register on any server and creates a personal key pair. The public key is stored on the server and the private -key is stored on the client. The private key is used to sign all requests to the server and the public key is used to -verify the signature. Once a user has registered on a server they can send friend requests to other users containing -their public key. If the other user accepts the friend request, the server stores the public key of the friend and -uses it to verify access to the friend's inventory. While accepting a friend request the user also automatically sends -their own public key to the friend's server. This way both users can access each other's inventory. - -The protocol is based on a simple HTTPS API exchanging JSON data that is signed with the user's private key. By default -Toolshed servers provide a documentation of the API at [/docs/api](/docs/api). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 95588dd..0ba4a97 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,8 +6,47 @@ This is the documentation for the Toolshed project. It is a work in progress. `#social` `#network` `#federation` `#decentralized` `#federated` `#socialnetwork` `#fediverse` `#community` `#hashtags` ## Getting Started - - [Deploying Toolshed](deployment.md) - - [Development Setup](development.md) - - [About Federation](federation.md) +## Installation +``` bash + # TODO add installation instructions + # similar to development instructions just with more docker + # TODO add docker-compose.yml +``` + +## Development + +``` bash +git clone https://github.com/gr4yj3d1/toolshed.git +``` +or +``` bash +git clone https://git.neulandlabor.de/j3d1/toolshed.git +``` + +### Frontend + +``` bash +cd toolshed/frontend +npm install +npm run dev +``` + +### Backend + +``` bash +cd toolshed/backend +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python manage.py migrate +python manage.py runserver 0.0.0.0:8000 +``` + +### Docs + +``` bash +cd toolshed/docs +mkdocs serve -a 0.0.0.0:8080 +``` diff --git a/frontend/src/dns.js b/frontend/src/dns.js deleted file mode 100644 index f07ea22..0000000 --- a/frontend/src/dns.js +++ /dev/null @@ -1,50 +0,0 @@ -import {query} from 'dns-query'; - -function get_prefered_server() { - try { - const servers = JSON.parse(localStorage.getItem('dns-servers')); - if (servers && servers.length > 0) { - return servers; - } - } catch (e) { - console.error(e); - } - const request = new XMLHttpRequest(); - request.open('GET', '/local/dns', false); - request.send(null); - if (request.status === 200) { - const servers = JSON.parse(request.responseText); - if (servers && servers.length > 0) { - return servers; - } - } - return ['1.1.1.1', '8.8.8.8']; -} - -class FallBackResolver { - constructor() { - this._servers = get_prefered_server(); - this._cache = JSON.parse(localStorage.getItem('dns-cache')) || {}; - } - - async query(domain, type) { - const key = domain + ':' + type; - if (key in this._cache && this._cache[key].time > Date.now() - 1000 * 60 * 60) { - const age_seconds = Math.ceil(Date.now() / 1000 - this._cache[key].time / 1000); - return [this._cache[key].data]; - } - const result = await query( - {question: {type: type, name: domain}}, - { - endpoints: this._servers, - } - ) - if (result.answers.length === 0) throw new Error('No answer'); - const first = result.answers[0]; - this._cache[key] = {time: Date.now(), ...first}; // TODO hadle multiple answers - localStorage.setItem('dns-cache', JSON.stringify(this._cache)); - return [first.data]; - } -} - -export default FallBackResolver; \ No newline at end of file diff --git a/frontend/src/federation.js b/frontend/src/federation.js deleted file mode 100644 index 9afe33c..0000000 --- a/frontend/src/federation.js +++ /dev/null @@ -1,324 +0,0 @@ -class ServerSet { - constructor(servers, unreachable_neighbors) { - if (!servers || !Array.isArray(servers)) { - throw new Error('no servers') - } - if (!unreachable_neighbors || typeof unreachable_neighbors.queryUnreachable !== 'function' || typeof unreachable_neighbors.unreachable !== 'function') { - throw new Error('no unreachable_neighbors') - } - this.servers = [...new Set(servers)] // deduplicate - this.unreachable_neighbors = unreachable_neighbors; - } - - add(server) { - console.log('adding server', server) - if (!server || typeof server !== 'string') { - throw new Error('server must be a string') - } - if (server in this.servers) { - console.log('server already in set', server) - return - } - this.servers.push(server); - } - - async get(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'GET', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('get from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('get from server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async post(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('post to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('post to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async patch(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('patch to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('patch to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async put(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('put to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('put to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async delete(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'DELETE', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('delete from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ) - } catch (e) { - console.error('delete from server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async getRaw(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'GET', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('get from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ) - } catch (e) { - console.error('get from server failed', server, e) - } - } - throw new Error('all servers failed') - } -} - -class ServerSetUnion { - constructor(serverSets) { - if (!serverSets || !Array.isArray(serverSets)) { - throw new Error('no serverSets') - } - this.serverSets = serverSets; - } - - add(serverset) { - if (!serverset || !(serverset instanceof ServerSet)) { - throw new Error('no serverset') - } - if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) { - console.warn('serverset already in union', serverset) - return - } - this.serverSets.push(serverset) - } - - async get(auth, target) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.get(auth, target)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async post(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.post(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async patch(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.patch(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async put(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.put(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async delete(auth, target) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.delete(auth, target)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } -} - - -class authMethod { - constructor(method, auth) { - this.method = method; - this.auth = auth; - } - - buildAuthHeader(url, data) { - return this.method(this.auth, {url, data}) - } - -} - -function createSignAuth(username, signKey) { - const context = {username, signKey} - if (!context.signKey || !context.username || typeof context.username !== 'string' - || !(context.signKey instanceof Uint8Array) || context.signKey.length !== 64) { - throw new Error('no signKey or username') - } - return new authMethod(({signKey, username}, {url, data}) => { - const json = JSON.stringify(data) - const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + (data ? json : "")), signKey) - return {'Authorization': 'Signature ' + username + ':' + nacl.to_hex(signature)} - }, context) -} - -function createTokenAuth(token) { - const context = {token} - if (!context.token) { - throw new Error('no token') - } - return new authMethod(({token}, {url, data}) => { - return {'Authorization': 'Token ' + token} - }, context) -} - -function createNullAuth() { - return new authMethod(() => { - return {} - }, {}) -} - -export {ServerSet, ServerSetUnion, createSignAuth, createTokenAuth, createNullAuth}; - - diff --git a/frontend/src/main.js b/frontend/src/main.js index 2277ac2..66ab5c7 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,11 +5,10 @@ import App from './App.vue' import './scss/toolshed.scss' import router from './router' -import store from './store'; import _nacl from 'js-nacl'; -const app = createApp(App).use(store).use(BootstrapIconsPlugin); +const app = createApp(App).use(BootstrapIconsPlugin); _nacl.instantiate((nacl) => { window.nacl = nacl diff --git a/frontend/src/neigbors.js b/frontend/src/neigbors.js deleted file mode 100644 index bbdfc06..0000000 --- a/frontend/src/neigbors.js +++ /dev/null @@ -1,48 +0,0 @@ -class NeighborsCache { - constructor() { - //this._max_age = 1000 * 60 * 60; // 1 hour - //this._max_age = 1000 * 60 * 5; // 5 minutes - this._max_age = 1000 * 15; // 15 seconds - this._cache = JSON.parse(localStorage.getItem('neighbor-cache')) || {}; - } - - reachable(domain) { - console.log('reachable neighbor ' + domain) - if (domain in this._cache) { - delete this._cache[domain]; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - } - - unreachable(domain) { - console.log('unreachable neighbor ' + domain) - this._cache[domain] = {time: Date.now()}; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - - queryUnreachable(domain) { - //return false if unreachable - if (domain in this._cache) { - if (this._cache[domain].time > Date.now() - this._max_age) { - console.log('skip unreachable neighbor ' + domain + ' ' + Math.ceil( - Date.now()/1000 - this._cache[domain].time/1000) + 's/' + Math.ceil(this._max_age/1000) + 's') - return true - } else { - delete this._cache[domain]; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - } - return false; - } - - list() { - return Object.entries(this._cache).map(([domain, elem]) => { - return { - domain: domain, - time: elem.time - } - }) - } -} - -export default NeighborsCache; \ No newline at end of file diff --git a/frontend/src/store.js b/frontend/src/store.js deleted file mode 100644 index 6d0dd73..0000000 --- a/frontend/src/store.js +++ /dev/null @@ -1,132 +0,0 @@ -import {createStore} from 'vuex'; -import router from '@/router'; -import FallBackResolver from "@/dns"; -import NeighborsCache from "@/neigbors"; -import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation"; - - -export default createStore({ - state: { - local_loaded: false, - last_load: {}, - user: null, - token: null, - keypair: null, - remember: false, - home_servers: null, - resolver: new FallBackResolver(), - unreachable_neighbors: new NeighborsCache(), - }, - mutations: { - setUser(state, user) { - state.user = user; - if (state.remember) - localStorage.setItem('user', user); - }, - setToken(state, token) { - state.token = token; - if (state.remember) - localStorage.setItem('token', token); - }, - setKey(state, keypair) { - state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair)) - if (state.remember) - localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64)) - }, - setRemember(state, remember) { - state.remember = remember; - if (!remember) { - localStorage.removeItem('user'); - localStorage.removeItem('token'); - localStorage.removeItem('keypair'); - } - localStorage.setItem('remember', remember); - }, - setHomeServers(state, home_servers) { - state.home_servers = home_servers; - }, - logout(state) { - state.user = null; - state.token = null; - state.keypair = null; - localStorage.removeItem('user'); - localStorage.removeItem('token'); - localStorage.removeItem('keypair'); - router.push('/login'); - }, - load_local(state) { - if (state.local_loaded) - return; - const remember = localStorage.getItem('remember'); - const user = localStorage.getItem('user'); - const token = localStorage.getItem('token'); - const keypair = localStorage.getItem('keypair'); - if (user && token) { - this.commit('setUser', user); - this.commit('setToken', token); - if (keypair) { - this.commit('setKey', keypair) - } - } - state.cache_loaded = true; - } - }, - actions: { - async login({commit, dispatch, state, getters}, {username, password, remember}) { - commit('setRemember', remember); - const data = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - .then(set => set.post(getters.nullAuth, '/auth/token/', {username, password})) - if (data.token && data.key) { - commit('setToken', data.token); - commit('setUser', username); - commit('setKey', data.key); - const s = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - commit('setHomeServers', s) - return true; - } else { - return false; - } - }, - async lookupServer({state}, {username}) { - const domain = username.split('@')[1] - const request = '_toolshed-server._tcp.' + domain + '.' - return await state.resolver.query(request, 'SRV').then( - (result) => result.map( - (answer) => answer.target + ':' + answer.port)) - }, - async getHomeServers({state, dispatch, commit}) { - if (state.home_servers) - return state.home_servers - const promise = dispatch('lookupServer', {username: state.user}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - commit('setHomeServers', promise) - return promise - }, - async getFriendServers({state, dispatch, commit}, {username}) { - return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - }, - }, - getters: { - isLoggedIn(state) { - if (!state.local_loaded) { - state.remember = localStorage.getItem('remember') === 'true' - state.user = localStorage.getItem('user') - state.token = localStorage.getItem('token') - const keypair = localStorage.getItem('keypair') - if (keypair) - state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair)) - state.local_loaded = true - } - - return state.user !== null && state.token !== null; - }, - signAuth(state) { - return createSignAuth(state.user, state.keypair.signSk) - }, - tokenAuth(state) { - return createTokenAuth(state.token) - }, - nullAuth(state) { - return createNullAuth({}) - }, - } -}) \ No newline at end of file