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, friends: [], item_map: {}, home_servers: null, all_friends_servers: null, messages: [], resolver: new FallBackResolver(), unreachable_neighbors: new NeighborsCache(), tags: [], properties: [], files: [], categories: [], availability_policies: [], }, 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); }, setInventoryItems(state, {url, items}) { state.item_map[url] = items; }, setFriends(state, friends) { state.friends = friends; }, setHomeServers(state, home_servers) { state.home_servers = home_servers; }, setAllFriendsServers(state, servers) { state.all_friends_servers = servers; }, setTags(state, tags) { state.tags = tags; }, setProperties(state, properties) { state.properties = properties; }, setCategories(state, categories) { state.categories = categories; }, setAvailabilityPolicies(state, availability_policies) { state.availability_policies = availability_policies; }, setDomains(state, domains) { state.domains = domains; }, 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) } router.push('/'); } state.cache_loaded = true; } }, actions: { async login({commit, dispatch, state, getters}, {username, password, remember}) { commit('setRemember', remember); const s = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) const data = await s.post(getters.nullAuth, '/auth/token/', {username, password}) if (data.token && data.key) { commit('setToken', data.token); commit('setUser', username); commit('setKey', data.key); commit('setHomeServers', s) return true; } else { return false; } }, async lookupServer({state}, {username}) { const domain = username.split('@')[1] if (domain === 'example.eleon') return ['10.23.42.186:8000']; if (domain === 'localhost') return ['127.0.0.1:8000']; if (domain === 'example.com') return ['10.23.42.128:8000','10.23.42.128:8000']; if (domain === 'example.jedi') return ['10.23.42.128:8000']; if (domain === 'example2.com') return ['10.23.42.128:9000']; 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 getAllKnownServers({state, dispatch, commit}) { const friends = await dispatch('fetchFriends') if (state.all_friends_servers) return state.all_friends_servers const promise = (async () => { const servers = new ServerSetUnion([]) const home = await dispatch('getHomeServers') servers.add(home) for (const friend of friends) { const s = await dispatch('lookupServer', {username: friend.username}) servers.add(new ServerSet(s, state.unreachable_neighbors)) } return servers })() commit('setAllFriendsServers', promise) return promise }, async getFriendServers({state, dispatch, commit}, {username}) { return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) }, async fetchInventoryItems({commit, dispatch, getters}) { const servers = await dispatch('getHomeServers') const items = await servers.get(getters.signAuth, '/api/inventory_items/') items.map(item => item.files.map(file => file.owner = item.owner)) commit('setInventoryItems', {url: '/', items}) return items }, async searchInventories({state, dispatch, getters}, {query}) { const servers = await dispatch('getAllFriendsServers') return await servers.get(getters.signAuth, '/api/inventory/search/?q=' + query) }, async createInventoryItem({state, dispatch, getters}, item) { const servers = await dispatch('getHomeServers') const data = {availability_policy: 'private', ...item} const reply = await servers.post(getters.signAuth, '/api/inventory_items/', data) state.last_load.files = 0 return reply }, async updateInventoryItem({state, dispatch, getters}, item) { const servers = await dispatch('getHomeServers') const data = {availability_policy: 'friends', ...item} data.files = data.files.map(file => file.id) return await servers.patch(getters.signAuth, '/api/inventory_items/' + item.id + '/', data) }, async deleteInventoryItem({state, dispatch, getters}, item) { const servers = await dispatch('getHomeServers') const ret = await servers.delete(getters.signAuth, '/api/inventory_items/' + item.id + '/') dispatch('fetchInventoryItems') return ret }, async fetchSearchResults({state, dispatch, getters}, {query}) { const servers = await dispatch('getAllFriendsServers') return await servers.get(getters.signAuth, '/api/search/?query=' + query) }, async fetchFriends({commit, dispatch, getters, state}) { const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/friends/') commit('setFriends', data) return data }, async fetchFriendRequests({state, dispatch, getters}) { const servers = await dispatch('getHomeServers') return await servers.get(getters.signAuth, '/api/friendrequests/') }, async requestFriend({state, dispatch, getters}, {username}) { if (username in state.friends) { return true; } const home_servers = await dispatch('getHomeServers') const home_reply = await home_servers.post(getters.signAuth, '/api/friendrequests/', { befriender: state.user, befriendee: username }) if (home_reply.status !== 'pending' || !home_reply.secret) return false; const befriendee_servers = await dispatch('getFriendServers', {username}) const ext_reply = await befriendee_servers.post(getters.signAuth, '/api/friendrequests/', { befriender: state.user, befriendee: username, befriender_key: nacl.to_hex(state.keypair.signPk), secret: home_reply.secret }) return true; }, async acceptFriend({state, dispatch, getters}, {id, secret, befriender}) { const home_servers = await dispatch('getHomeServers') const home_reply = await home_servers.post(getters.signAuth, '/api/friends/', { friend_request_id: id, secret: secret }) const ext_servers = await dispatch('getFriendServers', {username: befriender}) const ext_reply = await ext_servers.post(getters.signAuth, '/api/friendrequests/', { befriender: state.user, befriendee: befriender, befriender_key: nacl.to_hex(state.keypair.signPk), secret: secret }) return true }, async declineFriend({state, dispatch, getters}, {id}) { const servers = await dispatch('getHomeServers') return await servers.delete(getters.signAuth, '/api/friendrequests/' + id + '/') }, async dropFriend({state, dispatch, getters}, {id}) { const servers = await dispatch('getHomeServers') return await servers.delete(getters.signAuth, '/api/friends/' + id + '/') }, async fetchTags({state, commit, dispatch, getters}) { if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) { return state.tags } const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/tags/') commit('setTags', data) state.last_load.tags = Date.now() return data }, async fetchProperties({state, commit, dispatch, getters}) { if (state.last_load.properties > Date.now() - 1000 * 60 * 60 * 24) { return state.properties } const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/properties/') commit('setProperties', data) state.last_load.properties = Date.now() return data }, async fetchCategories({state, commit, dispatch, getters}) { if (state.last_load.categories > Date.now() - 1000 * 60 * 60 * 24) { return state.categories } const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/categories/') commit('setCategories', data) state.last_load.categories = Date.now() return data }, async fetchAvailabilityPolicies({state, commit, dispatch, getters}) { if (state.last_load.availability_policies > Date.now() - 1000 * 60 * 60 * 24) { return state.availability_policies } const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/availability_policies/') commit('setAvailabilityPolicies', data.map(policy => ({slug: policy[0], text: policy[1]}))) state.last_load.availability_policies = Date.now() return data }, async fetchInfo({state, commit, dispatch, getters}) { const last_load_info = Math.min( state.last_load.tags, state.last_load.properties, state.last_load.categories, state.last_load.availability_policies) if (last_load_info > Date.now() - 1000 * 60 * 60 * 24) { return state.info } const servers = await dispatch('getHomeServers') const data = await servers.get(getters.signAuth, '/api/info/') commit('setTags', data.tags) commit('setProperties', data.properties) commit('setCategories', data.categories) commit('setAvailabilityPolicies', data.availability_policies.map(policy => ({ slug: policy[0], text: policy[1] }))) commit('setDomains', data.domains) state.last_load.tags = Date.now() state.last_load.properties = Date.now() state.last_load.categories = Date.now() state.last_load.availability_policies = Date.now() return data } }, 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({}) }, inventory_items(state) { return state.item_map['/'] || [] }, loaded_items(state) { return Object.entries(state.item_map).reduce((acc, [url, items]) => { return acc.concat(items) }, []) }, notifications(state) { // supported types: error, warning, info, login, success, friend const u = state.unreachable_neighbors.list().map(elem => { return { type: 'error', title: elem.domain + ' unreachable', msg: 'The neighbor ' + elem.domain + ' is currently unreachable. Please try again later.', time: elem.time } }) return [...u, { type: 'info', title: 'Welcome to the Toolshed', msg: 'This is a federated social network. You can add friends from other servers and share items with them.', time: Date.now() }, { type: 'warning', title: 'Lorem ipsum', msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.', time: Date.now() - 1000 * 60 * 60 * 2 }, { type: 'login', title: 'Login from 192.186.1.8', time: Date.now() - 1000 * 60 * 60 * 5 }, { type: 'friend', title: 'New connection', msg: 'Christina accepted your request.', time: Date.now() - 1000 * 60 * 60 * 14 }, { type: 'success', title: 'Lorem ipsum', msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.', time: Date.now() - 1000 * 60 * 60 * 24 }] }, } })