This commit is contained in:
j3d1 2026-08-24 15:57:17 +02:00
parent 8d96bc97c4
commit ed04d98bf1
54 changed files with 661 additions and 1214 deletions

View file

@ -329,22 +329,17 @@ export default createStore({
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.
// Must match the browser's real Host header (federation.js signs/fetches
// "https://"+server+target); browser omits :443 for default HTTPS, so
// keeping it here would break signature checks on the receiving end.
(answer) => answer.port === 443 ? answer.target : answer.target + ':' + answer.port))
},
async getHomeServers({state, dispatch, commit, getters}) {
if (state.home_servers)
return state.home_servers
// isLoggedIn (store.js's getters) is what lazily hydrates state.user/token/keypair
// from localStorage on first read - a route with no requiresAuth meta (e.g. the
// short-id redirect) never triggers that beforeEach check, so state.user can still be
// null here even for an actually-logged-in visitor. Reading the getter first forces
// that hydration; if it's still false afterwards, the visitor really isn't logged in,
// so fail with a clear error instead of lookupServer crashing on username.split(...).
// Reading isLoggedIn first forces its lazy hydration of state.user from localStorage,
// needed here since routes without requiresAuth (e.g. short-id redirect) skip that
// check; fail clearly if still not logged in rather than crashing on username.split.
if (!getters.isLoggedIn) {
throw new Error('Not logged in')
}
@ -365,8 +360,7 @@ export default createStore({
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.
// Skip an unresolvable/unreachable friend rather than aborting the whole lookup.
console.error('could not resolve server for friend', friend.username, e)
}
}
@ -442,9 +436,8 @@ export default createStore({
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.
// owner is a full handle (username@domain); this endpoint looks the item up by
// owner, not requester (see toolshed/api/inventory.py get_shared_item).
const item = await servers.get(getters.signAuth, '/api/inventory_items/' + owner + '/' + id + '/');
if (item && item.files) {
item.files.forEach(file => file.owner = item.owner)
@ -455,21 +448,12 @@ export default createStore({
return null;
}
},
// A group handle (leading '#') has no working owner-handle GET route yet
// (get_shared_item, which fetchForeignItem calls, only resolves a personal
// ToolshedUser handle) - resolve it instead via the already-correct, already-
// authenticated group listing (?group=<id>, see fetchGroupInventoryItems) and pick the
// matching item out of that, which only ever contains this one group's own items, so an
// id collision with anything else can't happen. A personal/friend handle still goes
// through fetchForeignItem as before.
// Group handles have no owner-handle GET route yet, so they resolve differently than
// personal handles here. See docs/implementation.md#fetch-item-by-handle-group-vs-personal-handles.
async fetchItemByHandle({dispatch, getters}, {handle, id}) {
if (handle.startsWith('#')) {
// groupIdByHandle is derived from state.idmap (see store.js's getters), which
// nothing guarantees is loaded yet at this point - unlike Inventory.vue/
// StorageLocation.vue/Print.vue, a direct or refreshed visit to an item's own
// detail/edit page never fetched it. Loading it here, every time, is simplest;
// fetchIdMap is cheap and already called unconditionally (no cache check) by
// every other caller too.
// idmap isn't guaranteed loaded on a direct/refreshed visit here; fetchIdMap is
// cheap and already called unconditionally by every other caller too.
await dispatch('fetchIdMap')
const groupId = getters.groupIdByHandle[handle]
if (groupId === undefined) {
@ -528,10 +512,8 @@ export default createStore({
return await servers.delete(getters.signAuth, '/api/friends/' + id + '/')
},
// Groups are only ever hosted on the current user's own home backend for now (see
// docs/design-in-progress/groups-mvp.md) - a remote member's edit/delete rights on a
// group-owned item work regardless, but "My Groups" has no way to discover a group hosted
// elsewhere, so every group action below talks to getHomeServers rather than resolving a
// per-group domain.
// docs/design-in-progress/groups-mvp.md), so every group action below uses getHomeServers
// rather than resolving a per-group domain.
async fetchGroups({commit, dispatch, getters}) {
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/groups/')
@ -769,8 +751,8 @@ export default createStore({
},
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.
// The backend stores payload as an opaque string; the frontend (de)serializes it.
// See docs/implementation.md#workflow-payload-is-an-opaque-string.
const data = await servers.post(getters.signAuth, '/api/workflows/', serializeWorkflowPayload(workflowData))
state.last_load.active_workflows = 0 // Invalidate cache
return deserializeWorkflowPayload(data)
@ -838,9 +820,8 @@ export default createStore({
groupIdByHandle(state) {
return Object.fromEntries(state.idmap.groups.map(g => [g.handle, g.id]))
},
// Reverse of the two getters above - turns a short-id's raw owner_identity_id/
// owner_group_id back into a handle (see router.js's EXPANDED_ROUTE_BUILDERS), without
// a separate backend lookup since the idmap already has both directions of this data.
// Reverse of the two getters above: turns a short-id's raw owner_identity_id/owner_group_id
// back into a handle (see router.js EXPANDED_ROUTE_BUILDERS), no backend lookup needed.
identityHandleById(state) {
return Object.fromEntries(state.idmap.identities.map(i => [i.id, i.username]))
},
@ -901,11 +882,7 @@ export default createStore({
}
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.
*/
/** Extracts the name from a handle like "git:tools#tag:drill"; returns non-handles unchanged. */
getNameFromHandle: () => (handle) => {
if (typeof handle !== 'string') {
return handle;