Compare commits
1 commit
stable
...
jedi/dev/d
Author | SHA1 | Date | |
---|---|---|---|
37350f59c3 |
16 changed files with 156 additions and 1085 deletions
47
README.md
47
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/<version>/toolshed.zip` or
|
||||
`https://github.com/gr4yj3d1/toolshed/archive/refs/tags/<version>.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
|
||||
|
||||
|
|
|
@ -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':
|
||||
|
|
54
deploy/docker-compose.prod.yml
Normal file
54
deploy/docker-compose.prod.yml
Normal file
|
@ -0,0 +1,54 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ../backend/
|
||||
dockerfile: ../deploy/dev/Dockerfile.backend
|
||||
volumes:
|
||||
- ../backend:/code
|
||||
expose:
|
||||
- 8000
|
||||
command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend/
|
||||
dockerfile: ../deploy/dev/Dockerfile.frontend
|
||||
volumes:
|
||||
- ../frontend:/app
|
||||
expose:
|
||||
- 5173
|
||||
command: npm run dev -- --host
|
||||
|
||||
wiki:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: deploy/dev/Dockerfile.wiki
|
||||
volumes:
|
||||
- ../mkdocs.yml:/wiki/mkdocs.yml
|
||||
- ../docs:/wiki/docs
|
||||
expose:
|
||||
- 8001
|
||||
command: mkdocs serve --dev-addr=0.0.0.0:8001
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./dev/fullchain.pem2:/etc/nginx/nginx.crt
|
||||
- ./dev/privkey.pem2:/etc/nginx/nginx.key
|
||||
- ./dev/nginx-instance_a.dev.conf:/etc/nginx/nginx.conf
|
||||
- ./dev/dns.json:/var/www/dns.json
|
||||
- ./dev/domains.json:/var/www/domains.json
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 5353:5353
|
||||
|
||||
dns:
|
||||
build:
|
||||
context: ./dev/
|
||||
dockerfile: Dockerfile.dns
|
||||
volumes:
|
||||
- ./dev/zone.json:/dns/zone.json
|
||||
expose:
|
||||
- 8053
|
15
deploy/prod/Dockerfile.frontend
Normal file
15
deploy/prod/Dockerfile.frontend
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Use an official Node.js runtime as instance_a parent image
|
||||
FROM node:14
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
COPY package.json ./
|
||||
|
||||
RUN npm install
|
||||
# If you are building your code for production
|
||||
# RUN npm ci --only=production
|
||||
|
||||
CMD [ "npm", "run", "dev", "--", "--host"]
|
14
deploy/prod/Dockerfile.frontend.prod
Normal file
14
deploy/prod/Dockerfile.frontend.prod
Normal file
|
@ -0,0 +1,14 @@
|
|||
ROM node:alpine as builder
|
||||
WORKDIR /app
|
||||
COPY ./package.json /app/package.json
|
||||
COPY . /app
|
||||
RUN npm ci --only=production
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM nginx:alpine as runner
|
||||
#RUN apk add --update npm
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
|
@ -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/<version>/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/<version>.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 <domain>
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
``` bash
|
||||
cd /var/www
|
||||
wget https://git.neulandlabor.de/j3d1/toolshed/releases/download/<version>/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
|
||||
```
|
|
@ -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/`.
|
|
@ -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).
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="wrapper">
|
||||
<Sidebar/>
|
||||
<div class="main">
|
||||
<nav class="navbar navbar-expand navbar-light navbar-bg">
|
||||
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
||||
|
@ -15,13 +14,11 @@
|
|||
|
||||
<script>
|
||||
import Footer from "@/components/Footer.vue";
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
|
||||
export default {
|
||||
name: 'BaseLayout',
|
||||
components: {
|
||||
Footer,
|
||||
Sidebar
|
||||
Footer
|
||||
},
|
||||
props: {
|
||||
hideSearch: {
|
||||
|
@ -32,8 +29,6 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
toggleSidebar() {
|
||||
closeAllDropdowns();
|
||||
document.getElementById("sidebar").classList.toggle("collapsed");
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
<template>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
<router-link to="/" class="sidebar-brand">
|
||||
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed Logo" class="align-middle logo mr-2 h-75">
|
||||
<span class="align-middle">Toolshed</span>
|
||||
</router-link>
|
||||
<ul class="sidebar-nav">
|
||||
<li class="sidebar-header">
|
||||
Tools & Components
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
|
||||
export default {
|
||||
name: "Sidebar",
|
||||
components: {
|
||||
...BIcons
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
min-width: 260px;
|
||||
max-width: 260px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.sidebar, .sidebar-content {
|
||||
transition: margin-left .35s ease-in-out, left .35s ease-in-out, margin-right .35s ease-in-out, right .35s ease-in-out;
|
||||
background: #222e3c;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.sidebar {
|
||||
min-width: 260px;
|
||||
max-width: 260px;
|
||||
direction: ltr
|
||||
}
|
||||
|
||||
.sidebar, .sidebar-content {
|
||||
transition: margin-left .35s ease-in-out, left .35s ease-in-out, margin-right .35s ease-in-out, right .35s ease-in-out;
|
||||
background: #222e3c
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
flex-grow: 1
|
||||
}
|
||||
|
||||
.sidebar-link i, .sidebar-link svg, a.sidebar-link i, a.sidebar-link svg {
|
||||
margin-right: .75rem;
|
||||
color: rgba(233, 236, 239, .5)
|
||||
}
|
||||
|
||||
.sidebar-item.active .sidebar-link:hover, .sidebar-item.active > .sidebar-link {
|
||||
color: #e9ecef;
|
||||
background: linear-gradient(90deg, rgba(59, 125, 221, .1), rgba(59, 125, 221, .0875) 50%, transparent);
|
||||
border-left-color: #3b7ddd
|
||||
}
|
||||
|
||||
.sidebar-item.active .sidebar-link:hover i, .sidebar-item.active .sidebar-link:hover svg, .sidebar-item.active > .sidebar-link i, .sidebar-item.active > .sidebar-link svg {
|
||||
color: #e9ecef
|
||||
}
|
||||
|
||||
.sidebar-dropdown .sidebar-link {
|
||||
padding: .625rem 1.5rem .625rem 3.25rem;
|
||||
font-weight: 400;
|
||||
font-size: 90%;
|
||||
border-left: 0;
|
||||
color: #adb5bd;
|
||||
background: transparent
|
||||
}
|
||||
|
||||
.sidebar-dropdown .sidebar-link:before {
|
||||
content: "→";
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -14px;
|
||||
transition: all .1s ease;
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
.sidebar-dropdown .sidebar-item .sidebar-link:hover {
|
||||
font-weight: 400;
|
||||
border-left: 0;
|
||||
color: #e9ecef;
|
||||
background: transparent
|
||||
}
|
||||
|
||||
.sidebar-dropdown .sidebar-item .sidebar-link:hover:hover:before {
|
||||
transform: translateX(4px)
|
||||
}
|
||||
|
||||
.sidebar-dropdown .sidebar-item.active .sidebar-link {
|
||||
font-weight: 400;
|
||||
border-left: 0;
|
||||
color: #518be1;
|
||||
background: transparent
|
||||
}
|
||||
|
||||
.sidebar [data-toggle=collapse] {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.sidebar [data-toggle=collapse]:after {
|
||||
content: " ";
|
||||
border: solid;
|
||||
border-width: 0 .075rem .075rem 0;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
transform: rotate(45deg);
|
||||
position: absolute;
|
||||
top: 1.2rem;
|
||||
right: 1.5rem;
|
||||
transition: all .2s ease-out
|
||||
}
|
||||
|
||||
.sidebar [aria-expanded=true]:after, .sidebar [data-toggle=collapse]:not(.collapsed):after {
|
||||
transform: rotate(-135deg);
|
||||
top: 1.4rem
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
padding: 1.15rem 1.5rem;
|
||||
display: block;
|
||||
color: #f8f9fa
|
||||
}
|
||||
|
||||
.sidebar-brand:hover {
|
||||
text-decoration: none;
|
||||
color: #f8f9fa
|
||||
}
|
||||
|
||||
.sidebar-brand:focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
margin-left: -260px
|
||||
}
|
||||
|
||||
@media (min-width: 1px) and (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
margin-left: -260px
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
margin-left: 0
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
background: transparent;
|
||||
padding: 1.5rem 1.5rem .375rem;
|
||||
font-size: .75rem;
|
||||
color: #ced4da
|
||||
}
|
||||
</style>
|
|
@ -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;
|
|
@ -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};
|
||||
|
||||
|
|
@ -5,43 +5,12 @@ 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
|
||||
app.use(router).mount('#app')
|
||||
});
|
||||
|
||||
window.closeAllDropdowns = function () {
|
||||
const dropdowns = document.getElementsByClassName("dropdown-menu");
|
||||
let i;
|
||||
for (i = 0; i < dropdowns.length; i++) {
|
||||
const openDropdown = dropdowns[i];
|
||||
if (openDropdown.classList.contains('show')) {
|
||||
openDropdown.classList.remove('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onclick = function (event) {
|
||||
if (!event.target.matches('.dropdown-toggle *')
|
||||
&& !event.target.matches('.dropdown-toggle')
|
||||
&& !event.target.matches('.dropdown-menu *')
|
||||
&& !event.target.matches('.dropdown-menu')) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
if (!event.target.matches('.sidebar-toggle *')
|
||||
&& !event.target.matches('.sidebar-toggle')
|
||||
&& !event.target.matches('.sidebar *')
|
||||
&& !event.target.matches('.sidebar')) {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const marginLeft = parseInt(getComputedStyle(sidebar).marginLeft);
|
||||
if (sidebar.classList.contains('collapsed') && marginLeft === 0) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
|
@ -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({})
|
||||
},
|
||||
}
|
||||
})
|
Loading…
Reference in a new issue