Skip to content

Commit ceb1d7c

Browse files
RangerMauveMauve Signweaverachou11
authored
feat: remove server peer (#1017)
* feat: remove server peer * chore: Refactor based on PR review * chore: spelling in src/member-api.js Co-authored-by: Andrew Chou <[email protected]> --------- Co-authored-by: Mauve Signweaver <[email protected]> Co-authored-by: Andrew Chou <[email protected]>
1 parent 2c56d55 commit ceb1d7c

File tree

3 files changed

+99
-5
lines changed

3 files changed

+99
-5
lines changed

src/mapeo-project.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,9 @@ export class MapeoProject extends TypedEmitter {
410410
member.selfHostedServerDetails
411411
) {
412412
const { baseUrl } = member.selfHostedServerDetails
413-
const wsUrl = new URL(`/sync/${this.#projectPublicId}`, baseUrl)
414-
wsUrl.protocol = wsUrl.protocol === 'http:' ? 'ws:' : 'wss:'
415-
serverWebsocketUrls.push(wsUrl.href)
413+
serverWebsocketUrls.push(
414+
baseUrlToWS(baseUrl, this.#projectPublicId)
415+
)
416416
}
417417
}
418418
return serverWebsocketUrls
@@ -1086,3 +1086,16 @@ function mapAndValidateDeviceInfo(doc, { coreDiscoveryKey }) {
10861086
}
10871087
return doc
10881088
}
1089+
1090+
/**
1091+
*
1092+
* @param {string} baseUrl
1093+
* @param {string} projectPublicId
1094+
* @returns {string}
1095+
*/
1096+
export function baseUrlToWS(baseUrl, projectPublicId) {
1097+
const wsUrl = new URL(`/sync/${projectPublicId}`, baseUrl)
1098+
wsUrl.protocol = wsUrl.protocol === 'http:' ? 'ws:' : 'wss:'
1099+
1100+
return wsUrl.href
1101+
}

src/member-api.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import { isHostnameIpAddress } from './lib/is-hostname-ip-address.js'
1919
import { ErrorWithCode, getErrorMessage } from './lib/error.js'
2020
import { InviteAbortedError } from './errors.js'
2121
import { wsCoreReplicator } from './lib/ws-core-replicator.js'
22-
import { MEMBER_ROLE_ID, ROLES, isRoleIdForNewInvite } from './roles.js'
22+
import {
23+
BLOCKED_ROLE_ID,
24+
MEMBER_ROLE_ID,
25+
ROLES,
26+
isRoleIdForNewInvite,
27+
} from './roles.js'
2328
/**
2429
* @import {
2530
* DeviceInfo,
@@ -317,6 +322,46 @@ export class MemberApi extends TypedEmitter {
317322
})
318323
}
319324

325+
/**
326+
* Remove a server peer. Only works when the peer is reachable
327+
*
328+
* @param {string} serverDeviceId
329+
* @param {object} [options]
330+
* @param {boolean} [options.dangerouslyAllowInsecureConnections] Allow insecure network connections. Should only be used in tests.
331+
* @returns {Promise<void>}
332+
*/
333+
async removeServerPeer(
334+
serverDeviceId,
335+
{ dangerouslyAllowInsecureConnections = false } = {}
336+
) {
337+
// Get device ID for URL
338+
// Parse through URL to ensure end pathname if missing
339+
const member = await this.getById(serverDeviceId)
340+
341+
if (!member.selfHostedServerDetails) {
342+
throw new ErrorWithCode(
343+
'DEVICE_ID_NOT_FOR_SERVER',
344+
'DeviceId is not for a server peer'
345+
)
346+
}
347+
348+
if (member.role.roleId === BLOCKED_ROLE_ID) {
349+
throw new ErrorWithCode('ALREADY_BLOCKED', 'Server peer already blocked')
350+
}
351+
352+
const { baseUrl } = member.selfHostedServerDetails
353+
354+
// Add blocked role to project
355+
await this.#roles.assignRole(serverDeviceId, BLOCKED_ROLE_ID)
356+
357+
// TODO: Catch fail and sync with server after
358+
await this.#waitForInitialSyncWithServer({
359+
baseUrl,
360+
serverDeviceId,
361+
dangerouslyAllowInsecureConnections,
362+
})
363+
}
364+
320365
/**
321366
* @param {string} baseUrl Server base URL. Should already be validated.
322367
* @returns {Promise<{ serverDeviceId: string }>}
@@ -448,6 +493,7 @@ export class MemberApi extends TypedEmitter {
448493
result.name = deviceInfo.name
449494
result.deviceType = deviceInfo.deviceType
450495
result.joinedAt = deviceInfo.createdAt
496+
result.selfHostedServerDetails = deviceInfo.selfHostedServerDetails
451497
} catch (err) {
452498
// Attempting to get someone else may throw because sync hasn't occurred or completed
453499
// Only throw if attempting to get themself since the relevant information should be available

test-e2e/server.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import pDefer from 'p-defer'
1010
import { pEvent } from 'p-event'
1111
import RAM from 'random-access-memory'
1212
import { map } from 'iterpal'
13-
import { MEMBER_ROLE_ID } from '../src/roles.js'
13+
import { BLOCKED_ROLE_ID, MEMBER_ROLE_ID } from '../src/roles.js'
1414
import comapeoServer from '@comapeo/cloud'
1515
import {
1616
connectPeers,
@@ -372,6 +372,41 @@ test('data can be synced via a server', async (t) => {
372372
)
373373
})
374374

375+
test('add server, remove server, check that it knows it got blocked', async (t) => {
376+
const manager = createManager('seed', t)
377+
await manager.setDeviceInfo({ name: 'manager', deviceType: 'mobile' })
378+
379+
// Because we need to stop the server, we can't use a remote server here.
380+
const { server, serverBaseUrl } = await createLocalTestServer(t)
381+
t.after(() => server.close())
382+
383+
const projectId = await manager.createProject({ name: 'foo' })
384+
const project = await manager.getProject(projectId)
385+
await project.$member.addServerPeer(serverBaseUrl, {
386+
dangerouslyAllowInsecureConnections: true,
387+
})
388+
const serverPeer = await findServerPeer(project)
389+
assert(serverPeer, 'test setup: server peer exists')
390+
391+
await project.$member.removeServerPeer(serverPeer.deviceId, {
392+
dangerouslyAllowInsecureConnections: true,
393+
})
394+
395+
const serverMember = await findServerPeer(project)
396+
397+
assert.equal(serverMember?.role.roleId, BLOCKED_ROLE_ID, 'server now blocked')
398+
399+
// @ts-expect-error - does not exist on type
400+
const serverManager = /** @type {MapeoManager} */ server.comapeo
401+
const serverProject = await serverManager.getProject(projectId)
402+
const serverMemberState = await findServerPeer(serverProject)
403+
assert.equal(
404+
serverMemberState?.role.roleId,
405+
BLOCKED_ROLE_ID,
406+
'server knows it is blocked'
407+
)
408+
})
409+
375410
test('connecting and then immediately disconnecting (and then immediately connecting again)', async (t) => {
376411
const manager = createManager('seed', t)
377412
await manager.setDeviceInfo({ name: 'manager', deviceType: 'mobile' })

0 commit comments

Comments
 (0)