778 lines
No EOL
35 KiB
JavaScript
778 lines
No EOL
35 KiB
JavaScript
import {createStore} from 'vuex';
|
|
import router from '@/router';
|
|
import FallBackResolver from "@/dns";
|
|
import NeighborsCache from "@/neigbors";
|
|
import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation";
|
|
import {parseIdentityRecord, serializeIdentityRecord} from "@/identity";
|
|
import {serializeWorkflowPayload, deserializeWorkflowPayload} from "@/workflows.js";
|
|
//import sharedStatePlugin from "@/../extras/shared-state-plugin";
|
|
//import persistentStatePlugin from "@/../extras/persistent-state-plugin";
|
|
|
|
const defaultPreferenceDefinitions = [
|
|
{
|
|
key: 'ui.compact_mode',
|
|
type: 'boolean',
|
|
default: false,
|
|
label: 'Compact mode',
|
|
description: 'Show denser item rows and reduce spacing in lists.'
|
|
},
|
|
{
|
|
key: 'ui.default_search_scope',
|
|
type: 'enum',
|
|
options: ['inventory', 'friends', 'all'],
|
|
default: 'inventory',
|
|
label: 'Default search scope',
|
|
description: 'Choose where the global search starts.'
|
|
},
|
|
{
|
|
key: 'notifications.desktop_enabled',
|
|
type: 'boolean',
|
|
default: true,
|
|
label: 'Desktop notifications',
|
|
description: 'Enable in-browser notifications for important updates.'
|
|
},
|
|
{
|
|
key: 'files.max_upload_mb',
|
|
type: 'integer',
|
|
default: 25,
|
|
label: 'Default upload size limit (MB)',
|
|
description: 'Used as a prefill hint in upload dialogs.'
|
|
},
|
|
{
|
|
key: 'ui.experimental_flags',
|
|
type: 'json',
|
|
default: {},
|
|
label: 'Experimental flags',
|
|
description: 'Optional JSON toggles for feature previews.'
|
|
},
|
|
]
|
|
|
|
const parseStoredPreferences = (storageKey) => {
|
|
try {
|
|
const raw = localStorage.getItem(storageKey)
|
|
if (!raw) {
|
|
return {}
|
|
}
|
|
const parsed = JSON.parse(raw)
|
|
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
} catch (_error) {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
|
|
export default createStore({
|
|
state: {
|
|
local_loaded: false,
|
|
last_load: {},
|
|
user: null,
|
|
user_profile: null,
|
|
token: null,
|
|
keypair: null,
|
|
remember: false,
|
|
friends: [],
|
|
friendProfiles: {},
|
|
item_map: {},
|
|
home_servers: null,
|
|
all_friends_servers: null,
|
|
messages: [],
|
|
resolver: new FallBackResolver(),
|
|
unreachable_neighbors: new NeighborsCache(),
|
|
tags: [],
|
|
properties: [],
|
|
files: [],
|
|
categories: [],
|
|
availability_policies: [],
|
|
domains: [],
|
|
storage_locations: [],
|
|
active_workflows: [],
|
|
preferenceDefinitions: [],
|
|
accountPreferences: {},
|
|
devicePreferences: {},
|
|
preferencesLoaded: false,
|
|
},
|
|
mutations: {
|
|
setInventoryItems(state, {url, items}) {
|
|
state.item_map[url] = items;
|
|
},
|
|
setFriends(state, friends) {
|
|
state.friends = friends;
|
|
},
|
|
setFriendProfile(state, {username, profile}) {
|
|
state.friendProfiles = {...state.friendProfiles, [username]: profile};
|
|
},
|
|
setFriendProfiles(state, profiles) {
|
|
state.friendProfiles = profiles;
|
|
},
|
|
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;
|
|
},
|
|
setStorageLocations(state, storage_locations) {
|
|
state.storage_locations = storage_locations;
|
|
},
|
|
setFiles(state, files) {
|
|
state.files = files;
|
|
},
|
|
setActiveWorkflows(state, workflows) {
|
|
state.active_workflows = workflows;
|
|
},
|
|
setPreferenceDefinitions(state, definitions) {
|
|
state.preferenceDefinitions = definitions;
|
|
},
|
|
setAccountPreferences(state, preferences) {
|
|
state.accountPreferences = preferences;
|
|
},
|
|
setDevicePreferences(state, preferences) {
|
|
state.devicePreferences = preferences;
|
|
},
|
|
setAccountPreference(state, {key, value}) {
|
|
state.accountPreferences = {...state.accountPreferences, [key]: value};
|
|
},
|
|
setDevicePreference(state, {key, value}) {
|
|
state.devicePreferences = {...state.devicePreferences, [key]: value};
|
|
},
|
|
deleteAccountPreference(state, key) {
|
|
const prefs = {...state.accountPreferences};
|
|
delete prefs[key];
|
|
state.accountPreferences = prefs;
|
|
},
|
|
deleteDevicePreference(state, key) {
|
|
const prefs = {...state.devicePreferences};
|
|
delete prefs[key];
|
|
state.devicePreferences = prefs;
|
|
},
|
|
setPreferencesLoaded(state, loaded) {
|
|
state.preferencesLoaded = loaded;
|
|
},
|
|
setUser(state, user) {
|
|
state.user = user;
|
|
if (state.remember)
|
|
localStorage.setItem('user', user);
|
|
},
|
|
setUserProfile(state, profile) {
|
|
state.user_profile = profile;
|
|
},
|
|
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);
|
|
},
|
|
setUserFromIdentityRecord(state, {user, keypair}) {
|
|
try {
|
|
localStorage.removeItem('user');
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('keypair');
|
|
state.user = user;
|
|
state.keypair = keypair;
|
|
if (state.remember) {
|
|
localStorage.setItem('user', user);
|
|
localStorage.setItem('keypair', key)
|
|
}
|
|
} catch (e) {
|
|
console.error('Invalid identity record')
|
|
}
|
|
},
|
|
logout(state) {
|
|
state.user = null;
|
|
state.user_profile = null;
|
|
state.token = null;
|
|
state.keypair = null;
|
|
state.accountPreferences = {};
|
|
state.preferenceDefinitions = [];
|
|
state.preferencesLoaded = false;
|
|
// Note: devicePreferences are NOT cleared on logout (device-specific)
|
|
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 loadUserPreferences({state, commit, dispatch, getters}) {
|
|
if (state.preferencesLoaded) return
|
|
const deviceKey = 'toolshed.preferences.device'
|
|
commit('setDevicePreferences', parseStoredPreferences(deviceKey))
|
|
|
|
let definitions = defaultPreferenceDefinitions
|
|
let accountPreferences = {}
|
|
try {
|
|
const servers = await dispatch('getHomeServers')
|
|
definitions = await servers.get(getters.nullAuth, '/auth/preferences/') || defaultPreferenceDefinitions
|
|
accountPreferences = await servers.get(getters.signAuth, '/auth/self/preferences/') || {}
|
|
} catch (error) {
|
|
console.error('Failed to load user preferences:', error)
|
|
}
|
|
commit('setPreferenceDefinitions', definitions)
|
|
commit('setAccountPreferences', accountPreferences)
|
|
commit('setPreferencesLoaded', true)
|
|
},
|
|
async setAccountPreference({state, commit, dispatch, getters}, {key, value}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.put(getters.signAuth, '/auth/self/preferences/', {[key]: value})
|
|
commit('setAccountPreference', {key, value})
|
|
},
|
|
async resetAccountPreference({state, commit, dispatch, getters}, key) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.delete(getters.signAuth, '/auth/self/preferences/' + key + '/')
|
|
commit('deleteAccountPreference', key)
|
|
},
|
|
async setDevicePreference({state, commit}, {key, value}) {
|
|
commit('setDevicePreference', {key, value})
|
|
localStorage.setItem('toolshed.preferences.device', JSON.stringify(state.devicePreferences))
|
|
},
|
|
async resetDevicePreference({state, commit}, key) {
|
|
commit('deleteDevicePreference', key)
|
|
localStorage.setItem('toolshed.preferences.device', JSON.stringify(state.devicePreferences))
|
|
},
|
|
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)
|
|
await dispatch('fetchUserProfile', {force: true})
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
async fetchUserProfile({state, commit, dispatch, getters}, {force = false} = {}) {
|
|
if (!force && state.user_profile && state.last_load.user_profile > Date.now() - 1000 * 60) {
|
|
return state.user_profile
|
|
}
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.get(getters.signAuth, '/auth/user/')
|
|
commit('setUserProfile', data)
|
|
state.last_load.user_profile = Date.now()
|
|
return data
|
|
},
|
|
async updateUserProfilePicture({state, commit, dispatch, getters}, {file = null} = {}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const payload = file ? {profile_picture: {data: file.data, mime_type: file.mime_type}} : {profile_picture: null}
|
|
const data = await servers.patch(getters.signAuth, '/auth/user/', payload)
|
|
commit('setUserProfile', data)
|
|
state.last_load.user_profile = Date.now()
|
|
if (data.profile_picture) {
|
|
state.last_load.files = 0
|
|
}
|
|
return data
|
|
},
|
|
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(
|
|
// Must match what the browser actually puts in the Host header for
|
|
// the request this gets used to build (federation.js always signs
|
|
// and fetches "https://" + server + target) - it omits a :443 for
|
|
// the default HTTPS port, so keeping it here would make every
|
|
// signature check on the receiving end fail against the real request.
|
|
(answer) => answer.port === 443 ? answer.target : 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) {
|
|
try {
|
|
const s = await dispatch('lookupServer', {username: friend.username})
|
|
servers.add(new ServerSet(s, state.unreachable_neighbors))
|
|
} catch (e) {
|
|
// Don't let a single unresolvable/unreachable friend abort the whole
|
|
// search/federation lookup - just skip them and continue.
|
|
console.error('could not resolve server for friend', friend.username, e)
|
|
}
|
|
}
|
|
return servers
|
|
})().catch(e => {
|
|
// Don't cache a permanently-rejected promise - allow a retry next time.
|
|
state.all_friends_servers = null
|
|
throw e
|
|
})
|
|
commit('setAllFriendsServers', promise)
|
|
return promise
|
|
},
|
|
async getFriendServers({state, dispatch, commit}, {username}) {
|
|
return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
|
|
},
|
|
async fetchBackendVersion({dispatch, getters}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
return await servers.get(getters.nullAuth, '/api/version/')
|
|
},
|
|
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 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, commit, dispatch, getters}, {query}) {
|
|
const servers = await dispatch('getAllKnownServers')
|
|
const items = await servers.get(getters.signAuth, '/api/search/?query=' + query)
|
|
items.map(item => item.files.map(file => file.owner = item.owner))
|
|
commit('setInventoryItems', {url: '/search/' + query, items})
|
|
return items
|
|
},
|
|
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 fetchFriendProfile({commit, dispatch, getters}, {username}) {
|
|
try {
|
|
const servers = await dispatch('getFriendServers', {username});
|
|
// username here is already a full handle (username@domain), see FriendSerializer
|
|
const profile = await servers.get(getters.signAuth, '/auth/user/' + username + '/');
|
|
if (profile) {
|
|
commit('setFriendProfile', {username, profile});
|
|
}
|
|
return profile;
|
|
} catch (error) {
|
|
console.error(`Failed to fetch profile for ${username}:`, error);
|
|
return null;
|
|
}
|
|
},
|
|
async fetchForeignItem({dispatch, getters}, {owner, id}) {
|
|
try {
|
|
const servers = await dispatch('getFriendServers', {username: owner});
|
|
// owner here is a full handle (username@domain) - see the /api/inventory_items/<handle>/<id>/
|
|
// endpoint (toolshed/api/inventory.py get_shared_item), which looks the item up by owner rather
|
|
// than by requester, unlike the plain /api/inventory_items/ list/detail endpoints.
|
|
const item = await servers.get(getters.signAuth, '/api/inventory_items/' + owner + '/' + id + '/');
|
|
if (item && item.files) {
|
|
item.files.forEach(file => file.owner = item.owner)
|
|
}
|
|
return item;
|
|
} catch (error) {
|
|
console.error(`Failed to fetch item ${id} for ${owner}:`, error);
|
|
return null;
|
|
}
|
|
},
|
|
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 fetchFiles({state, commit, dispatch, getters}) {
|
|
if (state.last_load.files > Date.now() - 1000 * 60 * 60 * 24) {
|
|
return state.files
|
|
}
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.get(getters.signAuth, '/api/files/')
|
|
data.map(file => file.owner = state.user)
|
|
commit('setFiles', data)
|
|
state.last_load.files = Date.now()
|
|
return data
|
|
},
|
|
async fetchFilesByItem({state, commit, dispatch, getters}, {id}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
return await servers.get(getters.signAuth, '/api/item_files/' + id + '/')
|
|
},
|
|
async pushFile({state, dispatch, getters}, {item_id, file}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.post(getters.signAuth, '/api/item_files/' + item_id + '/', file)
|
|
if (data.name) {
|
|
data.owner = state.user
|
|
state.files.push(data)
|
|
return data
|
|
}
|
|
},
|
|
async deleteFile({state, dispatch, getters}, {id}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.delete(getters.signAuth, '/api/files/' + id + '/')
|
|
state.files = state.files.filter(file => file.id !== id)
|
|
},
|
|
async deleteItemFile({state, dispatch, getters}, {item_id, file_id}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.delete(getters.signAuth, '/api/item_files/' + item_id + '/' + file_id + '/')
|
|
state.files = state.files.filter(file => file.id !== file_id)
|
|
},
|
|
async stageFile({state, dispatch, getters}, {lifetime_id, file}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.post(getters.signAuth, '/api/staged_files/' + lifetime_id + '/', file)
|
|
if (data.hash) {
|
|
return data.hash
|
|
}
|
|
},
|
|
async unstageFile({state, dispatch, getters}, {lifetime_id, file_hash}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.delete(getters.signAuth, '/api/staged_files/' + lifetime_id + '/' + file_hash + '/')
|
|
},
|
|
async commitStagedFile({state, dispatch, getters}, {item_id, file_hash}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.post(getters.signAuth, '/api/item_files/' + item_id + '/', {file_hash})
|
|
if (data.name) {
|
|
data.owner = state.user
|
|
state.files.push(data)
|
|
return data
|
|
}
|
|
},
|
|
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 fetchStorageLocations({state, commit, dispatch, getters}) {
|
|
if (state.last_load.storage_locations > Date.now() - 1000 * 60 * 60 * 24) {
|
|
return state.storage_locations
|
|
}
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.get(getters.signAuth, '/api/storage_locations/')
|
|
commit('setStorageLocations', data)
|
|
state.last_load.storage_locations = Date.now()
|
|
return data
|
|
},
|
|
async deleteStorageLocation({state, dispatch, getters}, location) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const ret = await servers.delete(getters.signAuth, '/api/storage_locations/' + location.id + '/')
|
|
dispatch('fetchStorageLocations')
|
|
return ret
|
|
},
|
|
async createStorageLocation({state, dispatch, getters}, location) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = {...location}
|
|
if (data.parent === '') data.parent = null
|
|
const reply = await servers.post(getters.signAuth, '/api/storage_locations/', data)
|
|
state.last_load.storage_locations = 0
|
|
return reply
|
|
},
|
|
async updateStorageLocation({state, dispatch, getters}, location) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = {...location}
|
|
if (data.parent === '') data.parent = null
|
|
const reply = await servers.patch(getters.signAuth, '/api/storage_locations/' + location.id + '/', data)
|
|
dispatch('fetchStorageLocations')
|
|
return reply
|
|
},
|
|
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
|
|
},
|
|
async userIdentityRecord({state}, {password}) {
|
|
return await serializeIdentityRecord(state.user, state.keypair, password);
|
|
},
|
|
async fetchActiveWorkflows({state, commit, dispatch, getters}) {
|
|
if (state.last_load.active_workflows > Date.now() - 1000 * 60 * 5) { // Cache for 5 minutes
|
|
return state.active_workflows
|
|
}
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.get(getters.signAuth, '/api/workflows/')
|
|
const workflows = data.map(deserializeWorkflowPayload)
|
|
commit('setActiveWorkflows', workflows)
|
|
state.last_load.active_workflows = Date.now()
|
|
return workflows
|
|
},
|
|
async createWorkflow({state, commit, dispatch, getters}, workflowData) {
|
|
const servers = await dispatch('getHomeServers')
|
|
// The backend stores `payload` as an opaque string - the frontend is
|
|
// responsible for serializing/deserializing the JSON itself.
|
|
const data = await servers.post(getters.signAuth, '/api/workflows/', serializeWorkflowPayload(workflowData))
|
|
state.last_load.active_workflows = 0 // Invalidate cache
|
|
return deserializeWorkflowPayload(data)
|
|
},
|
|
async updateWorkflow({state, commit, dispatch, getters}, workflow) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.patch(getters.signAuth, '/api/workflows/' + workflow.id + '/', serializeWorkflowPayload(workflow))
|
|
state.last_load.active_workflows = 0 // Invalidate cache
|
|
return deserializeWorkflowPayload(data)
|
|
},
|
|
async updateWorkflowStep({state, commit, dispatch, getters}, {workflowId, currentStep, payload}) {
|
|
const servers = await dispatch('getHomeServers')
|
|
const data = await servers.post(getters.signAuth, '/api/workflows/' + workflowId + '/update_step/', {
|
|
current_step: currentStep,
|
|
payload: JSON.stringify(payload)
|
|
})
|
|
state.last_load.active_workflows = 0 // Invalidate cache
|
|
return deserializeWorkflowPayload(data)
|
|
},
|
|
async deleteWorkflow({state, commit, dispatch, getters}, workflowId) {
|
|
const servers = await dispatch('getHomeServers')
|
|
await servers.delete(getters.signAuth, '/api/workflows/' + workflowId + '/')
|
|
state.last_load.active_workflows = 0 // Invalidate cache
|
|
return true
|
|
}
|
|
},
|
|
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 || state.keypair !== null)
|
|
},
|
|
allUserProfiles(state) {
|
|
// Combine user's own profile with all friend profiles
|
|
const combined = {...state.friendProfiles};
|
|
if (state.user && state.user_profile) {
|
|
combined[state.user] = state.user_profile;
|
|
}
|
|
return combined;
|
|
},
|
|
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
|
|
}]
|
|
},
|
|
getPreference: (state) => (key, fallbackDefault = null) => {
|
|
if (Object.prototype.hasOwnProperty.call(state.devicePreferences, key) && state.devicePreferences[key] !== null) {
|
|
return state.devicePreferences[key]
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(state.accountPreferences, key) && state.accountPreferences[key] !== null) {
|
|
return state.accountPreferences[key]
|
|
}
|
|
const definition = state.preferenceDefinitions.find((pref) => pref.key === key)
|
|
if (definition) {
|
|
return definition.default
|
|
}
|
|
return fallbackDefault
|
|
},
|
|
/**
|
|
* Extracts the human-readable name from a fully qualified handle.
|
|
* Handles look like "git:tools#tag:drill" or "git:base#property:length".
|
|
* If the given value does not look like a handle, it is returned unchanged.
|
|
*/
|
|
getNameFromHandle: () => (handle) => {
|
|
if (typeof handle !== 'string') {
|
|
return handle;
|
|
}
|
|
const match = handle.match(/#(?:tag|property):(.+)$/);
|
|
return match ? match[1] : handle;
|
|
},
|
|
}
|
|
}) |