Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/mapeo-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -705,19 +705,16 @@ export class MapeoManager extends TypedEmitter {
// If it does, that means the project has already been either created or added before
const projectId = projectKeyToId(projectKey)
const projectInviteId = projectKeyToProjectInviteId(projectKey)
/** @type {ProjectKeys['projectSecretKey']} */
let projectSecretKey = undefined

const projectExists = this.#db
const existingProject = this.#db
.select()
.from(projectKeysTable)
.where(
and(
eq(projectKeysTable.projectId, projectId),
eq(projectKeysTable.hasLeftProject, false)
)
)
.where(and(eq(projectKeysTable.projectId, projectId)))
.get()

if (projectExists) {
if (existingProject && existingProject.hasLeftProject !== true) {
throw new Error(`Project with ID ${projectPublicId} already exists`)
}

Expand All @@ -731,6 +728,14 @@ export class MapeoManager extends TypedEmitter {
await activeProject.close()
}

if (existingProject) {
const projectKeys = this.#decodeProjectKeysCipher(
existingProject.keysCipher,
projectId
)
projectSecretKey = projectKeys.projectSecretKey
}

// No awaits here - need to update table in same tick as the projectExists check

// 3. Update the project keys table
Expand All @@ -740,6 +745,7 @@ export class MapeoManager extends TypedEmitter {
projectInviteId,
projectKeys: {
projectKey,
projectSecretKey,
encryptionKeys,
},
projectInfo: {
Expand Down
21 changes: 13 additions & 8 deletions src/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,6 @@ export class Roles extends TypedEmitter {
// device that has not yet synced (so we do not yet have a replica of
// their authCore). In this case we want fromIndex to be 0
}
const isAssigningProjectCreatorRole =
authCoreId === this.#projectCreatorAuthCoreId
if (isAssigningProjectCreatorRole && !this.#isProjectCreator()) {
throw new Error(
"Only the project creator can assign the project creator's role"
)
}

if (roleId === LEFT_ROLE_ID) {
if (deviceId !== this.#ownDeviceId) {
throw new Error('Cannot assign LEFT role to another device')
Expand All @@ -423,6 +415,19 @@ export class Roles extends TypedEmitter {
}
)
} else {
const isAssigningProjectCreatorRole =
authCoreId === this.#projectCreatorAuthCoreId
const isAssigningSelf = deviceId === this.#ownDeviceId
if (
!isAssigningSelf &&
isAssigningProjectCreatorRole &&
roleId !== BLOCKED_ROLE_ID
) {
throw new Error(
'Project creators can only be assigned the blocked role'
)
}

await this.#dataType[kCreateWithDocId](deviceId, {
schemaName: 'role',
roleId,
Expand Down
53 changes: 52 additions & 1 deletion test-e2e/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { once } from 'node:events'
import {
COORDINATOR_ROLE_ID,
CREATOR_ROLE,
CREATOR_ROLE_ID,
ROLES,
MEMBER_ROLE_ID,
NO_ROLE,
BLOCKED_ROLE_ID,
CREATOR_ROLE_ID,
} from '../src/roles.js'
import {
connectPeers,
Expand Down Expand Up @@ -846,6 +846,57 @@ test('remove member from project, add them back', async (t) => {
assert.equal(reRole.roleId, MEMBER_ROLE_ID, 'Sees self as a member again')
})

test('remove creator from project, add them back', async (t) => {
const managers = await createManagers(2, t)
const [creator, coordinator] = managers
const disconnectPeers = connectPeers(managers)
t.after(disconnectPeers)

const projectId = await creator.createProject({ name: 'Mapeo' })

await invite({
invitor: creator,
projectId,
invitees: [coordinator],
roleId: COORDINATOR_ROLE_ID,
})

const coordinatorProject = await coordinator.getProject(projectId)
const creatorProject = await creator.getProject(projectId)

const onRoleChange = pEvent(creatorProject, 'own-role-change', {
timeout: 1_000,
})

await coordinatorProject.$member.remove(creator.deviceId)

await waitForSync([creatorProject, coordinatorProject], 'initial')

const updatedRole = await onRoleChange

assert.equal(
updatedRole.role.roleId,
BLOCKED_ROLE_ID,
'creator sees they were removed'
)

assert.equal(updatedRole.role.reason, undefined, 'No reason for removal')

await creator.leaveProject(projectId)
await creatorProject.close()

await invite({
invitor: coordinator,
projectId,
invitees: [creator],
})

const reinviteeProject = await creator.getProject(projectId)

const reRole = await reinviteeProject.$getOwnRole()
assert.equal(reRole.roleId, MEMBER_ROLE_ID, 'Sees self as a member again')
})

test('Auto deny invites if invited before remove is processed', async (t) => {
const managers = await createManagers(2, t)
const [invitor, invitee] = managers
Expand Down