mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat: add designating a survivor in shared vault (#841)
* feat: add designating a survivor in shared vault * add designated survivor property to http representation * fix: specs * fix: more specs * fix: another spec fix * fix: yet another spec fix
This commit is contained in:
@@ -3,6 +3,8 @@ module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$',
|
||||
testTimeout: 20000,
|
||||
coverageReporters: ['text-summary'],
|
||||
reporters: ['summary'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 100,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
|
||||
import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
|
||||
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
|
||||
@@ -42,4 +42,19 @@ export class SharedVaultUsersController extends BaseHttpController {
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/:userUuid/designate-survivor')
|
||||
async designateSurvivor(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
'POST',
|
||||
'shared-vaults/:sharedVaultUuid/users/:userUuid/designate-survivor',
|
||||
request.params.sharedVaultUuid,
|
||||
request.params.userUuid,
|
||||
),
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Shared Vault Users Controller
|
||||
['[GET]:shared-vaults/:sharedVaultUuid/users', 'sync.shared-vault-users.get-users'],
|
||||
['[DELETE]:shared-vaults/:sharedVaultUuid/users/:userUuid', 'sync.shared-vault-users.remove-user'],
|
||||
[
|
||||
'[POST]:shared-vaults/:sharedVaultUuid/users/:userUuid/designate-survivor',
|
||||
'sync.shared-vault-users.designate-survivor',
|
||||
],
|
||||
])
|
||||
|
||||
resolveEndpointOrMethodIdentifier(method: string, endpoint: string, ...params: string[]): string {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddDesignatedSurvivor1695283870612 implements MigrationInterface {
|
||||
name = 'AddDesignatedSurvivor1695283870612'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE `auth_shared_vault_users` ADD `is_designated_survivor` tinyint NOT NULL DEFAULT 0',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `auth_shared_vault_users` DROP COLUMN `is_designated_survivor`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddDesignatedSurvivor1695283961201 implements MigrationInterface {
|
||||
name = 'AddDesignatedSurvivor1695283961201'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "user_uuid_on_auth_shared_vault_users"')
|
||||
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_auth_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "temporary_auth_shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL, "is_designated_survivor" boolean NOT NULL DEFAULT (0))',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "temporary_auth_shared_vault_users"("uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp" FROM "auth_shared_vault_users"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "auth_shared_vault_users"')
|
||||
await queryRunner.query('ALTER TABLE "temporary_auth_shared_vault_users" RENAME TO "auth_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "user_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("user_uuid") ',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "shared_vault_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("shared_vault_uuid") ',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_auth_shared_vault_users"')
|
||||
await queryRunner.query('DROP INDEX "user_uuid_on_auth_shared_vault_users"')
|
||||
await queryRunner.query('ALTER TABLE "auth_shared_vault_users" RENAME TO "temporary_auth_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "auth_shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "auth_shared_vault_users"("uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp" FROM "temporary_auth_shared_vault_users"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "temporary_auth_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "shared_vault_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("shared_vault_uuid") ',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "user_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("user_uuid") ',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,8 @@ import { AddSharedVaultUser } from '../Domain/UseCase/AddSharedVaultUser/AddShar
|
||||
import { RemoveSharedVaultUser } from '../Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser'
|
||||
import { UserAddedToSharedVaultEventHandler } from '../Domain/Handler/UserAddedToSharedVaultEventHandler'
|
||||
import { UserRemovedFromSharedVaultEventHandler } from '../Domain/Handler/UserRemovedFromSharedVaultEventHandler'
|
||||
import { DesignateSurvivor } from '../Domain/UseCase/DesignateSurvivor/DesignateSurvivor'
|
||||
import { UserDesignatedAsSurvivorInSharedVaultEventHandler } from '../Domain/Handler/UserDesignatedAsSurvivorInSharedVaultEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -957,6 +959,14 @@ export class ContainerConfigLoader {
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DesignateSurvivor>(TYPES.Auth_DesignateSurvivor)
|
||||
.toConstantValue(
|
||||
new DesignateSurvivor(
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
),
|
||||
)
|
||||
|
||||
// Controller
|
||||
container
|
||||
@@ -1122,6 +1132,16 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<UserDesignatedAsSurvivorInSharedVaultEventHandler>(
|
||||
TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler,
|
||||
)
|
||||
.toConstantValue(
|
||||
new UserDesignatedAsSurvivorInSharedVaultEventHandler(
|
||||
container.get<DesignateSurvivor>(TYPES.Auth_DesignateSurvivor),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
|
||||
@@ -1156,6 +1176,10 @@ export class ContainerConfigLoader {
|
||||
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
|
||||
['USER_ADDED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserAddedToSharedVaultEventHandler)],
|
||||
['USER_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Auth_UserRemovedFromSharedVaultEventHandler)],
|
||||
[
|
||||
'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
|
||||
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
|
||||
],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
|
||||
@@ -161,6 +161,7 @@ const TYPES = {
|
||||
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
|
||||
Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
|
||||
Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
|
||||
Auth_DesignateSurvivor: Symbol.for('Auth_DesignateSurvivor'),
|
||||
// Handlers
|
||||
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
@@ -192,6 +193,9 @@ const TYPES = {
|
||||
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
|
||||
Auth_UserAddedToSharedVaultEventHandler: Symbol.for('Auth_UserAddedToSharedVaultEventHandler'),
|
||||
Auth_UserRemovedFromSharedVaultEventHandler: Symbol.for('Auth_UserRemovedFromSharedVaultEventHandler'),
|
||||
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
|
||||
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
|
||||
),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DomainEventHandlerInterface, UserDesignatedAsSurvivorInSharedVaultEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { DesignateSurvivor } from '../UseCase/DesignateSurvivor/DesignateSurvivor'
|
||||
|
||||
export class UserDesignatedAsSurvivorInSharedVaultEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private designateSurvivorUseCase: DesignateSurvivor,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: UserDesignatedAsSurvivorInSharedVaultEvent): Promise<void> {
|
||||
const result = await this.designateSurvivorUseCase.execute({
|
||||
sharedVaultUuid: event.payload.sharedVaultUuid,
|
||||
userUuid: event.payload.userUuid,
|
||||
timestamp: event.payload.timestamp,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed designate survivor for user ${event.payload.userUuid} and shared vault ${
|
||||
event.payload.sharedVaultUuid
|
||||
}: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { SharedVaultUser, Uuid } from '@standardnotes/domain-core'
|
||||
export interface SharedVaultUserRepositoryInterface {
|
||||
findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
|
||||
findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]>
|
||||
findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null>
|
||||
save(sharedVaultUser: SharedVaultUser): Promise<void>
|
||||
remove(sharedVault: SharedVaultUser): Promise<void>
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export class AddSharedVaultUser implements UseCaseInterface<void> {
|
||||
sharedVaultUuid,
|
||||
permission,
|
||||
timestamps,
|
||||
isDesignatedSurvivor: false,
|
||||
})
|
||||
if (sharedVaultUserOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUserOrError.getError())
|
||||
|
||||
@@ -90,6 +90,7 @@ describe('CreateCrossServiceToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { DesignateSurvivor } from './DesignateSurvivor'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
|
||||
|
||||
describe('DesignateSurvivor', () => {
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let timer: TimerInterface
|
||||
|
||||
const createUseCase = () => new DesignateSurvivor(sharedVaultUserRepository, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockReturnValue(null)
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(sharedVaultUser)
|
||||
sharedVaultUserRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should fail if shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: 'invalid',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: 'invalid',
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if shared vault user is not found', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should designate a survivor if the user is a member', async () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(sharedVaultUser)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
|
||||
expect(sharedVaultUserRepository.save).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should designate a survivor if the user is a member and there is already a survivor', async () => {
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockReturnValue(
|
||||
SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: true,
|
||||
}).getValue(),
|
||||
)
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(sharedVaultUser)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
|
||||
expect(sharedVaultUserRepository.save).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should fail if the timestamp is older than the existing survivor', async () => {
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockReturnValue(
|
||||
SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: true,
|
||||
}).getValue(),
|
||||
)
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(sharedVaultUser)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 122,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should do nothing if the user is already a survivor', async () => {
|
||||
sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockReturnValue(
|
||||
SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: true,
|
||||
}).getValue(),
|
||||
)
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(sharedVaultUser)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
timestamp: 200,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
|
||||
|
||||
export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DesignateSurvivorDTO): Promise<Result<void>> {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const existingSurvivor =
|
||||
await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid)
|
||||
|
||||
if (existingSurvivor) {
|
||||
if (existingSurvivor.props.timestamps.updatedAt > dto.timestamp) {
|
||||
return Result.fail(
|
||||
'Cannot designate survivor to a previous version of the shared vault. Most probably a race condition.',
|
||||
)
|
||||
}
|
||||
if (existingSurvivor.props.userUuid.value === userUuid.value) {
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
existingSurvivor.props.isDesignatedSurvivor = false
|
||||
existingSurvivor.props.timestamps = Timestamps.create(
|
||||
existingSurvivor.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultUserRepository.save(existingSurvivor)
|
||||
}
|
||||
|
||||
const toBeDesignatedAsASurvivor = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
|
||||
userUuid,
|
||||
sharedVaultUuid,
|
||||
})
|
||||
if (!toBeDesignatedAsASurvivor) {
|
||||
return Result.fail('User is not a member of the shared vault')
|
||||
}
|
||||
|
||||
toBeDesignatedAsASurvivor.props.isDesignatedSurvivor = true
|
||||
toBeDesignatedAsASurvivor.props.timestamps = Timestamps.create(
|
||||
toBeDesignatedAsASurvivor.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultUserRepository.save(toBeDesignatedAsASurvivor)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface DesignateSurvivorDTO {
|
||||
sharedVaultUuid: string
|
||||
userUuid: string
|
||||
timestamp: number
|
||||
}
|
||||
@@ -26,6 +26,13 @@ export class TypeORMSharedVaultUser {
|
||||
})
|
||||
declare permission: string
|
||||
|
||||
@Column({
|
||||
name: 'is_designated_survivor',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
declare isDesignatedSurvivor: boolean
|
||||
|
||||
@Column({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
|
||||
@@ -10,6 +10,24 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
|
||||
private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
|
||||
) {}
|
||||
|
||||
async findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('shared_vault_user')
|
||||
.where('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
})
|
||||
.andWhere('shared_vault_user.is_designated_survivor = :isDesignatedSurvivor', {
|
||||
isDesignatedSurvivor: true,
|
||||
})
|
||||
.getOne()
|
||||
|
||||
if (persistence === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.mapper.toDomain(persistence)
|
||||
}
|
||||
|
||||
async findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('shared_vault_user')
|
||||
|
||||
@@ -41,6 +41,7 @@ export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedV
|
||||
sharedVaultUuid,
|
||||
permission,
|
||||
timestamps,
|
||||
isDesignatedSurvivor: !!projection.isDesignatedSurvivor,
|
||||
},
|
||||
new UniqueEntityId(projection.uuid),
|
||||
)
|
||||
@@ -61,6 +62,7 @@ export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedV
|
||||
typeorm.permission = domain.props.permission.value
|
||||
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
|
||||
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
|
||||
typeorm.isDesignatedSurvivor = !!domain.props.isDesignatedSurvivor
|
||||
|
||||
return typeorm
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('SharedVaultUser', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123456789, 123456789).getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
})
|
||||
|
||||
expect(entityOrError.isFailed()).toBeFalsy()
|
||||
|
||||
@@ -6,5 +6,6 @@ export interface SharedVaultUserProps {
|
||||
sharedVaultUuid: Uuid
|
||||
userUuid: Uuid
|
||||
permission: SharedVaultUserPermission
|
||||
isDesignatedSurvivor: boolean
|
||||
timestamps: Timestamps
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { UserDesignatedAsSurvivorInSharedVaultEventPayload } from './UserDesignatedAsSurvivorInSharedVaultEventPayload'
|
||||
|
||||
export interface UserDesignatedAsSurvivorInSharedVaultEvent extends DomainEventInterface {
|
||||
type: 'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT'
|
||||
payload: UserDesignatedAsSurvivorInSharedVaultEventPayload
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface UserDesignatedAsSurvivorInSharedVaultEventPayload {
|
||||
userUuid: string
|
||||
sharedVaultUuid: string
|
||||
timestamp: number
|
||||
}
|
||||
@@ -104,6 +104,8 @@ export * from './Event/TransitionStatusUpdatedEvent'
|
||||
export * from './Event/TransitionStatusUpdatedEventPayload'
|
||||
export * from './Event/UserAddedToSharedVaultEvent'
|
||||
export * from './Event/UserAddedToSharedVaultEventPayload'
|
||||
export * from './Event/UserDesignatedAsSurvivorInSharedVaultEvent'
|
||||
export * from './Event/UserDesignatedAsSurvivorInSharedVaultEventPayload'
|
||||
export * from './Event/UserDisabledSessionUserAgentLoggingEvent'
|
||||
export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload'
|
||||
export * from './Event/UserEmailChangedEvent'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddDesignatedSurvivor1695284084365 implements MigrationInterface {
|
||||
name = 'AddDesignatedSurvivor1695284084365'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vault_users` ADD `is_designated_survivor` tinyint NOT NULL DEFAULT 0')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vault_users` DROP COLUMN `is_designated_survivor`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddDesignatedSurvivor1695284084365 implements MigrationInterface {
|
||||
name = 'AddDesignatedSurvivor1695284084365'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vault_users` ADD `is_designated_survivor` tinyint NOT NULL DEFAULT 0')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `shared_vault_users` DROP COLUMN `is_designated_survivor`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddDesignatedSurvivor1695284249461 implements MigrationInterface {
|
||||
name = 'AddDesignatedSurvivor1695284249461'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "user_uuid_on_shared_vault_users"')
|
||||
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "temporary_shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL, "is_designated_survivor" boolean NOT NULL DEFAULT (0))',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "temporary_shared_vault_users"("uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp" FROM "shared_vault_users"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "shared_vault_users"')
|
||||
await queryRunner.query('ALTER TABLE "temporary_shared_vault_users" RENAME TO "shared_vault_users"')
|
||||
await queryRunner.query('CREATE INDEX "user_uuid_on_shared_vault_users" ON "shared_vault_users" ("user_uuid") ')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "shared_vault_uuid_on_shared_vault_users" ON "shared_vault_users" ("shared_vault_uuid") ',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_users"')
|
||||
await queryRunner.query('DROP INDEX "user_uuid_on_shared_vault_users"')
|
||||
await queryRunner.query('ALTER TABLE "shared_vault_users" RENAME TO "temporary_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
|
||||
)
|
||||
await queryRunner.query(
|
||||
'INSERT INTO "shared_vault_users"("uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp") SELECT "uuid", "shared_vault_uuid", "user_uuid", "permission", "created_at_timestamp", "updated_at_timestamp" FROM "temporary_shared_vault_users"',
|
||||
)
|
||||
await queryRunner.query('DROP TABLE "temporary_shared_vault_users"')
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "shared_vault_uuid_on_shared_vault_users" ON "shared_vault_users" ("shared_vault_uuid") ',
|
||||
)
|
||||
await queryRunner.query('CREATE INDEX "user_uuid_on_shared_vault_users" ON "shared_vault_users" ("user_uuid") ')
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,7 @@ import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionReq
|
||||
import { DeleteSharedVaults } from '../Domain/UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults'
|
||||
import { RemoveItemsFromSharedVault } from '../Domain/UseCase/SharedVaults/RemoveItemsFromSharedVault/RemoveItemsFromSharedVault'
|
||||
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
|
||||
import { DesignateSurvivor } from '../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -865,6 +866,16 @@ export class ContainerConfigLoader {
|
||||
: container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DesignateSurvivor>(TYPES.Sync_DesignateSurvivor)
|
||||
.toConstantValue(
|
||||
new DesignateSurvivor(
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container
|
||||
|
||||
@@ -87,6 +87,7 @@ const TYPES = {
|
||||
),
|
||||
Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
|
||||
Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
|
||||
Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SharedVaultRemovedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
UserAddedToSharedVaultEvent,
|
||||
UserDesignatedAsSurvivorInSharedVaultEvent,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
UserRemovedFromSharedVaultEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
@@ -22,6 +23,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createUserDesignatedAsSurvivorInSharedVaultEvent(dto: {
|
||||
sharedVaultUuid: string
|
||||
userUuid: string
|
||||
timestamp: number
|
||||
}): UserDesignatedAsSurvivorInSharedVaultEvent {
|
||||
return {
|
||||
type: 'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createSharedVaultRemovedEvent(dto: { sharedVaultUuid: string }): SharedVaultRemovedEvent {
|
||||
return {
|
||||
type: 'SHARED_VAULT_REMOVED',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SharedVaultRemovedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
UserAddedToSharedVaultEvent,
|
||||
UserDesignatedAsSurvivorInSharedVaultEvent,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
UserRemovedFromSharedVaultEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
@@ -102,4 +103,9 @@ export interface DomainEventFactoryInterface {
|
||||
userUuid: string
|
||||
}): ItemRemovedFromSharedVaultEvent
|
||||
createSharedVaultRemovedEvent(dto: { sharedVaultUuid: string }): SharedVaultRemovedEvent
|
||||
createUserDesignatedAsSurvivorInSharedVaultEvent(dto: {
|
||||
sharedVaultUuid: string
|
||||
userUuid: string
|
||||
timestamp: number
|
||||
}): UserDesignatedAsSurvivorInSharedVaultEvent
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
|
||||
@@ -329,6 +330,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
@@ -489,6 +491,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
@@ -649,6 +652,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
@@ -734,6 +738,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
@@ -802,6 +807,7 @@ describe('SharedVaultFilter', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
itemHash = ItemHash.create({
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('AddNotificationsForUsers', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
|
||||
@@ -63,6 +63,7 @@ export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
|
||||
sharedVaultUuid,
|
||||
permission,
|
||||
timestamps,
|
||||
isDesignatedSurvivor: false,
|
||||
})
|
||||
if (sharedVaultUserOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUserOrError.getError())
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
@@ -115,6 +116,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
@@ -140,6 +142,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
@@ -148,6 +151,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
})
|
||||
@@ -203,6 +207,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(null)
|
||||
@@ -230,6 +235,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
@@ -238,6 +244,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
@@ -281,6 +288,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
@@ -289,6 +297,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
@@ -315,6 +324,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
@@ -323,6 +333,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
@@ -349,6 +360,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
@@ -357,6 +369,7 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('DeleteSharedVault', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser])
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { DesignateSurvivor } from './DesignateSurvivor'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('DesignateSurvivor', () => {
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let sharedVaultOwner: SharedVaultUser
|
||||
let timer: TimerInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new DesignateSurvivor(sharedVaultUserRepository, timer, domainEventFactory, domainEventPublisher)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
|
||||
sharedVaultOwner = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUser = SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([])
|
||||
sharedVaultUserRepository.save = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createUserDesignatedAsSurvivorInSharedVaultEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should fail if shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: 'invalid',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: 'invalid',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if originator uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if shared vault user is not found', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner])
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if the originator is not the admin of the shared vault', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000003',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should designate a survivor if the user is a member', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
|
||||
expect(sharedVaultUserRepository.save).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should designate a survivor if the user is a member and there is already a survivor', async () => {
|
||||
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([
|
||||
sharedVaultOwner,
|
||||
sharedVaultUser,
|
||||
SharedVaultUser.create({
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: true,
|
||||
}).getValue(),
|
||||
])
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000002',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
|
||||
expect(sharedVaultUserRepository.save).toBeCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import {
|
||||
Result,
|
||||
SharedVaultUser,
|
||||
SharedVaultUserPermission,
|
||||
Timestamps,
|
||||
UseCaseInterface,
|
||||
Uuid,
|
||||
} from '@standardnotes/domain-core'
|
||||
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class DesignateSurvivor implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DesignateSurvivorDTO): Promise<Result<void>> {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const originatorUuidOrError = Uuid.create(dto.originatorUuid)
|
||||
if (originatorUuidOrError.isFailed()) {
|
||||
return Result.fail(originatorUuidOrError.getError())
|
||||
}
|
||||
const originatorUuid = originatorUuidOrError.getValue()
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
let sharedVaultExistingSurvivor: SharedVaultUser | undefined
|
||||
let toBeDesignatedAsASurvivor: SharedVaultUser | undefined
|
||||
let isOriginatorTheOwner = false
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
if (sharedVaultUser.props.userUuid.equals(userUuid)) {
|
||||
toBeDesignatedAsASurvivor = sharedVaultUser
|
||||
}
|
||||
if (sharedVaultUser.props.isDesignatedSurvivor) {
|
||||
sharedVaultExistingSurvivor = sharedVaultUser
|
||||
}
|
||||
if (
|
||||
sharedVaultUser.props.userUuid.equals(originatorUuid) &&
|
||||
sharedVaultUser.props.permission.value === SharedVaultUserPermission.PERMISSIONS.Admin
|
||||
) {
|
||||
isOriginatorTheOwner = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOriginatorTheOwner) {
|
||||
return Result.fail('Only the owner can designate a survivor')
|
||||
}
|
||||
|
||||
if (!toBeDesignatedAsASurvivor) {
|
||||
return Result.fail('Attempting to designate a survivor for a non-member')
|
||||
}
|
||||
|
||||
if (sharedVaultExistingSurvivor) {
|
||||
sharedVaultExistingSurvivor.props.isDesignatedSurvivor = false
|
||||
sharedVaultExistingSurvivor.props.timestamps = Timestamps.create(
|
||||
sharedVaultExistingSurvivor.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
await this.sharedVaultUserRepository.save(sharedVaultExistingSurvivor)
|
||||
}
|
||||
|
||||
toBeDesignatedAsASurvivor.props.isDesignatedSurvivor = true
|
||||
toBeDesignatedAsASurvivor.props.timestamps = Timestamps.create(
|
||||
toBeDesignatedAsASurvivor.props.timestamps.createdAt,
|
||||
this.timer.getTimestampInMicroseconds(),
|
||||
).getValue()
|
||||
|
||||
await this.sharedVaultUserRepository.save(toBeDesignatedAsASurvivor)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserDesignatedAsSurvivorInSharedVaultEvent({
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
userUuid: userUuid.value,
|
||||
timestamp: this.timer.getTimestampInMicroseconds(),
|
||||
}),
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface DesignateSurvivorDTO {
|
||||
sharedVaultUuid: string
|
||||
userUuid: string
|
||||
originatorUuid: string
|
||||
}
|
||||
@@ -25,6 +25,7 @@ describe('GetSharedVaultUsers', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('GetSharedVaults', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVaultUser])
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('InviteUserToSharedVault', () => {
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('RemoveUserFromSharedVault', () => {
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
isDesignatedSurvivor: false,
|
||||
}).getValue()
|
||||
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { controller, httpDelete, httpGet, results } from 'inversify-express-utils'
|
||||
import { controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
|
||||
import { inject } from 'inversify'
|
||||
import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
|
||||
import { Request, Response } from 'express'
|
||||
@@ -8,16 +8,23 @@ import TYPES from '../../Bootstrap/Types'
|
||||
import { SharedVaultUserHttpRepresentation } from '../../Mapping/Http/SharedVaultUserHttpRepresentation'
|
||||
import { GetSharedVaultUsers } from '../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
|
||||
import { RemoveUserFromSharedVault } from '../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DesignateSurvivor } from '../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
|
||||
|
||||
@controller('/shared-vaults/:sharedVaultUuid/users', TYPES.Sync_AuthMiddleware)
|
||||
export class AnnotatedSharedVaultUsersController extends BaseSharedVaultUsersController {
|
||||
constructor(
|
||||
@inject(TYPES.Sync_GetSharedVaultUsers) override getSharedVaultUsersUseCase: GetSharedVaultUsers,
|
||||
@inject(TYPES.Sync_RemoveSharedVaultUser) override removeUserFromSharedVaultUseCase: RemoveUserFromSharedVault,
|
||||
@inject(TYPES.Sync_DesignateSurvivor) override designateSurvivorUseCase: DesignateSurvivor,
|
||||
@inject(TYPES.Sync_SharedVaultUserHttpMapper)
|
||||
override sharedVaultUserHttpMapper: MapperInterface<SharedVaultUser, SharedVaultUserHttpRepresentation>,
|
||||
) {
|
||||
super(getSharedVaultUsersUseCase, removeUserFromSharedVaultUseCase, sharedVaultUserHttpMapper)
|
||||
super(
|
||||
getSharedVaultUsersUseCase,
|
||||
removeUserFromSharedVaultUseCase,
|
||||
designateSurvivorUseCase,
|
||||
sharedVaultUserHttpMapper,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
@@ -29,4 +36,9 @@ export class AnnotatedSharedVaultUsersController extends BaseSharedVaultUsersCon
|
||||
override async removeUserFromSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.removeUserFromSharedVault(request, response)
|
||||
}
|
||||
|
||||
@httpPost('/:userUuid/designate-survivor')
|
||||
override async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.designateSurvivor(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import { ControllerContainerInterface, MapperInterface, SharedVaultUser } from '
|
||||
import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation'
|
||||
import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
|
||||
import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DesignateSurvivor } from '../../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
|
||||
|
||||
export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
constructor(
|
||||
protected getSharedVaultUsersUseCase: GetSharedVaultUsers,
|
||||
protected removeUserFromSharedVaultUseCase: RemoveUserFromSharedVault,
|
||||
protected designateSurvivorUseCase: DesignateSurvivor,
|
||||
protected sharedVaultUserHttpMapper: MapperInterface<SharedVaultUser, SharedVaultUserHttpRepresentation>,
|
||||
private controllerContainer?: ControllerContainerInterface,
|
||||
) {
|
||||
@@ -22,6 +24,7 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
'sync.shared-vault-users.remove-user',
|
||||
this.removeUserFromSharedVault.bind(this),
|
||||
)
|
||||
this.controllerContainer.register('sync.shared-vault-users.designate-survivor', this.designateSurvivor.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +74,27 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.designateSurvivorUseCase.execute({
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
userUuid: request.params.userUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: result.getError(),
|
||||
},
|
||||
},
|
||||
HttpStatusCode.BadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ export class TypeORMSharedVaultUser {
|
||||
})
|
||||
declare permission: string
|
||||
|
||||
@Column({
|
||||
name: 'is_designated_survivor',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
declare isDesignatedSurvivor: boolean
|
||||
|
||||
@Column({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
|
||||
@@ -13,6 +13,7 @@ export class SharedVaultUserHttpMapper implements MapperInterface<SharedVaultUse
|
||||
user_uuid: domain.props.userUuid.value,
|
||||
permission: domain.props.permission.value,
|
||||
shared_vault_uuid: domain.props.sharedVaultUuid.value,
|
||||
is_designated_survivor: domain.props.isDesignatedSurvivor,
|
||||
created_at_timestamp: domain.props.timestamps.createdAt,
|
||||
updated_at_timestamp: domain.props.timestamps.updatedAt,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface SharedVaultUserHttpRepresentation {
|
||||
shared_vault_uuid: string
|
||||
user_uuid: string
|
||||
permission: string
|
||||
is_designated_survivor: boolean
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedV
|
||||
sharedVaultUuid,
|
||||
permission,
|
||||
timestamps,
|
||||
isDesignatedSurvivor: !!projection.isDesignatedSurvivor,
|
||||
},
|
||||
new UniqueEntityId(projection.uuid),
|
||||
)
|
||||
@@ -61,6 +62,7 @@ export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedV
|
||||
typeorm.permission = domain.props.permission.value
|
||||
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
|
||||
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
|
||||
typeorm.isDesignatedSurvivor = !!domain.props.isDesignatedSurvivor
|
||||
|
||||
return typeorm
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user