mirror of
https://github.com/standardnotes/server
synced 2026-01-19 11:04:28 -05:00
Compare commits
11 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12767237d2 | ||
|
|
230c96dcf1 | ||
|
|
e2696fcd1a | ||
|
|
a621cf1e3b | ||
|
|
db35b9fcab | ||
|
|
880db1038a | ||
|
|
414b090efb | ||
|
|
41e2136bc0 | ||
|
|
378ecedfcc | ||
|
|
06d4200909 | ||
|
|
22a8cc90f1 |
@@ -3,6 +3,8 @@ module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$',
|
||||
testTimeout: 20000,
|
||||
coverageReporters: ['text-summary'],
|
||||
reporters: ['summary'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 100,
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.26.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.19...@standardnotes/analytics@2.26.20) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.26.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.18...@standardnotes/analytics@2.26.19) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.26.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.26.17...@standardnotes/analytics@2.26.18) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.26.18",
|
||||
"version": "2.26.20",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.75.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.17...@standardnotes/api-gateway@1.75.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add designating a survivor in shared vault ([#841](https://github.com/standardnotes/api-gateway/issues/841)) ([230c96d](https://github.com/standardnotes/api-gateway/commit/230c96dcf1d8faed9ce8fe288549226da70317e6))
|
||||
|
||||
## [1.74.17](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.16...@standardnotes/api-gateway@1.74.17) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.74.16](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.74.15...@standardnotes/api-gateway@1.74.16) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.74.16",
|
||||
"version": "1.75.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.144.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.143.9...@standardnotes/auth-server@1.144.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add designating a survivor in shared vault ([#841](https://github.com/standardnotes/server/issues/841)) ([230c96d](https://github.com/standardnotes/server/commit/230c96dcf1d8faed9ce8fe288549226da70317e6))
|
||||
|
||||
## [1.143.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.143.8...@standardnotes/auth-server@1.143.9) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.143.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.143.7...@standardnotes/auth-server@1.143.8) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
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") ',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.143.8",
|
||||
"version": "1.144.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.32.0...@standardnotes/domain-core@1.33.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add designating a survivor in shared vault ([#841](https://github.com/standardnotes/server/issues/841)) ([230c96d](https://github.com/standardnotes/server/commit/230c96dcf1d8faed9ce8fe288549226da70317e6))
|
||||
|
||||
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.31.0...@standardnotes/domain-core@1.32.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** distinct notifications upon user removal from shared vault ([#840](https://github.com/standardnotes/server/issues/840)) ([41e2136](https://github.com/standardnotes/server/commit/41e2136bc07312974701a70652528d304105e0f9))
|
||||
|
||||
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.30.1...@standardnotes/domain-core@1.31.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.31.0",
|
||||
"version": "1.33.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,8 @@ import { NotificationTypeProps } from './NotificationTypeProps'
|
||||
export class NotificationType extends ValueObject<NotificationTypeProps> {
|
||||
static readonly TYPES = {
|
||||
SharedVaultItemRemoved: 'shared_vault_item_removed',
|
||||
RemovedFromSharedVault: 'removed_from_shared_vault',
|
||||
SelfRemovedFromSharedVault: 'self_removed_from_shared_vault',
|
||||
UserRemovedFromSharedVault: 'user_removed_from_shared_vault',
|
||||
UserAddedToSharedVault: 'user_added_to_shared_vault',
|
||||
SharedVaultInviteDeclined: 'shared_vault_invite_declined',
|
||||
SharedVaultFileUploaded: 'shared_vault_file_uploaded',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.32](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.31...@standardnotes/domain-events-infra@1.12.32) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.31](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.30...@standardnotes/domain-events-infra@1.12.31) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.30](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.29...@standardnotes/domain-events-infra@1.12.30) (2023-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.30",
|
||||
"version": "1.12.32",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.129.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.128.0...@standardnotes/domain-events@2.129.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add designating a survivor in shared vault ([#841](https://github.com/standardnotes/server/issues/841)) ([230c96d](https://github.com/standardnotes/server/commit/230c96dcf1d8faed9ce8fe288549226da70317e6))
|
||||
|
||||
# [2.128.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.127.0...@standardnotes/domain-events@2.128.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* add unassigning items and revisions upon shared vault removal ([#839](https://github.com/standardnotes/server/issues/839)) ([378eced](https://github.com/standardnotes/server/commit/378ecedfcc4fb23475c2329fb37479edb3b48a39))
|
||||
|
||||
# [2.127.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.126.0...@standardnotes/domain-events@2.127.0) (2023-09-15)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.127.0",
|
||||
"version": "2.129.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,4 @@ export interface ItemRemovedFromSharedVaultEventPayload {
|
||||
userUuid: string
|
||||
itemUuid: string
|
||||
sharedVaultUuid: string
|
||||
roleNames: string[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultRemovedEventPayload } from './SharedVaultRemovedEventPayload'
|
||||
|
||||
export interface SharedVaultRemovedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_REMOVED'
|
||||
payload: SharedVaultRemovedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SharedVaultRemovedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -76,6 +76,8 @@ export * from './Event/SharedVaultFileRemovedEvent'
|
||||
export * from './Event/SharedVaultFileRemovedEventPayload'
|
||||
export * from './Event/SharedVaultFileUploadedEvent'
|
||||
export * from './Event/SharedVaultFileUploadedEventPayload'
|
||||
export * from './Event/SharedVaultRemovedEvent'
|
||||
export * from './Event/SharedVaultRemovedEventPayload'
|
||||
export * from './Event/StatisticPersistenceRequestedEvent'
|
||||
export * from './Event/StatisticPersistenceRequestedEventPayload'
|
||||
export * from './Event/SubscriptionCancelledEvent'
|
||||
@@ -102,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'
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.48](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.47...@standardnotes/event-store@1.11.48) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.47](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.46...@standardnotes/event-store@1.11.47) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.46](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.45...@standardnotes/event-store@1.11.46) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.46",
|
||||
"version": "1.11.48",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.27](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.26...@standardnotes/files-server@1.22.27) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.26](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.25...@standardnotes/files-server@1.22.26) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.22.25](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.22.24...@standardnotes/files-server@1.22.25) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.22.25",
|
||||
"version": "1.22.27",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.15.74](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.73...@standardnotes/home-server@1.15.74) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.73](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.72...@standardnotes/home-server@1.15.73) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.72](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.71...@standardnotes/home-server@1.15.72) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.71](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.70...@standardnotes/home-server@1.15.71) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.70](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.69...@standardnotes/home-server@1.15.70) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.15.69](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.15.68...@standardnotes/home-server@1.15.69) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.15.69",
|
||||
"version": "1.15.74",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.36.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.2...@standardnotes/revisions-server@1.36.3) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.36.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.1...@standardnotes/revisions-server@1.36.2) (2023-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **revisions:** rename revisions table to all users stuck mid-migration process - fixes [#836](https://github.com/standardnotes/server/issues/836) ([#842](https://github.com/standardnotes/server/issues/842)) ([a621cf1](https://github.com/standardnotes/server/commit/a621cf1e3b891c450272e9762e4a71a199ea2932))
|
||||
|
||||
## [1.36.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.36.0...@standardnotes/revisions-server@1.36.1) (2023-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* secondary database catch up time ([880db10](https://github.com/standardnotes/server/commit/880db1038a39d4610a2593489a18e207487347a2))
|
||||
|
||||
# [1.36.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.35.8...@standardnotes/revisions-server@1.36.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* add unassigning items and revisions upon shared vault removal ([#839](https://github.com/standardnotes/server/issues/839)) ([378eced](https://github.com/standardnotes/server/commit/378ecedfcc4fb23475c2329fb37479edb3b48a39))
|
||||
|
||||
## [1.35.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.35.7...@standardnotes/revisions-server@1.35.8) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -4,6 +4,8 @@ export class removeDateIndexes1669636497932 implements MigrationInterface {
|
||||
name = 'removeDateIndexes1669636497932'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await this.renameRevisionsTable(queryRunner)
|
||||
|
||||
const indexRevisionsOnCreatedAt = await queryRunner.manager.query(
|
||||
'SHOW INDEX FROM `revisions_revisions` where `key_name` = "created_at"',
|
||||
)
|
||||
@@ -25,4 +27,14 @@ export class removeDateIndexes1669636497932 implements MigrationInterface {
|
||||
await queryRunner.query('CREATE INDEX `creation_date` ON `revisions_revisions` (`creation_date`)')
|
||||
await queryRunner.query('CREATE INDEX `created_at` ON `revisions_revisions` (`created_at`)')
|
||||
}
|
||||
|
||||
private async renameRevisionsTable(queryRunner: QueryRunner) {
|
||||
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
|
||||
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
|
||||
)
|
||||
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
|
||||
if (revisionsTableExists) {
|
||||
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,22 @@ export class makeUserUuidNullable1669735585016 implements MigrationInterface {
|
||||
name = 'makeUserUuidNullable1669735585016'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await this.renameRevisionsTable(queryRunner)
|
||||
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
|
||||
}
|
||||
|
||||
private async renameRevisionsTable(queryRunner: QueryRunner) {
|
||||
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
|
||||
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
|
||||
)
|
||||
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
|
||||
if (revisionsTableExists) {
|
||||
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddSharedVaultInformation1693915383950 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await this.renameRevisionsTable(queryRunner)
|
||||
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `edited_by` varchar(36) NULL')
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `shared_vault_uuid` varchar(36) NULL')
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `key_system_identifier` varchar(36) NULL')
|
||||
@@ -16,4 +18,14 @@ export class AddSharedVaultInformation1693915383950 implements MigrationInterfac
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` DROP COLUMN `shared_vault_uuid`')
|
||||
await queryRunner.query('ALTER TABLE `revisions_revisions` DROP COLUMN `last_edited_by`')
|
||||
}
|
||||
|
||||
private async renameRevisionsTable(queryRunner: QueryRunner) {
|
||||
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
|
||||
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
|
||||
)
|
||||
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
|
||||
if (revisionsTableExists) {
|
||||
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.35.8",
|
||||
"version": "1.36.3",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -67,6 +67,7 @@ import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevi
|
||||
import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
|
||||
import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler'
|
||||
import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionRequestedEventHandler'
|
||||
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -356,7 +357,9 @@ export class ContainerConfigLoader {
|
||||
.bind<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault)
|
||||
.toConstantValue(
|
||||
new RemoveRevisionsFromSharedVault(
|
||||
container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
|
||||
isSecondaryDatabaseEnabled
|
||||
? container.get<RevisionRepositoryInterface>(TYPES.Revisions_MongoDBRevisionRepository)
|
||||
: container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -448,6 +451,14 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultRemovedEventHandler>(TYPES.Revisions_SharedVaultRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultRemovedEventHandler(
|
||||
container.get<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault),
|
||||
container.get<winston.Logger>(TYPES.Revisions_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
|
||||
@@ -455,6 +466,7 @@ export class ContainerConfigLoader {
|
||||
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
|
||||
['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)],
|
||||
['TRANSITION_REQUESTED', container.get(TYPES.Revisions_TransitionRequestedEventHandler)],
|
||||
['SHARED_VAULT_REMOVED', container.get(TYPES.Revisions_SharedVaultRemovedEventHandler)],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
|
||||
@@ -57,6 +57,7 @@ const TYPES = {
|
||||
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
|
||||
Revisions_ItemRemovedFromSharedVaultEventHandler: Symbol.for('Revisions_ItemRemovedFromSharedVaultEventHandler'),
|
||||
Revisions_TransitionRequestedEventHandler: Symbol.for('Revisions_TransitionRequestedEventHandler'),
|
||||
Revisions_SharedVaultRemovedEventHandler: Symbol.for('Revisions_SharedVaultRemovedEventHandler'),
|
||||
// Services
|
||||
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
|
||||
Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -9,10 +9,15 @@ export class ItemRemovedFromSharedVaultEventHandler implements DomainEventHandle
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRemovedFromSharedVaultEvent): Promise<void> {
|
||||
if (!event.payload.itemUuid) {
|
||||
this.logger.error('ItemRemovedFromSharedVaultEvent is missing itemUuid')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.removeRevisionsFromSharedVault.execute({
|
||||
sharedVaultUuid: event.payload.sharedVaultUuid,
|
||||
itemUuid: event.payload.itemUuid,
|
||||
roleNames: event.payload.roleNames,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RemoveRevisionsFromSharedVault } from '../UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
|
||||
|
||||
export class SharedVaultRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private removeRevisionsFromSharedVault: RemoveRevisionsFromSharedVault,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SharedVaultRemovedEvent): Promise<void> {
|
||||
const result = await this.removeRevisionsFromSharedVault.execute({
|
||||
sharedVaultUuid: event.payload.sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to remove revisions from shared vault: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ export interface RevisionRepositoryInterface {
|
||||
updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
|
||||
findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
|
||||
insert(revision: Revision): Promise<boolean>
|
||||
clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void>
|
||||
clearSharedVaultAndKeySystemAssociations(dto: { itemUuid?: Uuid; sharedVaultUuid: Uuid }): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
|
||||
import { RemoveRevisionsFromSharedVault } from './RemoveRevisionsFromSharedVault'
|
||||
|
||||
describe('RemoveRevisionsFromSharedVault', () => {
|
||||
let revisionRepositoryResolver: RevisionRepositoryResolverInterface
|
||||
let revisionRepository: RevisionRepositoryInterface
|
||||
|
||||
const createUseCase = () => new RemoveRevisionsFromSharedVault(revisionRepositoryResolver)
|
||||
const createUseCase = () => new RemoveRevisionsFromSharedVault(revisionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
|
||||
revisionRepository.clearSharedVaultAndKeySystemAssociations = jest.fn()
|
||||
|
||||
revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
|
||||
revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
|
||||
})
|
||||
|
||||
it('should clear shared vault and key system associations', async () => {
|
||||
@@ -22,7 +17,16 @@ describe('RemoveRevisionsFromSharedVault', () => {
|
||||
await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(revisionRepository.clearSharedVaultAndKeySystemAssociations).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear shared vault and key system associations for all items in a vault when item uuid is not provided', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
|
||||
})
|
||||
|
||||
expect(revisionRepository.clearSharedVaultAndKeySystemAssociations).toHaveBeenCalled()
|
||||
@@ -34,7 +38,6 @@ describe('RemoveRevisionsFromSharedVault', () => {
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: 'invalid',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
@@ -46,19 +49,6 @@ describe('RemoveRevisionsFromSharedVault', () => {
|
||||
const result = await useCase.execute({
|
||||
itemUuid: 'invalid',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
|
||||
roleNames: ['CORE_USER'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return error when role names are invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
|
||||
roleNames: ['invalid'],
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { RemoveRevisionsFromSharedVaultDTO } from './RemoveRevisionsFromSharedVaultDTO'
|
||||
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
|
||||
|
||||
export class RemoveRevisionsFromSharedVault implements UseCaseInterface<void> {
|
||||
constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
|
||||
constructor(private revisionRepository: RevisionRepositoryInterface) {}
|
||||
|
||||
async execute(dto: RemoveRevisionsFromSharedVaultDTO): Promise<Result<void>> {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
@@ -12,21 +13,19 @@ export class RemoveRevisionsFromSharedVault implements UseCaseInterface<void> {
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
const itemUuidOrError = Uuid.create(dto.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return Result.fail(itemUuidOrError.getError())
|
||||
let itemUuid: Uuid | undefined
|
||||
if (dto.itemUuid !== undefined) {
|
||||
const itemUuidOrError = Uuid.create(dto.itemUuid)
|
||||
if (itemUuidOrError.isFailed()) {
|
||||
return Result.fail(itemUuidOrError.getError())
|
||||
}
|
||||
itemUuid = itemUuidOrError.getValue()
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
|
||||
if (roleNamesOrError.isFailed()) {
|
||||
return Result.fail(roleNamesOrError.getError())
|
||||
}
|
||||
const roleNames = roleNamesOrError.getValue()
|
||||
|
||||
const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
|
||||
|
||||
await revisionRepository.clearSharedVaultAndKeySystemAssociations(itemUuid, sharedVaultUuid)
|
||||
await this.revisionRepository.clearSharedVaultAndKeySystemAssociations({
|
||||
itemUuid,
|
||||
sharedVaultUuid,
|
||||
})
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface RemoveRevisionsFromSharedVaultDTO {
|
||||
itemUuid: string
|
||||
itemUuid?: string
|
||||
sharedVaultUuid: string
|
||||
roleNames: string[]
|
||||
}
|
||||
|
||||
@@ -59,8 +59,6 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
|
||||
const updatedRevisionsInSecondaryCount = updatedRevisionsInSecondary.length
|
||||
|
||||
await this.allowForSecondaryDatabaseToCatchUp()
|
||||
|
||||
const migrationTimeStart = this.timer.getTimestampInMicroseconds()
|
||||
|
||||
this.logger.info(`[${dto.userUuid}] Migrating revisions`)
|
||||
@@ -194,8 +192,8 @@ export class TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser implements
|
||||
}
|
||||
|
||||
private async allowForSecondaryDatabaseToCatchUp(): Promise<void> {
|
||||
const tenSecondsInMillisecondsToRebuildIndexes = 10_000
|
||||
await this.timer.sleep(tenSecondsInMillisecondsToRebuildIndexes)
|
||||
const twoSecondsInMilliseconds = 2_000
|
||||
await this.timer.sleep(twoSecondsInMilliseconds)
|
||||
}
|
||||
|
||||
private async hasAlreadyDataInSecondaryDatabase(userUuid: Uuid): Promise<boolean> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { MongoRepository } from 'typeorm'
|
||||
import { MongoRepository, ObjectLiteral } from 'typeorm'
|
||||
import { BSON } from 'mongodb'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@@ -16,19 +16,25 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
|
||||
await this.mongoRepository.updateMany(
|
||||
{
|
||||
itemUuid: { $eq: itemUuid.value },
|
||||
sharedVaultUuid: { $eq: sharedVaultUuid.value },
|
||||
async clearSharedVaultAndKeySystemAssociations(dto: { itemUuid?: Uuid; sharedVaultUuid: Uuid }): Promise<void> {
|
||||
let query: ObjectLiteral
|
||||
if (dto.itemUuid !== undefined) {
|
||||
query = {
|
||||
itemUuid: { $eq: dto.itemUuid.value },
|
||||
sharedVaultUuid: { $eq: dto.sharedVaultUuid.value },
|
||||
}
|
||||
} else {
|
||||
query = {
|
||||
sharedVaultUuid: { $eq: dto.sharedVaultUuid.value },
|
||||
}
|
||||
}
|
||||
|
||||
await this.mongoRepository.updateMany(query, {
|
||||
$set: {
|
||||
sharedVaultUuid: null,
|
||||
keySystemIdentifier: null,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
sharedVaultUuid: null,
|
||||
keySystemIdentifier: null,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async countByUserUuid(userUuid: Uuid): Promise<number> {
|
||||
|
||||
@@ -15,7 +15,7 @@ export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface
|
||||
protected logger: Logger,
|
||||
) {}
|
||||
|
||||
async clearSharedVaultAndKeySystemAssociations(_itemUuid: Uuid, _sharedVaultUuid: Uuid): Promise<void> {
|
||||
async clearSharedVaultAndKeySystemAssociations(_dto: { itemUuid?: Uuid; sharedVaultUuid: Uuid }): Promise<void> {
|
||||
this.logger.error('Method clearSharedVaultAndKeySystemAssociations not implemented.')
|
||||
}
|
||||
|
||||
|
||||
@@ -66,19 +66,27 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
|
||||
return this.revisionMapper.toDomain(sqlRevision)
|
||||
}
|
||||
|
||||
override async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({
|
||||
sharedVaultUuid: null,
|
||||
keySystemIdentifier: null,
|
||||
override async clearSharedVaultAndKeySystemAssociations(dto: {
|
||||
itemUuid?: Uuid
|
||||
sharedVaultUuid: Uuid
|
||||
}): Promise<void> {
|
||||
const queryBuilder = this.ormRepository.createQueryBuilder().update().set({
|
||||
sharedVaultUuid: null,
|
||||
keySystemIdentifier: null,
|
||||
})
|
||||
|
||||
if (dto.itemUuid !== undefined) {
|
||||
queryBuilder.where('item_uuid = :itemUuid AND shared_vault_uuid = :sharedVaultUuid', {
|
||||
itemUuid: dto.itemUuid.value,
|
||||
sharedVaultUuid: dto.sharedVaultUuid.value,
|
||||
})
|
||||
.where('item_uuid = :itemUuid AND shared_vault_uuid = :sharedVaultUuid', {
|
||||
itemUuid: itemUuid.value,
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
} else {
|
||||
queryBuilder.where('shared_vault_uuid = :sharedVaultUuid', {
|
||||
sharedVaultUuid: dto.sharedVaultUuid.value,
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
await queryBuilder.execute()
|
||||
}
|
||||
|
||||
override async findMetadataByItemId(
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.52](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.51...@standardnotes/scheduler-server@1.20.52) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.51](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.50...@standardnotes/scheduler-server@1.20.51) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.50](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.49...@standardnotes/scheduler-server@1.20.50) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.50",
|
||||
"version": "1.20.52",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.37](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.36...@standardnotes/settings@1.21.37) (2023-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.36](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.35...@standardnotes/settings@1.21.36) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
## [1.21.35](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.34...@standardnotes/settings@1.21.35) (2023-09-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.35",
|
||||
"version": "1.21.37",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,31 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.102.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.101.1...@standardnotes/syncing-server@1.102.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add designating a survivor in shared vault ([#841](https://github.com/standardnotes/syncing-server-js/issues/841)) ([230c96d](https://github.com/standardnotes/syncing-server-js/commit/230c96dcf1d8faed9ce8fe288549226da70317e6))
|
||||
|
||||
## [1.101.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.101.0...@standardnotes/syncing-server@1.101.1) (2023-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* secondary database catch up time ([880db10](https://github.com/standardnotes/syncing-server-js/commit/880db1038a39d4610a2593489a18e207487347a2))
|
||||
|
||||
# [1.101.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.100.0...@standardnotes/syncing-server@1.101.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* add unassigning items and revisions upon shared vault removal ([#839](https://github.com/standardnotes/syncing-server-js/issues/839)) ([378eced](https://github.com/standardnotes/syncing-server-js/commit/378ecedfcc4fb23475c2329fb37479edb3b48a39))
|
||||
* **syncing-server:** distinct notifications upon user removal from shared vault ([#840](https://github.com/standardnotes/syncing-server-js/issues/840)) ([41e2136](https://github.com/standardnotes/syncing-server-js/commit/41e2136bc07312974701a70652528d304105e0f9))
|
||||
|
||||
# [1.100.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.99.0...@standardnotes/syncing-server@1.100.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** remove owned shared vaults upon account deletion ([#838](https://github.com/standardnotes/syncing-server-js/issues/838)) ([22a8cc9](https://github.com/standardnotes/syncing-server-js/commit/22a8cc90f1232fd5f5646f613c80bd7c60186670))
|
||||
|
||||
# [1.99.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.98.6...@standardnotes/syncing-server@1.99.0) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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") ')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.99.0",
|
||||
"version": "1.102.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -165,6 +165,10 @@ import { SQLItemPersistenceMapper } from '../Mapping/Persistence/SQLItemPersiste
|
||||
import { SQLItemRepository } from '../Infra/TypeORM/SQLItemRepository'
|
||||
import { SendEventToClient } from '../Domain/UseCase/Syncing/SendEventToClient/SendEventToClient'
|
||||
import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionRequestedEventHandler'
|
||||
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
|
||||
@@ -763,6 +767,7 @@ export class ContainerConfigLoader {
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
|
||||
container.get<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
@@ -780,10 +785,21 @@ export class ContainerConfigLoader {
|
||||
.bind<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVault(
|
||||
container.get(TYPES.Sync_SharedVaultRepository),
|
||||
container.get(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get(TYPES.Sync_SharedVaultInviteRepository),
|
||||
container.get(TYPES.Sync_RemoveSharedVaultUser),
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
|
||||
container.get<SharedVaultInviteRepositoryInterface>(TYPES.Sync_SharedVaultInviteRepository),
|
||||
container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
|
||||
container.get<DeclineInviteToSharedVault>(TYPES.Sync_DeclineInviteToSharedVault),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults)
|
||||
.toConstantValue(
|
||||
new DeleteSharedVaults(
|
||||
container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
|
||||
container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -841,6 +857,25 @@ export class ContainerConfigLoader {
|
||||
env.get('MIGRATION_BATCH_SIZE', true) ? +env.get('MIGRATION_BATCH_SIZE', true) : 100,
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<RemoveItemsFromSharedVault>(TYPES.Sync_RemoveItemsFromSharedVault)
|
||||
.toConstantValue(
|
||||
new RemoveItemsFromSharedVault(
|
||||
isSecondaryDatabaseEnabled
|
||||
? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
|
||||
: 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
|
||||
@@ -902,6 +937,7 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
|
||||
container.get<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
@@ -955,6 +991,14 @@ export class ContainerConfigLoader {
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultRemovedEventHandler>(TYPES.Sync_SharedVaultRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultRemovedEventHandler(
|
||||
container.get<RemoveItemsFromSharedVault>(TYPES.Sync_RemoveItemsFromSharedVault),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Services
|
||||
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
||||
@@ -993,6 +1037,10 @@ export class ContainerConfigLoader {
|
||||
'TRANSITION_REQUESTED',
|
||||
container.get<TransitionRequestedEventHandler>(TYPES.Sync_TransitionRequestedEventHandler),
|
||||
],
|
||||
[
|
||||
'SHARED_VAULT_REMOVED',
|
||||
container.get<SharedVaultRemovedEventHandler>(TYPES.Sync_SharedVaultRemovedEventHandler),
|
||||
],
|
||||
])
|
||||
if (!isConfiguredForHomeServer) {
|
||||
container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
||||
|
||||
@@ -54,6 +54,7 @@ const TYPES = {
|
||||
Sync_GetSharedVaults: Symbol.for('Sync_GetSharedVaults'),
|
||||
Sync_CreateSharedVault: Symbol.for('Sync_CreateSharedVault'),
|
||||
Sync_DeleteSharedVault: Symbol.for('Sync_DeleteSharedVault'),
|
||||
Sync_DeleteSharedVaults: Symbol.for('Sync_DeleteSharedVaults'),
|
||||
Sync_CreateSharedVaultFileValetToken: Symbol.for('Sync_CreateSharedVaultFileValetToken'),
|
||||
Sync_GetSharedVaultUsers: Symbol.for('Sync_GetSharedVaultUsers'),
|
||||
Sync_AddUserToSharedVault: Symbol.for('Sync_AddUserToSharedVault'),
|
||||
@@ -85,6 +86,8 @@ const TYPES = {
|
||||
'Sync_TransitionItemsFromPrimaryToSecondaryDatabaseForUser',
|
||||
),
|
||||
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'),
|
||||
@@ -94,6 +97,7 @@ const TYPES = {
|
||||
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
|
||||
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
|
||||
Sync_TransitionRequestedEventHandler: Symbol.for('Sync_TransitionRequestedEventHandler'),
|
||||
Sync_SharedVaultRemovedEventHandler: Symbol.for('Sync_SharedVaultRemovedEventHandler'),
|
||||
// Services
|
||||
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
|
||||
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
MessageSentToUserEvent,
|
||||
NotificationAddedForUserEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
SharedVaultRemovedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
UserAddedToSharedVaultEvent,
|
||||
UserDesignatedAsSurvivorInSharedVaultEvent,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
UserRemovedFromSharedVaultEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
@@ -21,11 +23,44 @@ 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',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.sharedVaultUuid,
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createItemRemovedFromSharedVaultEvent(dto: {
|
||||
sharedVaultUuid: string
|
||||
itemUuid: string
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
}): ItemRemovedFromSharedVaultEvent {
|
||||
return {
|
||||
type: 'ITEM_REMOVED_FROM_SHARED_VAULT',
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
MessageSentToUserEvent,
|
||||
NotificationAddedForUserEvent,
|
||||
RevisionsCopyRequestedEvent,
|
||||
SharedVaultRemovedEvent,
|
||||
TransitionStatusUpdatedEvent,
|
||||
UserAddedToSharedVaultEvent,
|
||||
UserDesignatedAsSurvivorInSharedVaultEvent,
|
||||
UserInvitedToSharedVaultEvent,
|
||||
UserRemovedFromSharedVaultEvent,
|
||||
WebSocketMessageRequestedEvent,
|
||||
@@ -99,6 +101,11 @@ export interface DomainEventFactoryInterface {
|
||||
sharedVaultUuid: string
|
||||
itemUuid: string
|
||||
userUuid: string
|
||||
roleNames: string[]
|
||||
}): ItemRemovedFromSharedVaultEvent
|
||||
createSharedVaultRemovedEvent(dto: { sharedVaultUuid: string }): SharedVaultRemovedEvent
|
||||
createUserDesignatedAsSurvivorInSharedVaultEvent(dto: {
|
||||
sharedVaultUuid: string
|
||||
userUuid: string
|
||||
timestamp: number
|
||||
}): UserDesignatedAsSurvivorInSharedVaultEvent
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
||||
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let itemRepositoryResolver: ItemRepositoryResolverInterface
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let logger: Logger
|
||||
let event: AccountDeletionRequestedEvent
|
||||
let item: Item
|
||||
|
||||
const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepositoryResolver, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
item = Item.create(
|
||||
{
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
updatedWithSession: null,
|
||||
content: 'foobar',
|
||||
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||
encItemKey: null,
|
||||
authHash: null,
|
||||
itemsKeyId: null,
|
||||
duplicateOf: null,
|
||||
deleted: false,
|
||||
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
itemRepository.findAll = jest.fn().mockReturnValue([item])
|
||||
itemRepository.deleteByUserUuid = jest.fn()
|
||||
|
||||
itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
|
||||
itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '2-3-4',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
roleNames: ['CORE_USER'],
|
||||
}
|
||||
})
|
||||
|
||||
it('should remove all items for a user', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
|
||||
})
|
||||
|
||||
it('should do nothing if role names are not valid', async () => {
|
||||
event.payload.roleNames = ['INVALID_ROLE_NAME']
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(itemRepository.deleteByUserUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,12 @@ import { RoleNameCollection } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
|
||||
import { DeleteSharedVaults } from '../UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults'
|
||||
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private itemRepositoryResolver: ItemRepositoryResolverInterface,
|
||||
private deleteSharedVaults: DeleteSharedVaults,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -21,6 +23,15 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
await itemRepository.deleteByUserUuid(event.payload.userUuid)
|
||||
|
||||
const result = await this.deleteSharedVaults.execute({
|
||||
ownerUuid: event.payload.userUuid,
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to delete shared vaults for user: ${event.payload.userUuid}: ${result.getError()}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { RemoveItemsFromSharedVault } from '../UseCase/SharedVaults/RemoveItemsFromSharedVault/RemoveItemsFromSharedVault'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class SharedVaultRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private removeItemsFromSharedVault: RemoveItemsFromSharedVault,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SharedVaultRemovedEvent): Promise<void> {
|
||||
const result = await this.removeItemsFromSharedVault.execute({
|
||||
sharedVaultUuid: event.payload.sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`Failed to remove items from shared vault ${event.payload.sharedVaultUuid}: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,5 @@ export interface ItemRepositoryInterface {
|
||||
save(item: Item): Promise<void>
|
||||
markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
|
||||
updateContentSize(itemUuid: string, contentSize: number): Promise<void>
|
||||
unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void>
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('Notification', () => {
|
||||
it('should create an entity', () => {
|
||||
const payload = NotificationPayload.create({
|
||||
sharedVaultUuid: Uuid.create('0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e').getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.SelfRemovedFromSharedVault).getValue(),
|
||||
version: '1.0',
|
||||
}).getValue()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SharedVault } from './SharedVault'
|
||||
|
||||
export interface SharedVaultRepositoryInterface {
|
||||
findByUuid(uuid: Uuid): Promise<SharedVault | null>
|
||||
findByUserUuid(userUuid: Uuid): Promise<SharedVault[]>
|
||||
countByUserUuid(userUuid: Uuid): Promise<number>
|
||||
findByUuids(uuids: Uuid[], lastSyncTime?: number): Promise<SharedVault[]>
|
||||
save(sharedVault: SharedVault): Promise<void>
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface SharedVaultInviteRepositoryInterface {
|
||||
findByUuid(sharedVaultInviteUuid: Uuid): Promise<SharedVaultInvite | null>
|
||||
save(sharedVaultInvite: SharedVaultInvite): Promise<void>
|
||||
remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
|
||||
removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
|
||||
findBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultInvite[]>
|
||||
findByUserUuid(userUuid: Uuid): Promise<SharedVaultInvite[]>
|
||||
findByUserUuidUpdatedAfter(userUuid: Uuid, updatedAtTimestamp: number): Promise<SharedVaultInvite[]>
|
||||
findBySenderUuid(senderUuid: Uuid): Promise<SharedVaultInvite[]>
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
payload = NotificationPayload.create({
|
||||
sharedVaultUuid: Uuid.create('0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e').getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.RemovedFromSharedVault).getValue(),
|
||||
type: NotificationType.create(NotificationType.TYPES.SelfRemovedFromSharedVault).getValue(),
|
||||
version: '1.0',
|
||||
}).getValue()
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
|
||||
type: NotificationType.TYPES.RemovedFromSharedVault,
|
||||
type: NotificationType.TYPES.SelfRemovedFromSharedVault,
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
@@ -63,7 +63,7 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
type: NotificationType.TYPES.RemovedFromSharedVault,
|
||||
type: NotificationType.TYPES.SelfRemovedFromSharedVault,
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
@@ -94,7 +94,7 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
|
||||
type: NotificationType.TYPES.RemovedFromSharedVault,
|
||||
type: NotificationType.TYPES.SelfRemovedFromSharedVault,
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
@@ -111,7 +111,7 @@ describe('AddNotificationForUser', () => {
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e',
|
||||
type: NotificationType.TYPES.RemovedFromSharedVault,
|
||||
type: NotificationType.TYPES.SelfRemovedFromSharedVault,
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -54,6 +55,35 @@ describe('AddNotificationsForUsers', () => {
|
||||
expect(addNotificationForUser.execute).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not add notification for exceptUserUuid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
exceptUserUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
type: 'test',
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(addNotificationForUser.execute).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should return error if exceptUserUuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
exceptUserUuid: 'invalid',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
type: 'test',
|
||||
payload,
|
||||
version: '1.0',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if shared vault uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
|
||||
@@ -17,8 +17,21 @@ export class AddNotificationsForUsers implements UseCaseInterface<void> {
|
||||
}
|
||||
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
|
||||
let exceptUserUuid: Uuid | undefined
|
||||
if (dto.exceptUserUuid) {
|
||||
const exceptUserUuidOrError = Uuid.create(dto.exceptUserUuid)
|
||||
if (exceptUserUuidOrError.isFailed()) {
|
||||
return Result.fail(exceptUserUuidOrError.getError())
|
||||
}
|
||||
exceptUserUuid = exceptUserUuidOrError.getValue()
|
||||
}
|
||||
|
||||
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultUser of sharedVaultUsers) {
|
||||
if (exceptUserUuid && sharedVaultUser.props.userUuid.equals(exceptUserUuid)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.addNotificationForUser.execute({
|
||||
userUuid: sharedVaultUser.props.userUuid.value,
|
||||
type: dto.type,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NotificationPayload } from '@standardnotes/domain-core'
|
||||
|
||||
export interface AddNotificationsForUsersDTO {
|
||||
exceptUserUuid?: string
|
||||
sharedVaultUuid: string
|
||||
version: string
|
||||
type: string
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core'
|
||||
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
|
||||
@@ -6,14 +7,21 @@ import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/Sh
|
||||
import { DeleteSharedVault } from './DeleteSharedVault'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
|
||||
import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('DeleteSharedVault', () => {
|
||||
let sharedVaultRepository: SharedVaultRepositoryInterface
|
||||
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
|
||||
let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
|
||||
let removeUserFromSharedVault: RemoveUserFromSharedVault
|
||||
let declineInviteToSharedVault: DeclineInviteToSharedVault
|
||||
let sharedVault: SharedVault
|
||||
let sharedVaultUser: SharedVaultUser
|
||||
let sharedVaultInvite: SharedVaultInvite
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new DeleteSharedVault(
|
||||
@@ -21,6 +29,9 @@ describe('DeleteSharedVault', () => {
|
||||
sharedVaultUserRepository,
|
||||
sharedVaultInviteRepository,
|
||||
removeUserFromSharedVault,
|
||||
declineInviteToSharedVault,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -38,15 +49,38 @@ 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])
|
||||
|
||||
sharedVaultInvite = SharedVaultInvite.create({
|
||||
encryptedMessage: 'test',
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
}).getValue()
|
||||
sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
|
||||
sharedVaultInviteRepository.removeBySharedVaultUuid = jest.fn()
|
||||
sharedVaultInviteRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultInvite])
|
||||
|
||||
declineInviteToSharedVault = {} as jest.Mocked<DeclineInviteToSharedVault>
|
||||
declineInviteToSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
removeUserFromSharedVault = {} as jest.Mocked<RemoveUserFromSharedVault>
|
||||
removeUserFromSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createUserRemovedFromSharedVaultEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
domainEventFactory.createSharedVaultRemovedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove shared vault', async () => {
|
||||
@@ -59,7 +93,7 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(sharedVaultRepository.remove).toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -74,7 +108,7 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -88,7 +122,7 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -102,7 +136,7 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -122,7 +156,7 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -137,7 +171,22 @@ describe('DeleteSharedVault', () => {
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return error if declining invite to shared vault fails', async () => {
|
||||
declineInviteToSharedVault.execute = jest.fn().mockReturnValue(Result.fail('failed'))
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
|
||||
expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
|
||||
expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { DeleteSharedVaultDTO } from './DeleteSharedVaultDTO'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
|
||||
import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
|
||||
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
|
||||
|
||||
export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@@ -12,6 +15,9 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
|
||||
private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
|
||||
private removeUserFromSharedVault: RemoveUserFromSharedVault,
|
||||
private declineInviteToSharedVault: DeclineInviteToSharedVault,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
|
||||
@@ -50,10 +56,26 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await this.sharedVaultInviteRepository.removeBySharedVaultUuid(sharedVaultUuid)
|
||||
const sharedVaultInvites = await this.sharedVaultInviteRepository.findBySharedVaultUuid(sharedVaultUuid)
|
||||
for (const sharedVaultInvite of sharedVaultInvites) {
|
||||
const result = await this.declineInviteToSharedVault.execute({
|
||||
inviteUuid: sharedVaultInvite.id.toString(),
|
||||
userUuid: sharedVaultInvite.props.userUuid.value,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
await this.sharedVaultRepository.remove(sharedVault)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedVaultRemovedEvent({
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
}),
|
||||
)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { DeleteSharedVault } from '../DeleteSharedVault/DeleteSharedVault'
|
||||
import { DeleteSharedVaults } from './DeleteSharedVaults'
|
||||
|
||||
describe('DeleteSharedVaults', () => {
|
||||
let sharedVaultRepository: SharedVaultRepositoryInterface
|
||||
let deleteSharedVaultUseCase: DeleteSharedVault
|
||||
let sharedVault: SharedVault
|
||||
|
||||
const createUseCase = () => new DeleteSharedVaults(sharedVaultRepository, deleteSharedVaultUseCase)
|
||||
|
||||
beforeEach(() => {
|
||||
sharedVault = SharedVault.create(
|
||||
{
|
||||
fileUploadBytesUsed: 2,
|
||||
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
timestamps: Timestamps.create(123, 123).getValue(),
|
||||
},
|
||||
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||
).getValue()
|
||||
sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
|
||||
sharedVaultRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVault])
|
||||
|
||||
deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
|
||||
deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.ok())
|
||||
})
|
||||
|
||||
it('should delete all shared vaults for a user', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
ownerUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
expect(sharedVaultRepository.findByUserUuid).toHaveBeenCalled()
|
||||
expect(deleteSharedVaultUseCase.execute).toHaveBeenCalledWith({
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error if delete shared vault fails', async () => {
|
||||
deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.fail('error'))
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
ownerUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(sharedVaultRepository.findByUserUuid).toHaveBeenCalled()
|
||||
expect(deleteSharedVaultUseCase.execute).toHaveBeenCalledWith({
|
||||
originatorUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error if owner uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
ownerUuid: 'invalid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(sharedVaultRepository.findByUserUuid).not.toHaveBeenCalled()
|
||||
expect(deleteSharedVaultUseCase.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { DeleteSharedVaultsDTO } from './DeleteSharedVaultsDTO'
|
||||
import { DeleteSharedVault } from '../DeleteSharedVault/DeleteSharedVault'
|
||||
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
|
||||
export class DeleteSharedVaults implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private sharedVaultRepository: SharedVaultRepositoryInterface,
|
||||
private deleteSharedVaultUseCase: DeleteSharedVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: DeleteSharedVaultsDTO): Promise<Result<void>> {
|
||||
const ownerUuidOrError = Uuid.create(dto.ownerUuid)
|
||||
if (ownerUuidOrError.isFailed()) {
|
||||
return Result.fail(ownerUuidOrError.getError())
|
||||
}
|
||||
const ownerUuid = ownerUuidOrError.getValue()
|
||||
|
||||
const sharedVaults = await this.sharedVaultRepository.findByUserUuid(ownerUuid)
|
||||
|
||||
for (const sharedVault of sharedVaults) {
|
||||
const result = await this.deleteSharedVaultUseCase.execute({
|
||||
originatorUuid: ownerUuid.value,
|
||||
sharedVaultUuid: sharedVault.id.toString(),
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface DeleteSharedVaultsDTO {
|
||||
ownerUuid: string
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user