add vuex store and federation layer for api calls
This commit is contained in:
		
							parent
							
								
									0fd49bc023
								
							
						
					
					
						commit
						8d64a3c528
					
				
					 4 changed files with 506 additions and 1 deletions
				
			
		
							
								
								
									
										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 router from './router' | ||||
| import store from './store'; | ||||
| 
 | ||||
| import _nacl from 'js-nacl'; | ||||
| 
 | ||||
| const app = createApp(App).use(BootstrapIconsPlugin); | ||||
| const app = createApp(App).use(store).use(BootstrapIconsPlugin); | ||||
| 
 | ||||
| _nacl.instantiate((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; | ||||
							
								
								
									
										132
									
								
								frontend/src/store.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								frontend/src/store.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | |||
| 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue