Compare commits
2 commits
stable
...
jedi/dev/f
Author | SHA1 | Date | |
---|---|---|---|
a10eb089b4 | |||
4bb75c095e |
9 changed files with 629 additions and 30 deletions
|
@ -6,6 +6,11 @@
|
||||||
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
||||||
<i class="hamburger align-self-center"></i>
|
<i class="hamburger align-self-center"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="navbar-collapse collapse">
|
||||||
|
<ul class="navbar-nav navbar-align">
|
||||||
|
<UserDropdown/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
@ -14,12 +19,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
/* global closeAllDropdowns */
|
||||||
import Footer from "@/components/Footer.vue";
|
import Footer from "@/components/Footer.vue";
|
||||||
import Sidebar from "@/components/Sidebar.vue";
|
import Sidebar from "@/components/Sidebar.vue";
|
||||||
|
import UserDropdown from "@/components/UserDropdown.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BaseLayout',
|
name: 'BaseLayout',
|
||||||
components: {
|
components: {
|
||||||
|
UserDropdown,
|
||||||
Footer,
|
Footer,
|
||||||
Sidebar
|
Sidebar
|
||||||
},
|
},
|
||||||
|
@ -68,6 +76,10 @@ export default {
|
||||||
box-shadow: 0 0 2rem var(--bs-shadow)
|
box-shadow: 0 0 2rem var(--bs-shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-align {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-bg {
|
.navbar-bg {
|
||||||
background: var(--bs-white);
|
background: var(--bs-white);
|
||||||
}
|
}
|
||||||
|
|
47
frontend/src/components/UserDropdown.vue
Normal file
47
frontend/src/components/UserDropdown.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-icon dropdown-toggle d-inline-block d-sm-none" href="#" @click="toggleDropdown">
|
||||||
|
<i class="align-middle" data-feather="settings"></i>
|
||||||
|
<b-icon-person class="bi-valign-middle"></b-icon-person>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link dropdown-toggle d-none d-sm-inline-block" href="#" @click="toggleDropdown">
|
||||||
|
<span class="text-dark">
|
||||||
|
{{ username }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" id="userDropdown">
|
||||||
|
<a class="dropdown-item" href="#" @click="logout"> Log out</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from 'bootstrap-icons-vue';
|
||||||
|
import {mapMutations, mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UserDropdown",
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations(['logout']),
|
||||||
|
toggleDropdown() {
|
||||||
|
closeAllDropdowns();
|
||||||
|
document.getElementById("userDropdown").classList.toggle("show");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['user']),
|
||||||
|
username() {
|
||||||
|
return this.user;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
324
frontend/src/federation.js
Normal file
324
frontend/src/federation.js
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
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,10 +5,11 @@ import App from './App.vue'
|
||||||
import './scss/toolshed.scss'
|
import './scss/toolshed.scss'
|
||||||
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
import _nacl from 'js-nacl';
|
import _nacl from 'js-nacl';
|
||||||
|
|
||||||
const app = createApp(App).use(BootstrapIconsPlugin);
|
const app = createApp(App).use(store).use(BootstrapIconsPlugin);
|
||||||
|
|
||||||
_nacl.instantiate((nacl) => {
|
_nacl.instantiate((nacl) => {
|
||||||
window.nacl = nacl
|
window.nacl = nacl
|
||||||
|
|
48
frontend/src/neigbors.js
Normal file
48
frontend/src/neigbors.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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;
|
|
@ -2,6 +2,7 @@ import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import Index from '@/views/Index.vue';
|
import Index from '@/views/Index.vue';
|
||||||
import Login from '@/views/Login.vue';
|
import Login from '@/views/Login.vue';
|
||||||
import Register from '@/views/Register.vue';
|
import Register from '@/views/Register.vue';
|
||||||
|
import store from '@/store';
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
@ -20,7 +21,7 @@ const router = createRouter({
|
||||||
router.beforeEach((to/*, from*/) => {
|
router.beforeEach((to/*, from*/) => {
|
||||||
// instead of having to check every route record with
|
// instead of having to check every route record with
|
||||||
// to.matched.some(record => record.meta.requiresAuth)
|
// to.matched.some(record => record.meta.requiresAuth)
|
||||||
if (to.meta.requiresAuth && false) {
|
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
||||||
// this route requires auth, check if logged in
|
// this route requires auth, check if logged in
|
||||||
// if not, redirect to login page.
|
// if not, redirect to login page.
|
||||||
console.log("Not logged in, redirecting to login page")
|
console.log("Not logged in, redirecting to login page")
|
||||||
|
|
144
frontend/src/store.js
Normal file
144
frontend/src/store.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
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}))
|
||||||
|
//const data = await fetch('/auth/token/', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {'Content-Type': 'application/json'},
|
||||||
|
// body: JSON.stringify({username: username, password: password}),
|
||||||
|
// credentials: 'omit'
|
||||||
|
//}).then(r => r.json())
|
||||||
|
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]
|
||||||
|
if (domain === 'localhost')
|
||||||
|
return ['localhost:5173'];
|
||||||
|
if (domain === 'example.com')
|
||||||
|
return ['localhost:5173'];
|
||||||
|
if (domain === 'example.jedi')
|
||||||
|
return ['localhost:5173'];
|
||||||
|
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({})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
|
@ -24,7 +24,9 @@
|
||||||
<label class="form-label">Username</label>
|
<label class="form-label">Username</label>
|
||||||
<input class="form-control form-control-lg" type="text"
|
<input class="form-control form-control-lg" type="text"
|
||||||
name="username" placeholder="Enter your username"
|
name="username" placeholder="Enter your username"
|
||||||
|
:class="errors.username?['is-invalid']:[]"
|
||||||
v-model="username"/>
|
v-model="username"/>
|
||||||
|
<div class="invalid-feedback">{{ errors.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
|
@ -65,6 +67,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {mapActions, mapMutations} from 'vuex';
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -74,15 +77,15 @@ export default {
|
||||||
msg: 'Welcome to ' + location.hostname,
|
msg: 'Welcome to ' + location.hostname,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
remember: false
|
remember: false,
|
||||||
|
errors: {
|
||||||
|
username: '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setRemember(remember) {
|
...mapActions(['login']),
|
||||||
},
|
...mapMutations(['setRemember']),
|
||||||
login(data) {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
async do_login(e) {
|
async do_login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (await this.login({username: this.username, password: this.password, remember: this.remember})) {
|
if (await this.login({username: this.username, password: this.password, remember: this.remember})) {
|
||||||
|
@ -92,11 +95,24 @@ export default {
|
||||||
await router.push({path: '/'});
|
await router.push({path: '/'});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!this.username.includes('@')) {
|
||||||
|
this.errors.username = 'Username should be in the form of user@domain';
|
||||||
|
}
|
||||||
this.msg = 'Invalid username or password';
|
this.msg = 'Invalid username or password';
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$route.query.redirect) {
|
||||||
|
this.msg = 'You must be logged in to access that page';
|
||||||
|
}
|
||||||
|
const user = localStorage.getItem('user');
|
||||||
|
if (user) {
|
||||||
|
this.username = user;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
<div :class="errors.email?['mb-3','is-invalid']:['mb-3']">
|
<div :class="errors.email?['mb-3','is-invalid']:['mb-3']">
|
||||||
<label class="form-label">Email</label>
|
<label class="form-label">Email</label>
|
||||||
<input class="form-control form-control-lg" type="email"
|
<input class="form-control form-control-lg" type="email"
|
||||||
|
:class="errors.email?['is-invalid']:[]"
|
||||||
v-model="form.email" placeholder="Enter your email"/>
|
v-model="form.email" placeholder="Enter your email"/>
|
||||||
<div class="invalid-feedback">{{ errors.email }}</div>
|
<div class="invalid-feedback">{{ errors.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
<div :class="errors.password?['mb-3','is-invalid']:['mb-3']">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<input class="form-control form-control-lg" type="password"
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
:class="errors.password?['is-invalid']:[]"
|
||||||
v-model="form.password" placeholder="Enter your password"/>
|
v-model="form.password" placeholder="Enter your password"/>
|
||||||
<div class="invalid-feedback">{{ errors.password }}</div>
|
<div class="invalid-feedback">{{ errors.password }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,6 +60,7 @@
|
||||||
<div :class="errors.password2?['mb-3','is-invalid']:['mb-3']">
|
<div :class="errors.password2?['mb-3','is-invalid']:['mb-3']">
|
||||||
<label class="form-label">Password Check</label>
|
<label class="form-label">Password Check</label>
|
||||||
<input class="form-control form-control-lg" type="password"
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
:class="errors.password2?['is-invalid']:[]"
|
||||||
v-model="password2" placeholder="Enter your password again"/>
|
v-model="password2" placeholder="Enter your password again"/>
|
||||||
<div class="invalid-feedback">{{ errors.password2 }}</div>
|
<div class="invalid-feedback">{{ errors.password2 }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,6 +89,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {mapActions} from 'vuex';
|
||||||
|
import {ServerSet, createNullAuth} from '../federation';
|
||||||
|
import NeighborsCache from '../neigbors';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
data() {
|
data() {
|
||||||
|
@ -109,6 +116,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(['lookupServer']),
|
||||||
do_register() {
|
do_register() {
|
||||||
console.log('do_register');
|
console.log('do_register');
|
||||||
console.log(this.form);
|
console.log(this.form);
|
||||||
|
@ -118,36 +126,34 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.errors.password2 = null;
|
this.errors.password2 = null;
|
||||||
}
|
}
|
||||||
fetch('/auth/register/', {
|
const user = this.form.username + '@' + this.form.domain;
|
||||||
method: 'POST',
|
var unreachable = new NeighborsCache();
|
||||||
headers: {
|
var nullAuth = createNullAuth({})
|
||||||
'Content-Type': 'application/json',
|
this.lookupServer({username: user}).then(servers => new ServerSet(servers, unreachable))
|
||||||
},
|
.then(set => set.post(nullAuth, '/auth/register/', this.form))
|
||||||
body: JSON.stringify(this.form)
|
.then(response => {
|
||||||
})
|
if (response.errors) {
|
||||||
.then(response => response.json())
|
console.error('Error:', response.errors);
|
||||||
.then(data => {
|
this.errors = response.errors;
|
||||||
if (data.errors) {
|
|
||||||
console.error('Error:', data.errors);
|
|
||||||
this.errors = data.errors;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Success:', data);
|
console.log('Success:', response);
|
||||||
this.msg = 'Success';
|
this.msg = 'Success';
|
||||||
|
localStorage.setItem('user', user);
|
||||||
this.$router.push('/login');
|
this.$router.push('/login');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
this.msg = 'Error';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
fetch('/api/domains/')
|
try {
|
||||||
.then(response => response.json())
|
fetch('/local/domains')
|
||||||
.then(data => {
|
.then(response => response.json())
|
||||||
this.domains = data;
|
.then(data => {
|
||||||
});
|
this.domains = data;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue