diff --git a/packages/api-gateway/src/Controller/AuthMiddleware.ts b/packages/api-gateway/src/Controller/AuthMiddleware.ts index 224b2152d..c2fe6316c 100644 --- a/packages/api-gateway/src/Controller/AuthMiddleware.ts +++ b/packages/api-gateway/src/Controller/AuthMiddleware.ts @@ -72,6 +72,7 @@ export abstract class AuthMiddleware extends BaseMiddleware { response.locals.session = decodedToken.session response.locals.roles = decodedToken.roles response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context + response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? [] } catch (error) { const errorMessage = (error as AxiosError).isAxiosError ? JSON.stringify((error as AxiosError).response?.data) diff --git a/packages/auth/migrations/mysql/1694000575425-add-shared-vault-users.ts b/packages/auth/migrations/mysql/1694000575425-add-shared-vault-users.ts new file mode 100644 index 000000000..47ccbd3ca --- /dev/null +++ b/packages/auth/migrations/mysql/1694000575425-add-shared-vault-users.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSharedVaultUsers1694000575425 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE `auth_shared_vault_users` (`uuid` varchar(36) 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, INDEX `shared_vault_uuid_on_auth_shared_vault_users` (`shared_vault_uuid`), INDEX `user_uuid_on_auth_shared_vault_users` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB', + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX `user_uuid_on_auth_shared_vault_users` ON `auth_shared_vault_users`') + await queryRunner.query('DROP INDEX `shared_vault_uuid_on_auth_shared_vault_users` ON `auth_shared_vault_users`') + await queryRunner.query('DROP TABLE `auth_shared_vault_users`') + } +} diff --git a/packages/auth/migrations/sqlite/1694000640645-add-shared-vault-users.ts b/packages/auth/migrations/sqlite/1694000640645-add-shared-vault-users.ts new file mode 100644 index 000000000..b6ea8994d --- /dev/null +++ b/packages/auth/migrations/sqlite/1694000640645-add-shared-vault-users.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSharedVaultUsers1694000640645 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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( + '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") ', + ) + } + + public async down(queryRunner: QueryRunner): Promise { + 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('DROP TABLE "auth_shared_vault_users"') + } +} diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 5a0a9234b..8c5029ea9 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -189,6 +189,7 @@ import { ControllerContainer, ControllerContainerInterface, MapperInterface, + SharedVaultUser, } from '@standardnotes/domain-core' import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper' import { SessionTrace } from '../Domain/Session/SessionTrace' @@ -263,6 +264,14 @@ import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTr import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler' import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus' import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus' +import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' +import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper' +import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface' +import { TypeORMSharedVaultUserRepository } from '../Infra/TypeORM/TypeORMSharedVaultUserRepository' +import { AddSharedVaultUser } from '../Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser' +import { RemoveSharedVaultUser } from '../Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser' +import { UserAddedToSharedVaultEventHandler } from '../Domain/Handler/UserAddedToSharedVaultEventHandler' +import { UserRemovedFromSharedVaultEventHandler } from '../Domain/Handler/UserRemovedFromSharedVaultEventHandler' export class ContainerConfigLoader { async load(configuration?: { @@ -372,6 +381,9 @@ export class ContainerConfigLoader { container .bind>(TYPES.Auth_CacheEntryPersistenceMapper) .toConstantValue(new CacheEntryPersistenceMapper()) + container + .bind>(TYPES.Auth_SharedVaultUserPersistenceMapper) + .toConstantValue(new SharedVaultUserPersistenceMapper()) // ORM container @@ -412,6 +424,9 @@ export class ContainerConfigLoader { container .bind>(TYPES.Auth_ORMCacheEntryRepository) .toConstantValue(appDataSource.getRepository(TypeORMCacheEntry)) + container + .bind>(TYPES.Auth_ORMSharedVaultUserRepository) + .toConstantValue(appDataSource.getRepository(TypeORMSharedVaultUser)) // Repositories container.bind(TYPES.Auth_SessionRepository).to(TypeORMSessionRepository) @@ -468,6 +483,16 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_CacheEntryPersistenceMapper), ), ) + container + .bind(TYPES.Auth_SharedVaultUserRepository) + .toConstantValue( + new TypeORMSharedVaultUserRepository( + container.get>(TYPES.Auth_ORMSharedVaultUserRepository), + container.get>( + TYPES.Auth_SharedVaultUserPersistenceMapper, + ), + ), + ) // Middleware container.bind(TYPES.Auth_SessionMiddleware).to(SessionMiddleware) @@ -926,6 +951,18 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_UserRepository), ), ) + container + .bind(TYPES.Auth_AddSharedVaultUser) + .toConstantValue( + new AddSharedVaultUser(container.get(TYPES.Auth_SharedVaultUserRepository)), + ) + container + .bind(TYPES.Auth_RemoveSharedVaultUser) + .toConstantValue( + new RemoveSharedVaultUser( + container.get(TYPES.Auth_SharedVaultUserRepository), + ), + ) // Controller container @@ -1075,6 +1112,22 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_Logger), ), ) + container + .bind(TYPES.Auth_UserAddedToSharedVaultEventHandler) + .toConstantValue( + new UserAddedToSharedVaultEventHandler( + container.get(TYPES.Auth_AddSharedVaultUser), + container.get(TYPES.Auth_Logger), + ), + ) + container + .bind(TYPES.Auth_UserRemovedFromSharedVaultEventHandler) + .toConstantValue( + new UserRemovedFromSharedVaultEventHandler( + container.get(TYPES.Auth_RemoveSharedVaultUser), + container.get(TYPES.Auth_Logger), + ), + ) const eventHandlers: Map = new Map([ ['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)], @@ -1107,6 +1160,8 @@ export class ContainerConfigLoader { ['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)], ['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)], ['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)], ]) if (isConfiguredForHomeServer) { diff --git a/packages/auth/src/Bootstrap/DataSource.ts b/packages/auth/src/Bootstrap/DataSource.ts index 2ae8eda4c..510ad304f 100644 --- a/packages/auth/src/Bootstrap/DataSource.ts +++ b/packages/auth/src/Bootstrap/DataSource.ts @@ -18,6 +18,7 @@ import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmerge import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace' import { Env } from './Env' import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions' +import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' export class AppDataSource { private _dataSource: DataSource | undefined @@ -64,6 +65,7 @@ export class AppDataSource { TypeORMAuthenticatorChallenge, TypeORMEmergencyAccessInvitation, TypeORMCacheEntry, + TypeORMSharedVaultUser, ], migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`], migrationsRun: true, diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index c82f92445..3678cd965 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -9,6 +9,7 @@ const TYPES = { Auth_AuthenticatorPersistenceMapper: Symbol.for('Auth_AuthenticatorPersistenceMapper'), Auth_AuthenticatorHttpMapper: Symbol.for('Auth_AuthenticatorHttpMapper'), Auth_CacheEntryPersistenceMapper: Symbol.for('Auth_CacheEntryPersistenceMapper'), + Auth_SharedVaultUserPersistenceMapper: Symbol.for('Auth_SharedVaultUserPersistenceMapper'), // Controller Auth_ControllerContainer: Symbol.for('Auth_ControllerContainer'), Auth_AuthController: Symbol.for('Auth_AuthController'), @@ -36,6 +37,7 @@ const TYPES = { Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'), Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'), Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'), + Auth_SharedVaultUserRepository: Symbol.for('Auth_SharedVaultUserRepository'), // ORM Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'), Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'), @@ -51,6 +53,7 @@ const TYPES = { Auth_ORMAuthenticatorRepository: Symbol.for('Auth_ORMAuthenticatorRepository'), Auth_ORMAuthenticatorChallengeRepository: Symbol.for('Auth_ORMAuthenticatorChallengeRepository'), Auth_ORMCacheEntryRepository: Symbol.for('Auth_ORMCacheEntryRepository'), + Auth_ORMSharedVaultUserRepository: Symbol.for('Auth_ORMSharedVaultUserRepository'), // Middleware Auth_RequiredCrossServiceTokenMiddleware: Symbol.for('Auth_RequiredCrossServiceTokenMiddleware'), Auth_OptionalCrossServiceTokenMiddleware: Symbol.for('Auth_OptionalCrossServiceTokenMiddleware'), @@ -157,6 +160,8 @@ const TYPES = { Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'), Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'), Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'), + Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'), + Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'), // Handlers Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'), Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'), @@ -186,6 +191,8 @@ const TYPES = { Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'), Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'), Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'), + Auth_UserAddedToSharedVaultEventHandler: Symbol.for('Auth_UserAddedToSharedVaultEventHandler'), + Auth_UserRemovedFromSharedVaultEventHandler: Symbol.for('Auth_UserRemovedFromSharedVaultEventHandler'), // Services Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'), Auth_SessionService: Symbol.for('Auth_SessionService'), diff --git a/packages/auth/src/Domain/Handler/UserAddedToSharedVaultEventHandler.ts b/packages/auth/src/Domain/Handler/UserAddedToSharedVaultEventHandler.ts new file mode 100644 index 000000000..2b25ed1c7 --- /dev/null +++ b/packages/auth/src/Domain/Handler/UserAddedToSharedVaultEventHandler.ts @@ -0,0 +1,24 @@ +import { DomainEventHandlerInterface, UserAddedToSharedVaultEvent } from '@standardnotes/domain-events' +import { Logger } from 'winston' +import { AddSharedVaultUser } from '../UseCase/AddSharedVaultUser/AddSharedVaultUser' + +export class UserAddedToSharedVaultEventHandler implements DomainEventHandlerInterface { + constructor( + private addSharedVaultUser: AddSharedVaultUser, + private logger: Logger, + ) {} + + async handle(event: UserAddedToSharedVaultEvent): Promise { + const result = await this.addSharedVaultUser.execute({ + userUuid: event.payload.userUuid, + sharedVaultUuid: event.payload.sharedVaultUuid, + permission: event.payload.permission, + createdAt: event.payload.createdAt, + updatedAt: event.payload.updatedAt, + }) + + if (result.isFailed()) { + this.logger.error(`Failed to add user to shared vault: ${result.getError()}`) + } + } +} diff --git a/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts b/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts new file mode 100644 index 000000000..c70567aad --- /dev/null +++ b/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts @@ -0,0 +1,22 @@ +import { DomainEventHandlerInterface, UserRemovedFromSharedVaultEvent } from '@standardnotes/domain-events' +import { Logger } from 'winston' + +import { RemoveSharedVaultUser } from '../UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser' + +export class UserRemovedFromSharedVaultEventHandler implements DomainEventHandlerInterface { + constructor( + private removeSharedVaultUser: RemoveSharedVaultUser, + private logger: Logger, + ) {} + + async handle(event: UserRemovedFromSharedVaultEvent): Promise { + const result = await this.removeSharedVaultUser.execute({ + userUuid: event.payload.userUuid, + sharedVaultUuid: event.payload.sharedVaultUuid, + }) + + if (result.isFailed()) { + this.logger.error(`Failed to remove user from shared vault: ${result.getError()}`) + } + } +} diff --git a/packages/auth/src/Domain/SharedVault/SharedVaultUserRepositoryInterface.ts b/packages/auth/src/Domain/SharedVault/SharedVaultUserRepositoryInterface.ts new file mode 100644 index 000000000..2fe53754f --- /dev/null +++ b/packages/auth/src/Domain/SharedVault/SharedVaultUserRepositoryInterface.ts @@ -0,0 +1,8 @@ +import { SharedVaultUser, Uuid } from '@standardnotes/domain-core' + +export interface SharedVaultUserRepositoryInterface { + findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise + findByUserUuid(userUuid: Uuid): Promise + save(sharedVaultUser: SharedVaultUser): Promise + remove(sharedVault: SharedVaultUser): Promise +} diff --git a/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.spec.ts b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.spec.ts new file mode 100644 index 000000000..799576ebd --- /dev/null +++ b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.spec.ts @@ -0,0 +1,106 @@ +import { Result, SharedVaultUser } from '@standardnotes/domain-core' +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { AddSharedVaultUser } from './AddSharedVaultUser' + +describe('AddSharedVaultUser', () => { + let sharedVaultUserRepository: SharedVaultUserRepositoryInterface + + const createUseCase = () => new AddSharedVaultUser(sharedVaultUserRepository) + + beforeEach(() => { + sharedVaultUserRepository = {} as jest.Mocked + sharedVaultUserRepository.save = jest.fn() + }) + + it('should save shared vault user', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + createdAt: 1, + updatedAt: 2, + }) + + expect(result.isFailed()).toBeFalsy() + expect(sharedVaultUserRepository.save).toHaveBeenCalled() + }) + + it('should fail when user uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: 'invalid', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + createdAt: 1, + updatedAt: 2, + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.save).not.toHaveBeenCalled() + }) + + it('should fail when shared vault uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: 'invalid', + permission: 'read', + createdAt: 1, + updatedAt: 2, + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.save).not.toHaveBeenCalled() + }) + + it('should fail when permission is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + permission: 'invalid', + createdAt: 1, + updatedAt: 2, + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.save).not.toHaveBeenCalled() + }) + + it('should fail when timestamps are invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + createdAt: 'invalid' as unknown as number, + updatedAt: 1, + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.save).not.toHaveBeenCalled() + }) + + it('should fail when shared vault user is invalid', async () => { + const mock = jest.spyOn(SharedVaultUser, 'create') + mock.mockReturnValue(Result.fail('Oops')) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + createdAt: 2, + updatedAt: 1, + }) + + expect(result.isFailed()).toBeTruthy() + + mock.mockRestore() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.ts b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.ts new file mode 100644 index 000000000..1959809c5 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.ts @@ -0,0 +1,56 @@ +import { + Result, + SharedVaultUser, + SharedVaultUserPermission, + Timestamps, + UseCaseInterface, + Uuid, +} from '@standardnotes/domain-core' + +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { AddSharedVaultUserDTO } from './AddSharedVaultUserDTO' + +export class AddSharedVaultUser implements UseCaseInterface { + constructor(private sharedVaultUserRepository: SharedVaultUserRepositoryInterface) {} + + async execute(dto: AddSharedVaultUserDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(sharedVaultUuidOrError.getError()) + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const permissionOrError = SharedVaultUserPermission.create(dto.permission) + if (permissionOrError.isFailed()) { + return Result.fail(permissionOrError.getError()) + } + const permission = permissionOrError.getValue() + + const timestampsOrError = Timestamps.create(dto.createdAt, dto.updatedAt) + if (timestampsOrError.isFailed()) { + return Result.fail(timestampsOrError.getError()) + } + const timestamps = timestampsOrError.getValue() + + const sharedVaultUserOrError = SharedVaultUser.create({ + userUuid, + sharedVaultUuid, + permission, + timestamps, + }) + if (sharedVaultUserOrError.isFailed()) { + return Result.fail(sharedVaultUserOrError.getError()) + } + const sharedVaultUser = sharedVaultUserOrError.getValue() + + await this.sharedVaultUserRepository.save(sharedVaultUser) + + return Result.ok() + } +} diff --git a/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUserDTO.ts b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUserDTO.ts new file mode 100644 index 000000000..9591eaab1 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUserDTO.ts @@ -0,0 +1,7 @@ +export interface AddSharedVaultUserDTO { + sharedVaultUuid: string + userUuid: string + permission: string + createdAt: number + updatedAt: number +} diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts index 9e6fe80a8..7d5a342dc 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts @@ -9,8 +9,9 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { CreateCrossServiceToken } from './CreateCrossServiceToken' import { GetSetting } from '../GetSetting/GetSetting' -import { Result } from '@standardnotes/domain-core' +import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface' +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' describe('CreateCrossServiceToken', () => { let userProjector: ProjectorInterface @@ -20,6 +21,7 @@ describe('CreateCrossServiceToken', () => { let userRepository: UserRepositoryInterface let getSettingUseCase: GetSetting let transitionStatusRepository: TransitionStatusRepositoryInterface + let sharedVaultUserRepository: SharedVaultUserRepositoryInterface const jwtTTL = 60 let session: Session @@ -36,6 +38,7 @@ describe('CreateCrossServiceToken', () => { jwtTTL, getSettingUseCase, transitionStatusRepository, + sharedVaultUserRepository, ) beforeEach(() => { @@ -70,6 +73,16 @@ describe('CreateCrossServiceToken', () => { transitionStatusRepository = {} as jest.Mocked transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO') + + sharedVaultUserRepository = {} as jest.Mocked + sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([ + SharedVaultUser.create({ + permission: SharedVaultUserPermission.create('read').getValue(), + sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123456789, 123456789).getValue(), + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }).getValue(), + ]) }) it('should create a cross service token for user', async () => { @@ -86,6 +99,12 @@ describe('CreateCrossServiceToken', () => { uuid: '1-3-4', }, ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], session: { test: 'test', }, @@ -115,6 +134,12 @@ describe('CreateCrossServiceToken', () => { uuid: '1-3-4', }, ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], session: { test: 'test', }, @@ -141,6 +166,12 @@ describe('CreateCrossServiceToken', () => { uuid: '1-3-4', }, ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], user: { email: 'test@test.te', uuid: '00000000-0000-0000-0000-000000000000', @@ -164,6 +195,12 @@ describe('CreateCrossServiceToken', () => { uuid: '1-3-4', }, ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], user: { email: 'test@test.te', uuid: '00000000-0000-0000-0000-000000000000', @@ -208,6 +245,12 @@ describe('CreateCrossServiceToken', () => { uuid: '1-3-4', }, ], + belongs_to_shared_vaults: [ + { + shared_vault_uuid: '00000000-0000-0000-0000-000000000000', + permission: 'read', + }, + ], session: { test: 'test', }, diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts index d5ec5ad27..4238a7a20 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts @@ -13,6 +13,7 @@ import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO' import { GetSetting } from '../GetSetting/GetSetting' import { SettingName } from '@standardnotes/settings' import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface' +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' @injectable() export class CreateCrossServiceToken implements UseCaseInterface { @@ -27,6 +28,7 @@ export class CreateCrossServiceToken implements UseCaseInterface { private getSettingUseCase: GetSetting, @inject(TYPES.Auth_TransitionStatusRepository) private transitionStatusRepository: TransitionStatusRepositoryInterface, + @inject(TYPES.Auth_SharedVaultUserRepository) private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, ) {} async execute(dto: CreateCrossServiceTokenDTO): Promise> { @@ -49,11 +51,19 @@ export class CreateCrossServiceToken implements UseCaseInterface { const roles = await user.roles + const sharedVaultAssociations = await this.sharedVaultUserRepository.findByUserUuid( + Uuid.create(user.uuid).getValue(), + ) + const authTokenData: CrossServiceTokenData = { user: this.projectUser(user), roles: this.projectRoles(roles), shared_vault_owner_context: undefined, ongoing_transition: transitionStatus === 'STARTED', + belongs_to_shared_vaults: sharedVaultAssociations.map((association) => ({ + shared_vault_uuid: association.props.sharedVaultUuid.value, + permission: association.props.permission.value, + })), } if (dto.sharedVaultOwnerContext !== undefined) { diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts new file mode 100644 index 000000000..4db3624a2 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts @@ -0,0 +1,68 @@ +import { SharedVaultUser } from '@standardnotes/domain-core' + +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { RemoveSharedVaultUser } from './RemoveSharedVaultUser' + +describe('RemoveSharedVaultUser', () => { + let sharedVaultUserRepository: SharedVaultUserRepositoryInterface + + const createUseCase = () => new RemoveSharedVaultUser(sharedVaultUserRepository) + + beforeEach(() => { + sharedVaultUserRepository = {} as jest.Mocked + sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest + .fn() + .mockReturnValue({} as jest.Mocked) + sharedVaultUserRepository.remove = jest.fn() + }) + + it('should remove shared vault user', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(sharedVaultUserRepository.remove).toHaveBeenCalled() + }) + + it('should fail when user uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: 'invalid', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled() + }) + + it('should fail when shared vault uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled() + }) + + it('should fail when shared vault user is not found', async () => { + sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeTruthy() + expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts new file mode 100644 index 000000000..afccd6bd1 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts @@ -0,0 +1,34 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface' +import { RemoveSharedVaultUserDTO } from './RemoveSharedVaultUserDTO' + +export class RemoveSharedVaultUser implements UseCaseInterface { + constructor(private sharedVaultUserRepository: SharedVaultUserRepositoryInterface) {} + + async execute(dto: RemoveSharedVaultUserDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(sharedVaultUuidOrError.getError()) + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({ + userUuid, + sharedVaultUuid, + }) + if (!sharedVaultUser) { + return Result.fail('Shared vault user not found') + } + + await this.sharedVaultUserRepository.remove(sharedVaultUser) + + return Result.ok() + } +} diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts new file mode 100644 index 000000000..ac3970af4 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts @@ -0,0 +1,4 @@ +export interface RemoveSharedVaultUserDTO { + sharedVaultUuid: string + userUuid: string +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUser.ts b/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUser.ts new file mode 100644 index 000000000..e1b149e87 --- /dev/null +++ b/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUser.ts @@ -0,0 +1,40 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'auth_shared_vault_users' }) +export class TypeORMSharedVaultUser { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + name: 'shared_vault_uuid', + length: 36, + }) + @Index('shared_vault_uuid_on_auth_shared_vault_users') + declare sharedVaultUuid: string + + @Column({ + name: 'user_uuid', + length: 36, + }) + @Index('user_uuid_on_auth_shared_vault_users') + declare userUuid: string + + @Column({ + name: 'permission', + type: 'varchar', + length: 24, + }) + declare permission: string + + @Column({ + name: 'created_at_timestamp', + type: 'bigint', + }) + declare createdAtTimestamp: number + + @Column({ + name: 'updated_at_timestamp', + type: 'bigint', + }) + declare updatedAtTimestamp: number +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts new file mode 100644 index 000000000..1d8503eca --- /dev/null +++ b/packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts @@ -0,0 +1,54 @@ +import { Repository } from 'typeorm' +import { MapperInterface, SharedVaultUser, Uuid } from '@standardnotes/domain-core' + +import { SharedVaultUserRepositoryInterface } from '../../Domain/SharedVault/SharedVaultUserRepositoryInterface' +import { TypeORMSharedVaultUser } from './TypeORMSharedVaultUser' + +export class TypeORMSharedVaultUserRepository implements SharedVaultUserRepositoryInterface { + constructor( + private ormRepository: Repository, + private mapper: MapperInterface, + ) {} + + async findByUserUuid(userUuid: Uuid): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('shared_vault_user') + .where('shared_vault_user.user_uuid = :userUuid', { + userUuid: userUuid.value, + }) + .getMany() + + return persistence.map((p) => this.mapper.toDomain(p)) + } + + async findByUserUuidAndSharedVaultUuid(dto: { + userUuid: Uuid + sharedVaultUuid: Uuid + }): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('shared_vault_user') + .where('shared_vault_user.user_uuid = :userUuid', { + userUuid: dto.userUuid.value, + }) + .andWhere('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', { + sharedVaultUuid: dto.sharedVaultUuid.value, + }) + .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) + } + + async save(sharedVaultUser: SharedVaultUser): Promise { + const persistence = this.mapper.toProjection(sharedVaultUser) + + await this.ormRepository.save(persistence) + } + + async remove(sharedVaultUser: SharedVaultUser): Promise { + await this.ormRepository.remove(this.mapper.toProjection(sharedVaultUser)) + } +} diff --git a/packages/auth/src/Mapping/SharedVaultUserPersistenceMapper.ts b/packages/auth/src/Mapping/SharedVaultUserPersistenceMapper.ts new file mode 100644 index 000000000..21ff1da88 --- /dev/null +++ b/packages/auth/src/Mapping/SharedVaultUserPersistenceMapper.ts @@ -0,0 +1,67 @@ +import { + Timestamps, + MapperInterface, + UniqueEntityId, + Uuid, + SharedVaultUserPermission, + SharedVaultUser, +} from '@standardnotes/domain-core' + +import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' + +export class SharedVaultUserPersistenceMapper implements MapperInterface { + toDomain(projection: TypeORMSharedVaultUser): SharedVaultUser { + const userUuidOrError = Uuid.create(projection.userUuid) + if (userUuidOrError.isFailed()) { + throw new Error(`Failed to create shared vault user from projection: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUuidOrError.getError()}`) + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp) + if (timestampsOrError.isFailed()) { + throw new Error(`Failed to create shared vault user from projection: ${timestampsOrError.getError()}`) + } + const timestamps = timestampsOrError.getValue() + + const permissionOrError = SharedVaultUserPermission.create(projection.permission) + if (permissionOrError.isFailed()) { + throw new Error(`Failed to create shared vault user from projection: ${permissionOrError.getError()}`) + } + const permission = permissionOrError.getValue() + + const sharedVaultUserOrError = SharedVaultUser.create( + { + userUuid, + sharedVaultUuid, + permission, + timestamps, + }, + new UniqueEntityId(projection.uuid), + ) + if (sharedVaultUserOrError.isFailed()) { + throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUserOrError.getError()}`) + } + const sharedVaultUser = sharedVaultUserOrError.getValue() + + return sharedVaultUser + } + + toProjection(domain: SharedVaultUser): TypeORMSharedVaultUser { + const typeorm = new TypeORMSharedVaultUser() + + typeorm.uuid = domain.id.toString() + typeorm.sharedVaultUuid = domain.props.sharedVaultUuid.value + typeorm.userUuid = domain.props.userUuid.value + typeorm.permission = domain.props.permission.value + typeorm.createdAtTimestamp = domain.props.timestamps.createdAt + typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt + + return typeorm + } +} diff --git a/packages/domain-core/src/Domain/Common/Timestamps.spec.ts b/packages/domain-core/src/Domain/Common/Timestamps.spec.ts new file mode 100644 index 000000000..3e784a7a7 --- /dev/null +++ b/packages/domain-core/src/Domain/Common/Timestamps.spec.ts @@ -0,0 +1,23 @@ +import { Timestamps } from './Timestamps' + +describe('Timestamps', () => { + it('should create a value object', () => { + const valueOrError = Timestamps.create(123, 234) + + expect(valueOrError.isFailed()).toBeFalsy() + expect(valueOrError.getValue().createdAt).toEqual(123) + expect(valueOrError.getValue().updatedAt).toEqual(234) + }) + + it('should not create an invalid value object', () => { + const valueOrError = Timestamps.create('' as unknown as number, 123) + + expect(valueOrError.isFailed()).toBeTruthy() + }) + + it('should not create an invalid value object', () => { + const valueOrError = Timestamps.create(123, '' as unknown as number) + + expect(valueOrError.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/domain-core/src/Domain/Common/Timestamps.ts b/packages/domain-core/src/Domain/Common/Timestamps.ts index e4598b487..6e7d1331b 100644 --- a/packages/domain-core/src/Domain/Common/Timestamps.ts +++ b/packages/domain-core/src/Domain/Common/Timestamps.ts @@ -16,12 +16,12 @@ export class Timestamps extends ValueObject { } static create(createdAt: number, updatedAt: number): Result { - if (isNaN(createdAt)) { + if (isNaN(createdAt) || typeof createdAt !== 'number') { return Result.fail( `Could not create Timestamps. Creation date should be a number, given: ${createdAt}`, ) } - if (isNaN(updatedAt)) { + if (isNaN(updatedAt) || typeof updatedAt !== 'number') { return Result.fail(`Could not create Timestamps. Update date should be a number, given: ${updatedAt}`) } diff --git a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.spec.ts b/packages/domain-core/src/Domain/SharedVault/SharedVaultUser.spec.ts similarity index 79% rename from packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.spec.ts rename to packages/domain-core/src/Domain/SharedVault/SharedVaultUser.spec.ts index cfedc5a63..01518352c 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.spec.ts +++ b/packages/domain-core/src/Domain/SharedVault/SharedVaultUser.spec.ts @@ -1,6 +1,7 @@ -import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' - +import { SharedVaultUserPermission } from './SharedVaultUserPermission' import { SharedVaultUser } from './SharedVaultUser' +import { Uuid } from '../Common/Uuid' +import { Timestamps } from '../Common/Timestamps' describe('SharedVaultUser', () => { it('should create an entity', () => { diff --git a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.ts b/packages/domain-core/src/Domain/SharedVault/SharedVaultUser.ts similarity index 74% rename from packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.ts rename to packages/domain-core/src/Domain/SharedVault/SharedVaultUser.ts index 9295f3bcc..96bca5d29 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.ts +++ b/packages/domain-core/src/Domain/SharedVault/SharedVaultUser.ts @@ -1,5 +1,6 @@ -import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core' - +import { Entity } from '../Core/Entity' +import { Result } from '../Core/Result' +import { UniqueEntityId } from '../Core/UniqueEntityId' import { SharedVaultUserProps } from './SharedVaultUserProps' export class SharedVaultUser extends Entity { diff --git a/packages/domain-core/src/Domain/SharedVault/SharedVaultUserProps.ts b/packages/domain-core/src/Domain/SharedVault/SharedVaultUserProps.ts new file mode 100644 index 000000000..4e6dc4214 --- /dev/null +++ b/packages/domain-core/src/Domain/SharedVault/SharedVaultUserProps.ts @@ -0,0 +1,10 @@ +import { Timestamps } from '../Common/Timestamps' +import { Uuid } from '../Common/Uuid' +import { SharedVaultUserPermission } from './SharedVaultUserPermission' + +export interface SharedVaultUserProps { + sharedVaultUuid: Uuid + userUuid: Uuid + permission: SharedVaultUserPermission + timestamps: Timestamps +} diff --git a/packages/domain-core/src/Domain/index.ts b/packages/domain-core/src/Domain/index.ts index 67d1e8bd0..384336838 100644 --- a/packages/domain-core/src/Domain/index.ts +++ b/packages/domain-core/src/Domain/index.ts @@ -59,8 +59,10 @@ export * from './Service/ServiceIdentifier' export * from './Service/ServiceIdentifierProps' export * from './Service/ServiceInterface' +export * from './SharedVault/SharedVaultUser' export * from './SharedVault/SharedVaultUserPermission' export * from './SharedVault/SharedVaultUserPermissionProps' +export * from './SharedVault/SharedVaultUserProps' export * from './Subscription/SubscriptionPlanName' export * from './Subscription/SubscriptionPlanNameProps' diff --git a/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEvent.ts b/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEvent.ts new file mode 100644 index 000000000..114e86e4b --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { UserAddedToSharedVaultEventPayload } from './UserAddedToSharedVaultEventPayload' + +export interface UserAddedToSharedVaultEvent extends DomainEventInterface { + type: 'USER_ADDED_TO_SHARED_VAULT' + payload: UserAddedToSharedVaultEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEventPayload.ts b/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEventPayload.ts new file mode 100644 index 000000000..171c6f2da --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEventPayload.ts @@ -0,0 +1,7 @@ +export interface UserAddedToSharedVaultEventPayload { + sharedVaultUuid: string + userUuid: string + permission: string + createdAt: number + updatedAt: number +} diff --git a/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEvent.ts b/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEvent.ts new file mode 100644 index 000000000..a28d03dba --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { UserRemovedFromSharedVaultEventPayload } from './UserRemovedFromSharedVaultEventPayload' + +export interface UserRemovedFromSharedVaultEvent extends DomainEventInterface { + type: 'USER_REMOVED_FROM_SHARED_VAULT' + payload: UserRemovedFromSharedVaultEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEventPayload.ts b/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEventPayload.ts new file mode 100644 index 000000000..9add5fc2b --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEventPayload.ts @@ -0,0 +1,4 @@ +export interface UserRemovedFromSharedVaultEventPayload { + sharedVaultUuid: string + userUuid: string +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index 141b2f8a1..afd8339c6 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -96,6 +96,8 @@ export * from './Event/SubscriptionSyncRequestedEvent' export * from './Event/SubscriptionSyncRequestedEventPayload' export * from './Event/TransitionStatusUpdatedEvent' export * from './Event/TransitionStatusUpdatedEventPayload' +export * from './Event/UserAddedToSharedVaultEvent' +export * from './Event/UserAddedToSharedVaultEventPayload' export * from './Event/UserDisabledSessionUserAgentLoggingEvent' export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload' export * from './Event/UserEmailChangedEvent' @@ -104,6 +106,8 @@ export * from './Event/UserInvitedToSharedVaultEvent' export * from './Event/UserInvitedToSharedVaultEventPayload' export * from './Event/UserRegisteredEvent' export * from './Event/UserRegisteredEventPayload' +export * from './Event/UserRemovedFromSharedVaultEvent' +export * from './Event/UserRemovedFromSharedVaultEventPayload' export * from './Event/UserRolesChangedEvent' export * from './Event/UserRolesChangedEventPayload' export * from './Event/WebSocketMessageRequestedEvent' diff --git a/packages/revisions/migrations/mysql-legacy/1669113322388-init.ts b/packages/revisions/migrations/mysql-legacy/1669113322388-init.ts new file mode 100644 index 000000000..7a11d6585 --- /dev/null +++ b/packages/revisions/migrations/mysql-legacy/1669113322388-init.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class init1669113322388 implements MigrationInterface { + name = 'init1669113322388' + + public async up(queryRunner: QueryRunner): Promise { + await this.syncSchemaBetweenLegacyRevisions(queryRunner) + + await queryRunner.query( + 'CREATE TABLE IF NOT EXISTS `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB', + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX `created_at` ON `revisions`') + await queryRunner.query('DROP INDEX `creation_date` ON `revisions`') + await queryRunner.query('DROP INDEX `user_uuid` ON `revisions`') + await queryRunner.query('DROP INDEX `item_uuid` ON `revisions`') + await queryRunner.query('DROP TABLE `revisions`') + } + + private async syncSchemaBetweenLegacyRevisions(queryRunner: QueryRunner): Promise { + 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) { + return + } + + const revisionsTableHasUserUuidColumnQueryResult = await queryRunner.manager.query( + 'SELECT COUNT(*) as count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "revisions" AND column_name = "user_uuid"', + ) + const revisionsTableHasUserUuidColumn = revisionsTableHasUserUuidColumnQueryResult[0].count === 1 + if (revisionsTableHasUserUuidColumn) { + return + } + + await queryRunner.query('ALTER TABLE `revisions` ADD COLUMN `user_uuid` varchar(36) NULL') + } +} diff --git a/packages/revisions/migrations/mysql-legacy/1669636497932-remove-date-indexes.ts b/packages/revisions/migrations/mysql-legacy/1669636497932-remove-date-indexes.ts new file mode 100644 index 000000000..9e644078c --- /dev/null +++ b/packages/revisions/migrations/mysql-legacy/1669636497932-remove-date-indexes.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class removeDateIndexes1669636497932 implements MigrationInterface { + name = 'removeDateIndexes1669636497932' + + public async up(queryRunner: QueryRunner): Promise { + const indexRevisionsOnCreatedAt = await queryRunner.manager.query( + 'SHOW INDEX FROM `revisions` where `key_name` = "created_at"', + ) + const indexRevisionsOnCreatedAtExist = indexRevisionsOnCreatedAt && indexRevisionsOnCreatedAt.length > 0 + if (indexRevisionsOnCreatedAtExist) { + await queryRunner.query('DROP INDEX `created_at` ON `revisions`') + } + + const indexRevisionsOnCreationDate = await queryRunner.manager.query( + 'SHOW INDEX FROM `revisions` where `key_name` = "creation_date"', + ) + const indexRevisionsOnCreationDateAtExist = indexRevisionsOnCreationDate && indexRevisionsOnCreationDate.length > 0 + if (indexRevisionsOnCreationDateAtExist) { + await queryRunner.query('DROP INDEX `creation_date` ON `revisions`') + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE INDEX `creation_date` ON `revisions` (`creation_date`)') + await queryRunner.query('CREATE INDEX `created_at` ON `revisions` (`created_at`)') + } +} diff --git a/packages/revisions/migrations/mysql-legacy/1669735585016-make-user-uuid-nullable.ts b/packages/revisions/migrations/mysql-legacy/1669735585016-make-user-uuid-nullable.ts new file mode 100644 index 000000000..7d2d158e7 --- /dev/null +++ b/packages/revisions/migrations/mysql-legacy/1669735585016-make-user-uuid-nullable.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class makeUserUuidNullable1669735585016 implements MigrationInterface { + name = 'makeUserUuidNullable1669735585016' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL') + } +} diff --git a/packages/revisions/migrations/mysql/1693915383950-add-shared-vault-information.ts b/packages/revisions/migrations/mysql/1693915383950-add-shared-vault-information.ts new file mode 100644 index 000000000..bd15b0159 --- /dev/null +++ b/packages/revisions/migrations/mysql/1693915383950-add-shared-vault-information.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSharedVaultInformation1693915383950 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE `revisions` ADD `edited_by` varchar(36) NULL') + await queryRunner.query('ALTER TABLE `revisions` ADD `shared_vault_uuid` varchar(36) NULL') + await queryRunner.query('ALTER TABLE `revisions` ADD `key_system_identifier` varchar(36) NULL') + await queryRunner.query('CREATE INDEX `index_revisions_on_shared_vault_uuid` ON `revisions` (`shared_vault_uuid`)') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX `index_revisions_on_shared_vault_uuid` ON `revisions`') + await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `key_system_identifier`') + await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `shared_vault_uuid`') + await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `last_edited_by`') + } +} diff --git a/packages/revisions/migrations/sqlite/1693915775491-add-shared-vault-information.ts b/packages/revisions/migrations/sqlite/1693915775491-add-shared-vault-information.ts new file mode 100644 index 000000000..d4c0bd809 --- /dev/null +++ b/packages/revisions/migrations/sqlite/1693915775491-add-shared-vault-information.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSharedVaultInformation1693915775491 implements MigrationInterface { + name = 'AddSharedVaultInformation1693915775491' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX "user_uuid"') + await queryRunner.query('DROP INDEX "item_uuid"') + await queryRunner.query( + 'CREATE TABLE "temporary_revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6), "edited_by" varchar(36), "shared_vault_uuid" varchar(36), "key_system_identifier" varchar(36))', + ) + await queryRunner.query( + 'INSERT INTO "temporary_revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "revisions"', + ) + await queryRunner.query('DROP TABLE "revisions"') + await queryRunner.query('ALTER TABLE "temporary_revisions" RENAME TO "revisions"') + await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ') + await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ') + await queryRunner.query('CREATE INDEX "index_revisions_on_shared_vault_uuid" ON "revisions" ("shared_vault_uuid") ') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX "index_revisions_on_shared_vault_uuid"') + await queryRunner.query('DROP INDEX "item_uuid"') + await queryRunner.query('DROP INDEX "user_uuid"') + await queryRunner.query('ALTER TABLE "revisions" RENAME TO "temporary_revisions"') + await queryRunner.query( + 'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))', + ) + await queryRunner.query( + 'INSERT INTO "revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "temporary_revisions"', + ) + await queryRunner.query('DROP TABLE "temporary_revisions"') + await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ') + await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ') + } +} diff --git a/packages/revisions/src/Bootstrap/Container.ts b/packages/revisions/src/Bootstrap/Container.ts index c108c5b08..fc9aac139 100644 --- a/packages/revisions/src/Bootstrap/Container.ts +++ b/packages/revisions/src/Bootstrap/Container.ts @@ -7,8 +7,8 @@ import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns' import { Revision } from '../Domain/Revision/Revision' import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata' import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface' -import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository' -import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision' +import { SQLLegacyRevisionRepository } from '../Infra/TypeORM/SQL/SQLLegacyRevisionRepository' +import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision' import { AppDataSource } from './DataSource' import { Env } from './Env' import TYPES from './Types' @@ -48,8 +48,8 @@ import { BaseRevisionsController } from '../Infra/InversifyExpress/Base/BaseRevi import { Transform } from 'stream' import { MongoDBRevision } from '../Infra/TypeORM/MongoDB/MongoDBRevision' import { MongoDBRevisionRepository } from '../Infra/TypeORM/MongoDB/MongoDBRevisionRepository' -import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper' -import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper' +import { SQLLegacyRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper' +import { SQLLegacyRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper' import { MongoDBRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionMetadataPersistenceMapper' import { MongoDBRevisionPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionPersistenceMapper' import { RevisionHttpMapper } from '../Mapping/Http/RevisionHttpMapper' @@ -62,6 +62,10 @@ import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryI import { DomainEventFactory } from '../Domain/Event/DomainEventFactory' import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler' import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser' +import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision' +import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository' +import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper' +import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper' export class ContainerConfigLoader { async load(configuration?: { @@ -77,6 +81,8 @@ export class ContainerConfigLoader { env.load() const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server' + const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted' + const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting const isSecondaryDatabaseEnabled = env.get('SECONDARY_DB_ENABLED', true) === 'true' const container = new Container({ @@ -197,9 +203,17 @@ export class ContainerConfigLoader { .toConstantValue(new DomainEventFactory(container.get(TYPES.Revisions_Timer))) // Map + container + .bind>( + TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper, + ) + .toConstantValue(new SQLLegacyRevisionMetadataPersistenceMapper()) container .bind>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper) .toConstantValue(new SQLRevisionMetadataPersistenceMapper()) + container + .bind>(TYPES.Revisions_SQLLegacyRevisionPersistenceMapper) + .toConstantValue(new SQLLegacyRevisionPersistenceMapper()) container .bind>(TYPES.Revisions_SQLRevisionPersistenceMapper) .toConstantValue(new SQLRevisionPersistenceMapper()) @@ -213,22 +227,36 @@ export class ContainerConfigLoader { .toConstantValue(new MongoDBRevisionPersistenceMapper()) // ORM + container + .bind>(TYPES.Revisions_ORMLegacyRevisionRepository) + .toDynamicValue(() => appDataSource.getRepository(SQLLegacyRevision)) container .bind>(TYPES.Revisions_ORMRevisionRepository) - .toDynamicValue(() => appDataSource.getRepository(SQLRevision)) + .toConstantValue(appDataSource.getRepository(SQLRevision)) // Repositories container .bind(TYPES.Revisions_SQLRevisionRepository) .toConstantValue( - new SQLRevisionRepository( - container.get>(TYPES.Revisions_ORMRevisionRepository), - container.get>( - TYPES.Revisions_SQLRevisionMetadataPersistenceMapper, - ), - container.get>(TYPES.Revisions_SQLRevisionPersistenceMapper), - container.get(TYPES.Revisions_Logger), - ), + isConfiguredForHomeServerOrSelfHosting + ? new SQLRevisionRepository( + container.get>(TYPES.Revisions_ORMRevisionRepository), + container.get>( + TYPES.Revisions_SQLRevisionMetadataPersistenceMapper, + ), + container.get>(TYPES.Revisions_SQLRevisionPersistenceMapper), + container.get(TYPES.Revisions_Logger), + ) + : new SQLLegacyRevisionRepository( + container.get>(TYPES.Revisions_ORMLegacyRevisionRepository), + container.get>( + TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper, + ), + container.get>( + TYPES.Revisions_SQLLegacyRevisionPersistenceMapper, + ), + container.get(TYPES.Revisions_Logger), + ), ) if (isSecondaryDatabaseEnabled) { diff --git a/packages/revisions/src/Bootstrap/DataSource.ts b/packages/revisions/src/Bootstrap/DataSource.ts index d909b27e3..3303359e5 100644 --- a/packages/revisions/src/Bootstrap/DataSource.ts +++ b/packages/revisions/src/Bootstrap/DataSource.ts @@ -1,11 +1,12 @@ import { DataSource, EntityTarget, LoggerOptions, MongoRepository, ObjectLiteral, Repository } from 'typeorm' import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions' -import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision' +import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision' import { Env } from './Env' import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions' import { MongoDBRevision } from '../Infra/TypeORM/MongoDB/MongoDBRevision' +import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision' export class AppDataSource { private _dataSource: DataSource | undefined @@ -65,14 +66,23 @@ export class AppDataSource { const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql' + const isConfiguredForHomeServerOrSelfHosting = + this.env.get('MODE', true) === 'home-server' || this.env.get('MODE', true) === 'self-hosted' + const maxQueryExecutionTime = this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true) ? +this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true) : 45_000 + const migrationsSourceDirectoryName = isConfiguredForMySQL + ? isConfiguredForHomeServerOrSelfHosting + ? 'mysql' + : 'mysql-legacy' + : 'sqlite' + const commonDataSourceOptions = { maxQueryExecutionTime, - entities: [SQLRevision], - migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`], + entities: [isConfiguredForHomeServerOrSelfHosting ? SQLRevision : SQLLegacyRevision], + migrations: [`${__dirname}/../../migrations/${migrationsSourceDirectoryName}/*.js`], migrationsRun: true, logging: this.env.get('DB_DEBUG_LEVEL', true) ?? 'info', } diff --git a/packages/revisions/src/Bootstrap/MigrationsDataSource.ts b/packages/revisions/src/Bootstrap/MigrationsDataSource.ts new file mode 100644 index 000000000..b69bd4497 --- /dev/null +++ b/packages/revisions/src/Bootstrap/MigrationsDataSource.ts @@ -0,0 +1,7 @@ +import { AppDataSource } from './DataSource' +import { Env } from './Env' + +const env: Env = new Env() +env.load() + +export const MigrationsDataSource = new AppDataSource(env).dataSource diff --git a/packages/revisions/src/Bootstrap/Types.ts b/packages/revisions/src/Bootstrap/Types.ts index 6d8f7d939..e907fe1f5 100644 --- a/packages/revisions/src/Bootstrap/Types.ts +++ b/packages/revisions/src/Bootstrap/Types.ts @@ -6,7 +6,11 @@ const TYPES = { Revisions_S3: Symbol.for('Revisions_S3'), Revisions_Env: Symbol.for('Revisions_Env'), // Map + Revisions_SQLLegacyRevisionMetadataPersistenceMapper: Symbol.for( + 'Revisions_SQLLegacyRevisionMetadataPersistenceMapper', + ), Revisions_SQLRevisionMetadataPersistenceMapper: Symbol.for('Revisions_SQLRevisionMetadataPersistenceMapper'), + Revisions_SQLLegacyRevisionPersistenceMapper: Symbol.for('Revisions_SQLLegacyRevisionPersistenceMapper'), Revisions_SQLRevisionPersistenceMapper: Symbol.for('Revisions_SQLRevisionPersistenceMapper'), Revisions_MongoDBRevisionMetadataPersistenceMapper: Symbol.for('Revisions_MongoDBRevisionMetadataPersistenceMapper'), Revisions_MongoDBRevisionPersistenceMapper: Symbol.for('Revisions_MongoDBRevisionPersistenceMapper'), @@ -15,6 +19,7 @@ const TYPES = { Revisions_RevisionMetadataHttpMapper: Symbol.for('Revisions_RevisionMetadataHttpMapper'), // ORM Revisions_ORMRevisionRepository: Symbol.for('Revisions_ORMRevisionRepository'), + Revisions_ORMLegacyRevisionRepository: Symbol.for('Revisions_ORMLegacyRevisionRepository'), // Mongo Revisions_ORMMongoRevisionRepository: Symbol.for('Revisions_ORMMongoRevisionRepository'), // Repositories diff --git a/packages/revisions/src/Domain/KeySystem/KeySystemAssocationProps.ts b/packages/revisions/src/Domain/KeySystem/KeySystemAssocationProps.ts new file mode 100644 index 000000000..0ae4beae5 --- /dev/null +++ b/packages/revisions/src/Domain/KeySystem/KeySystemAssocationProps.ts @@ -0,0 +1,3 @@ +export interface KeySystemAssociationProps { + keySystemIdentifier: string +} diff --git a/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.spec.ts b/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.spec.ts new file mode 100644 index 000000000..a50be92d1 --- /dev/null +++ b/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.spec.ts @@ -0,0 +1,15 @@ +import { KeySystemAssociation } from './KeySystemAssociation' + +describe('KeySystemAssociation', () => { + it('should create a value object', () => { + const entityOrError = KeySystemAssociation.create('00000000-0000-0000-0000-000000000000') + + expect(entityOrError.isFailed()).toBeFalsy() + }) + + it('should fail to create a value object with an empty key system identifier', () => { + const entityOrError = KeySystemAssociation.create('') + + expect(entityOrError.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.ts b/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.ts new file mode 100644 index 000000000..b0eea3f88 --- /dev/null +++ b/packages/revisions/src/Domain/KeySystem/KeySystemAssociation.ts @@ -0,0 +1,18 @@ +import { Result, Validator, ValueObject } from '@standardnotes/domain-core' + +import { KeySystemAssociationProps } from './KeySystemAssocationProps' + +export class KeySystemAssociation extends ValueObject { + private constructor(props: KeySystemAssociationProps) { + super(props) + } + + static create(keySystemIdentifier: string): Result { + const validationResult = Validator.isNotEmptyString(keySystemIdentifier) + if (validationResult.isFailed()) { + return Result.fail(validationResult.getError()) + } + + return Result.ok(new KeySystemAssociation({ keySystemIdentifier })) + } +} diff --git a/packages/revisions/src/Domain/Revision/RevisionProps.ts b/packages/revisions/src/Domain/Revision/RevisionProps.ts index e3db9f713..58136c3ec 100644 --- a/packages/revisions/src/Domain/Revision/RevisionProps.ts +++ b/packages/revisions/src/Domain/Revision/RevisionProps.ts @@ -1,5 +1,8 @@ import { ContentType, Dates, Uuid } from '@standardnotes/domain-core' +import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation' + export interface RevisionProps { itemUuid: Uuid userUuid: Uuid | null @@ -10,4 +13,6 @@ export interface RevisionProps { authHash: string | null creationDate: Date dates: Dates + sharedVaultAssociation?: SharedVaultAssociation + keySystemAssociation?: KeySystemAssociation } diff --git a/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts index f94798345..450f28a00 100644 --- a/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts +++ b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts @@ -9,7 +9,7 @@ export interface RevisionRepositoryInterface { removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findByItemUuid(itemUuid: Uuid): Promise> - findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise> + findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise> updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise> insert(revision: Revision): Promise diff --git a/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.spec.ts b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.spec.ts new file mode 100644 index 000000000..b1ee24d52 --- /dev/null +++ b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.spec.ts @@ -0,0 +1,14 @@ +import { Uuid } from '@standardnotes/domain-core' + +import { SharedVaultAssociation } from './SharedVaultAssociation' + +describe('SharedVaultAssociation', () => { + it('should create a value object', () => { + const entityOrError = SharedVaultAssociation.create({ + editedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + }) + + expect(entityOrError.isFailed()).toBeFalsy() + }) +}) diff --git a/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.ts b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.ts new file mode 100644 index 000000000..87da0e1d9 --- /dev/null +++ b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.ts @@ -0,0 +1,13 @@ +import { Result, ValueObject } from '@standardnotes/domain-core' + +import { SharedVaultAssociationProps } from './SharedVaultAssociationProps' + +export class SharedVaultAssociation extends ValueObject { + private constructor(props: SharedVaultAssociationProps) { + super(props) + } + + static create(props: SharedVaultAssociationProps): Result { + return Result.ok(new SharedVaultAssociation(props)) + } +} diff --git a/packages/revisions/src/Domain/SharedVault/SharedVaultAssociationProps.ts b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociationProps.ts new file mode 100644 index 000000000..f2129dd9a --- /dev/null +++ b/packages/revisions/src/Domain/SharedVault/SharedVaultAssociationProps.ts @@ -0,0 +1,6 @@ +import { Uuid } from '@standardnotes/domain-core' + +export interface SharedVaultAssociationProps { + editedBy: Uuid + sharedVaultUuid: Uuid +} diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts index d6828113a..e2150d399 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts @@ -22,17 +22,30 @@ describe('GetRevisionsMetada', () => { itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', roleNames: ['CORE_USER'], + sharedVaultUuids: ['84c0f8e8-544a-4c7e-9adf-26209303bc1d'], }) expect(result.isFailed()).toBeFalsy() expect(result.getValue().length).toEqual(1) }) + it('should not return revisions metadata for an invalid shared vault uuid', async () => { + const result = await createUseCase().execute({ + itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', + userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', + roleNames: ['CORE_USER'], + sharedVaultUuids: ['1-2-3'], + }) + + expect(result.isFailed()).toBeTruthy() + }) + it('should not return revisions metadata for a an invalid item uuid', async () => { const result = await createUseCase().execute({ itemUuid: '1-2-3', userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', roleNames: ['CORE_USER'], + sharedVaultUuids: [], }) expect(result.isFailed()).toBeTruthy() @@ -43,6 +56,7 @@ describe('GetRevisionsMetada', () => { userUuid: '1-2-3', itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', roleNames: ['CORE_USER'], + sharedVaultUuids: [], }) expect(result.isFailed()).toBeTruthy() @@ -53,6 +67,7 @@ describe('GetRevisionsMetada', () => { itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', roleNames: ['INVALID_ROLE_NAME'], + sharedVaultUuids: [], }) expect(result.isFailed()).toBeTruthy() diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts index 5b7735ea5..4ad6459fe 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts @@ -19,6 +19,15 @@ export class GetRevisionsMetada implements UseCaseInterface return Result.fail(`Could not get revisions: ${userUuidOrError.getError()}`) } + const sharedVaultUuids = [] + for (const sharedVaultUuid of dto.sharedVaultUuids) { + const sharedVaultUuidOrError = Uuid.create(sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(`Could not get revisions: ${sharedVaultUuidOrError.getError()}`) + } + sharedVaultUuids.push(sharedVaultUuidOrError.getValue()) + } + const roleNamesOrError = RoleNameCollection.create(dto.roleNames) if (roleNamesOrError.isFailed()) { return Result.fail(roleNamesOrError.getError()) @@ -30,6 +39,7 @@ export class GetRevisionsMetada implements UseCaseInterface const revisionsMetdata = await revisionRepository.findMetadataByItemId( itemUuidOrError.getValue(), userUuidOrError.getValue(), + sharedVaultUuids, ) return Result.ok(revisionsMetdata) diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts index 1cf80681e..c0b738168 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts @@ -1,5 +1,6 @@ export interface GetRevisionsMetadaDTO { itemUuid: string + sharedVaultUuids: string[] userUuid: string roleNames: string[] } diff --git a/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts b/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts index acfda1685..82340ea3d 100644 --- a/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts +++ b/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts @@ -38,6 +38,9 @@ export class BaseRevisionsController extends BaseHttpController { itemUuid: request.params.itemUuid, userUuid: response.locals.user.uuid, roleNames: response.locals.roles.map((role: Role) => role.name), + sharedVaultUuids: response.locals.belongsToSharedVaults.map( + (association: { shared_vault_uuid: string; permission: string }) => association.shared_vault_uuid, + ), }) if (revisionMetadataOrError.isFailed()) { diff --git a/packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts b/packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts index c722b9aff..bf484c761 100644 --- a/packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts +++ b/packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts @@ -47,6 +47,7 @@ export class ApiGatewayAuthMiddleware extends BaseMiddleware { response.locals.roles = token.roles response.locals.session = token.session response.locals.readOnlyAccess = token.session?.readonly_access ?? false + response.locals.belongsToSharedVaults = token.belongs_to_shared_vaults ?? [] return next() } catch (error) { diff --git a/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts b/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts index 757c3c325..44c8310c8 100644 --- a/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts +++ b/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts @@ -37,4 +37,14 @@ export class MongoDBRevision { @Column() declare updatedAt: Date + + @Column() + declare editedBy: string | null + + @Column() + @Index('index_revisions_on_shared_vault_uuid') + declare sharedVaultUuid: string | null + + @Column() + declare keySystemIdentifier: string | null } diff --git a/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts b/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts index b88a08646..60561a832 100644 --- a/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts +++ b/packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts @@ -86,16 +86,36 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface { return revisions } - async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise { - const persistence = await this.mongoRepository.find({ - select: ['_id', 'contentType', 'createdAt', 'updatedAt'], - where: { - $and: [{ itemUuid: { $eq: itemUuid.value } }, { userUuid: { $eq: userUuid.value } }], - }, - order: { - createdAt: 'DESC', - }, - }) + async findMetadataByItemId( + itemUuid: Uuid, + userUuid: Uuid, + sharedVaultUuids: Uuid[], + ): Promise> { + let persistence = [] + if (sharedVaultUuids.length > 0) { + persistence = await this.mongoRepository.find({ + select: ['_id', 'contentType', 'createdAt', 'updatedAt'], + where: { + $and: [ + { itemUuid: { $eq: itemUuid.value } }, + { sharedVaultUuid: { $in: sharedVaultUuids.map((uuid) => uuid.value) } }, + ], + }, + order: { + createdAt: 'DESC', + }, + }) + } else { + persistence = await this.mongoRepository.find({ + select: ['_id', 'contentType', 'createdAt', 'updatedAt'], + where: { + $and: [{ itemUuid: { $eq: itemUuid.value } }, { userUuid: { $eq: userUuid.value } }], + }, + order: { + createdAt: 'DESC', + }, + }) + } const revisions: RevisionMetadata[] = [] diff --git a/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevision.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevision.ts new file mode 100644 index 000000000..c93715819 --- /dev/null +++ b/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevision.ts @@ -0,0 +1,83 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'revisions' }) +export class SQLLegacyRevision { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + name: 'item_uuid', + length: 36, + }) + @Index('item_uuid') + declare itemUuid: string + + @Column({ + name: 'user_uuid', + length: 36, + type: 'varchar', + nullable: true, + }) + @Index('user_uuid') + declare userUuid: string | null + + @Column({ + type: 'text', + nullable: true, + }) + declare content: string | null + + @Column({ + name: 'content_type', + type: 'varchar', + length: 255, + nullable: true, + }) + declare contentType: string | null + + @Column({ + type: 'varchar', + name: 'items_key_id', + length: 255, + nullable: true, + }) + declare itemsKeyId: string | null + + @Column({ + name: 'enc_item_key', + type: 'text', + nullable: true, + }) + declare encItemKey: string | null + + @Column({ + name: 'auth_hash', + type: 'varchar', + length: 255, + nullable: true, + }) + declare authHash: string | null + + @Column({ + name: 'creation_date', + type: 'date', + nullable: true, + }) + declare creationDate: Date + + @Column({ + name: 'created_at', + type: 'datetime', + precision: 6, + nullable: true, + }) + declare createdAt: Date + + @Column({ + name: 'updated_at', + type: 'datetime', + precision: 6, + nullable: true, + }) + declare updatedAt: Date +} diff --git a/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts new file mode 100644 index 000000000..ec7dd5fba --- /dev/null +++ b/packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts @@ -0,0 +1,146 @@ +import { MapperInterface, Uuid } from '@standardnotes/domain-core' +import { Repository } from 'typeorm' +import { Logger } from 'winston' + +import { Revision } from '../../../Domain/Revision/Revision' +import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata' +import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface' +import { SQLLegacyRevision } from './SQLLegacyRevision' + +export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface { + constructor( + protected ormRepository: Repository, + protected revisionMetadataMapper: MapperInterface, + protected revisionMapper: MapperInterface, + protected logger: Logger, + ) {} + + async countByUserUuid(userUuid: Uuid): Promise { + return this.ormRepository + .createQueryBuilder() + .where('user_uuid = :userUuid', { userUuid: userUuid.value }) + .getCount() + } + + async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise { + const queryBuilder = this.ormRepository + .createQueryBuilder('revision') + .where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value }) + .orderBy('revision.uuid', 'ASC') + + if (dto.offset !== undefined) { + queryBuilder.skip(dto.offset) + } + + if (dto.limit !== undefined) { + queryBuilder.take(dto.limit) + } + + const sqlRevisions = await queryBuilder.getMany() + + const revisions = [] + for (const sqlRevision of sqlRevisions) { + revisions.push(this.revisionMapper.toDomain(sqlRevision)) + } + + return revisions + } + + async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise { + await this.ormRepository + .createQueryBuilder() + .update() + .set({ + userUuid: userUuid.value, + }) + .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) + .execute() + } + + async findByItemUuid(itemUuid: Uuid): Promise { + const SQLLegacyRevisions = await this.ormRepository + .createQueryBuilder() + .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) + .getMany() + + const revisions = [] + for (const revision of SQLLegacyRevisions) { + revisions.push(this.revisionMapper.toDomain(revision)) + } + + return revisions + } + + async removeByUserUuid(userUuid: Uuid): Promise { + await this.ormRepository + .createQueryBuilder() + .delete() + .from('revisions') + .where('user_uuid = :userUuid', { userUuid: userUuid.value }) + .execute() + } + + async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { + await this.ormRepository + .createQueryBuilder() + .delete() + .from('revisions') + .where('uuid = :revisionUuid AND user_uuid = :userUuid', { + userUuid: userUuid.value, + revisionUuid: revisionUuid.value, + }) + .execute() + } + + async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { + const SQLLegacyRevision = await this.ormRepository + .createQueryBuilder() + .where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value }) + .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value }) + .getOne() + + if (SQLLegacyRevision === null) { + return null + } + + return this.revisionMapper.toDomain(SQLLegacyRevision) + } + + async insert(revision: Revision): Promise { + const SQLLegacyRevision = this.revisionMapper.toProjection(revision) + + await this.ormRepository.insert(SQLLegacyRevision) + + return true + } + + async findMetadataByItemId( + itemUuid: Uuid, + userUuid: Uuid, + _sharedVaultUuids: Uuid[], + ): Promise> { + const queryBuilder = this.ormRepository + .createQueryBuilder() + .select('uuid', 'uuid') + .addSelect('content_type', 'contentType') + .addSelect('created_at', 'createdAt') + .addSelect('updated_at', 'updatedAt') + .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) + .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value }) + .orderBy('created_at', 'DESC') + + const simplifiedRevisions = await queryBuilder.getRawMany() + + this.logger.debug( + `Found ${simplifiedRevisions.length} revisions entries for item ${itemUuid.value}`, + simplifiedRevisions, + ) + + const metadata = [] + for (const simplifiedRevision of simplifiedRevisions) { + metadata.push(this.revisionMetadataMapper.toDomain(simplifiedRevision)) + } + + return metadata + } +} diff --git a/packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts index b3ef58147..d0c61fcde 100644 --- a/packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts +++ b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts @@ -1,83 +1,31 @@ -import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' +import { Column, Entity, Index } from 'typeorm' + +import { SQLLegacyRevision } from './SQLLegacyRevision' @Entity({ name: 'revisions' }) -export class SQLRevision { - @PrimaryGeneratedColumn('uuid') - declare uuid: string - +export class SQLRevision extends SQLLegacyRevision { @Column({ - name: 'item_uuid', + type: 'varchar', + name: 'edited_by', length: 36, + nullable: true, }) - @Index('item_uuid') - declare itemUuid: string + declare editedBy: string | null @Column({ - name: 'user_uuid', + type: 'varchar', + name: 'shared_vault_uuid', length: 36, - type: 'varchar', nullable: true, }) - @Index('user_uuid') - declare userUuid: string | null - - @Column({ - type: 'text', - nullable: true, - }) - declare content: string | null - - @Column({ - name: 'content_type', - type: 'varchar', - length: 255, - nullable: true, - }) - declare contentType: string | null + @Index('index_revisions_on_shared_vault_uuid') + declare sharedVaultUuid: string | null @Column({ type: 'varchar', - name: 'items_key_id', - length: 255, + name: 'key_system_identifier', + length: 36, nullable: true, }) - declare itemsKeyId: string | null - - @Column({ - name: 'enc_item_key', - type: 'text', - nullable: true, - }) - declare encItemKey: string | null - - @Column({ - name: 'auth_hash', - type: 'varchar', - length: 255, - nullable: true, - }) - declare authHash: string | null - - @Column({ - name: 'creation_date', - type: 'date', - nullable: true, - }) - declare creationDate: Date - - @Column({ - name: 'created_at', - type: 'datetime', - precision: 6, - nullable: true, - }) - declare createdAt: Date - - @Column({ - name: 'updated_at', - type: 'datetime', - precision: 6, - nullable: true, - }) - declare updatedAt: Date + declare keySystemIdentifier: string | null } diff --git a/packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts index 47e5a0af7..49ee3ac81 100644 --- a/packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts +++ b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts @@ -4,127 +4,40 @@ import { Logger } from 'winston' import { Revision } from '../../../Domain/Revision/Revision' import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata' -import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface' +import { SQLLegacyRevisionRepository } from './SQLLegacyRevisionRepository' import { SQLRevision } from './SQLRevision' -export class SQLRevisionRepository implements RevisionRepositoryInterface { +export class SQLRevisionRepository extends SQLLegacyRevisionRepository { constructor( - private ormRepository: Repository, - private revisionMetadataMapper: MapperInterface, - private revisionMapper: MapperInterface, - private logger: Logger, - ) {} - - async countByUserUuid(userUuid: Uuid): Promise { - return this.ormRepository - .createQueryBuilder() - .where('user_uuid = :userUuid', { userUuid: userUuid.value }) - .getCount() + protected override ormRepository: Repository, + protected override revisionMetadataMapper: MapperInterface, + protected override revisionMapper: MapperInterface, + protected override logger: Logger, + ) { + super(ormRepository, revisionMetadataMapper, revisionMapper, logger) } - async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise { - const queryBuilder = this.ormRepository - .createQueryBuilder('revision') - .where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value }) - .orderBy('revision.uuid', 'ASC') - - if (dto.offset !== undefined) { - queryBuilder.skip(dto.offset) - } - - if (dto.limit !== undefined) { - queryBuilder.take(dto.limit) - } - - const sqlRevisions = await queryBuilder.getMany() - - const revisions = [] - for (const sqlRevision of sqlRevisions) { - revisions.push(this.revisionMapper.toDomain(sqlRevision)) - } - - return revisions - } - - async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise { - await this.ormRepository - .createQueryBuilder() - .update() - .set({ - userUuid: userUuid.value, - }) - .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) - .execute() - } - - async findByItemUuid(itemUuid: Uuid): Promise { - const SQLRevisions = await this.ormRepository - .createQueryBuilder() - .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) - .getMany() - - const revisions = [] - for (const revision of SQLRevisions) { - revisions.push(this.revisionMapper.toDomain(revision)) - } - - return revisions - } - - async removeByUserUuid(userUuid: Uuid): Promise { - await this.ormRepository - .createQueryBuilder() - .delete() - .from('revisions') - .where('user_uuid = :userUuid', { userUuid: userUuid.value }) - .execute() - } - - async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { - await this.ormRepository - .createQueryBuilder() - .delete() - .from('revisions') - .where('uuid = :revisionUuid AND user_uuid = :userUuid', { - userUuid: userUuid.value, - revisionUuid: revisionUuid.value, - }) - .execute() - } - - async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { - const SQLRevision = await this.ormRepository - .createQueryBuilder() - .where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value }) - .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value }) - .getOne() - - if (SQLRevision === null) { - return null - } - - return this.revisionMapper.toDomain(SQLRevision) - } - - async insert(revision: Revision): Promise { - const SQLRevision = this.revisionMapper.toProjection(revision) - - await this.ormRepository.insert(SQLRevision) - - return true - } - - async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise> { + override async findMetadataByItemId( + itemUuid: Uuid, + userUuid: Uuid, + sharedVaultUuids: Uuid[], + ): Promise> { const queryBuilder = this.ormRepository .createQueryBuilder() .select('uuid', 'uuid') .addSelect('content_type', 'contentType') .addSelect('created_at', 'createdAt') .addSelect('updated_at', 'updatedAt') - .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value }) - .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value }) + .where('item_uuid = :itemUuid AND user_uuid = :userUuid', { itemUuid: itemUuid.value, userUuid: userUuid.value }) .orderBy('created_at', 'DESC') + if (sharedVaultUuids.length > 0) { + queryBuilder.orWhere('item_uuid = :itemUuid AND shared_vault_uuid IN (:...sharedVaultUuids)', { + itemUuid: itemUuid.value, + sharedVaultUuids: sharedVaultUuids.map((uuid) => uuid.value), + }) + } + const simplifiedRevisions = await queryBuilder.getRawMany() this.logger.debug( diff --git a/packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts b/packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts index 490757508..9839e3262 100644 --- a/packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts +++ b/packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts @@ -1,6 +1,8 @@ import { MapperInterface, Dates, Uuid, ContentType } from '@standardnotes/domain-core' import { Revision } from '../../Domain/Revision/Revision' +import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation' export class RevisionItemStringMapper implements MapperInterface { toDomain(projection: string): Revision { @@ -24,6 +26,39 @@ export class RevisionItemStringMapper implements MapperInterface +{ + toDomain(projection: SQLLegacyRevision): RevisionMetadata { + const contentTypeOrError = ContentType.create(projection.contentType) + if (contentTypeOrError.isFailed()) { + throw new Error(`Could not create content type: ${contentTypeOrError.getError()}`) + } + const contentType = contentTypeOrError.getValue() + + const createdAt = projection.createdAt instanceof Date ? projection.createdAt : new Date(projection.createdAt) + const updatedAt = projection.updatedAt instanceof Date ? projection.updatedAt : new Date(projection.updatedAt) + + const datesOrError = Dates.create(createdAt, updatedAt) + if (datesOrError.isFailed()) { + throw new Error(`Could not create dates: ${datesOrError.getError()}`) + } + const dates = datesOrError.getValue() + + const revisionMetadataOrError = RevisionMetadata.create( + { + contentType, + dates, + }, + new UniqueEntityId(projection.uuid), + ) + + if (revisionMetadataOrError.isFailed()) { + throw new Error(`Could not create revision metdata: ${revisionMetadataOrError.getError()}`) + } + + return revisionMetadataOrError.getValue() + } + + toProjection(_domain: RevisionMetadata): SQLLegacyRevision { + throw new Error('Method not implemented.') + } +} diff --git a/packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper.ts b/packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper.ts new file mode 100644 index 000000000..8f52d0599 --- /dev/null +++ b/packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper.ts @@ -0,0 +1,73 @@ +import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core' + +import { Revision } from '../../../Domain/Revision/Revision' +import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision' + +export class SQLLegacyRevisionPersistenceMapper implements MapperInterface { + toDomain(projection: SQLLegacyRevision): Revision { + const contentTypeOrError = ContentType.create(projection.contentType) + if (contentTypeOrError.isFailed()) { + throw new Error(`Could not map typeorm revision to domain revision: ${contentTypeOrError.getError()}`) + } + const contentType = contentTypeOrError.getValue() + + const datesOrError = Dates.create(projection.createdAt, projection.updatedAt) + if (datesOrError.isFailed()) { + throw new Error(`Could not map typeorm revision to domain revision: ${datesOrError.getError()}`) + } + const dates = datesOrError.getValue() + + const itemUuidOrError = Uuid.create(projection.itemUuid) + if (itemUuidOrError.isFailed()) { + throw new Error(`Could not map typeorm revision to domain revision: ${itemUuidOrError.getError()}`) + } + const itemUuid = itemUuidOrError.getValue() + + let userUuid = null + if (projection.userUuid !== null) { + const userUuidOrError = Uuid.create(projection.userUuid) + if (userUuidOrError.isFailed()) { + throw new Error(`Could not map typeorm revision to domain revision: ${userUuidOrError.getError()}`) + } + userUuid = userUuidOrError.getValue() + } + + const revisionOrError = Revision.create( + { + authHash: projection.authHash, + content: projection.content, + contentType, + creationDate: projection.creationDate, + encItemKey: projection.encItemKey, + itemsKeyId: projection.itemsKeyId, + itemUuid, + userUuid, + dates, + }, + new UniqueEntityId(projection.uuid), + ) + if (revisionOrError.isFailed()) { + throw new Error(`Could not map typeorm revision to domain revision: ${revisionOrError.getError()}`) + } + + return revisionOrError.getValue() + } + + toProjection(domain: Revision): SQLLegacyRevision { + const sqlRevision = new SQLLegacyRevision() + + sqlRevision.authHash = domain.props.authHash + sqlRevision.content = domain.props.content + sqlRevision.contentType = domain.props.contentType.value + sqlRevision.createdAt = domain.props.dates.createdAt + sqlRevision.updatedAt = domain.props.dates.updatedAt + sqlRevision.creationDate = domain.props.creationDate + sqlRevision.encItemKey = domain.props.encItemKey + sqlRevision.itemUuid = domain.props.itemUuid.value + sqlRevision.itemsKeyId = domain.props.itemsKeyId + sqlRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null + sqlRevision.uuid = domain.id.toString() + + return sqlRevision + } +} diff --git a/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts b/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts index 7f16e0d7a..0f84be579 100644 --- a/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts +++ b/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts @@ -2,6 +2,8 @@ import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@stan import { Revision } from '../../../Domain/Revision/Revision' import { SQLRevision } from '../../../Infra/TypeORM/SQL/SQLRevision' +import { SharedVaultAssociation } from '../../../Domain/SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../../../Domain/KeySystem/KeySystemAssociation' export class SQLRevisionPersistenceMapper implements MapperInterface { toDomain(projection: SQLRevision): Revision { @@ -32,6 +34,39 @@ export class SQLRevisionPersistenceMapper implements MapperInterface shared_vault_owner_context?: { upload_bytes_limit: number } diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 46e0299c1..becdc79e3 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -58,7 +58,12 @@ import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInte import { FSItemBackupService } from '../Infra/FS/FSItemBackupService' import { AuthHttpService } from '../Infra/HTTP/AuthHttpService' import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService' -import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' +import { + ControllerContainer, + ControllerContainerInterface, + MapperInterface, + SharedVaultUser, +} from '@standardnotes/domain-core' import { BaseItemsController } from '../Infra/InversifyExpressUtils/Base/BaseItemsController' import { Transform } from 'stream' import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem' @@ -87,7 +92,6 @@ import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser' import { SharedVaultRepositoryInterface } from '../Domain/SharedVault/SharedVaultRepositoryInterface' import { SharedVaultPersistenceMapper } from '../Mapping/Persistence/SharedVaultPersistenceMapper' import { SharedVault } from '../Domain/SharedVault/SharedVault' -import { SharedVaultUser } from '../Domain/SharedVault/User/SharedVaultUser' import { SharedVaultUserPersistenceMapper } from '../Mapping/Persistence/SharedVaultUserPersistenceMapper' import { SharedVaultInvite } from '../Domain/SharedVault/User/Invite/SharedVaultInvite' import { SharedVaultInvitePersistenceMapper } from '../Mapping/Persistence/SharedVaultInvitePersistenceMapper' @@ -699,9 +703,11 @@ export class ContainerConfigLoader { .bind(TYPES.Sync_AddUserToSharedVault) .toConstantValue( new AddUserToSharedVault( - container.get(TYPES.Sync_SharedVaultRepository), - container.get(TYPES.Sync_SharedVaultUserRepository), - container.get(TYPES.Sync_Timer), + container.get(TYPES.Sync_SharedVaultRepository), + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_Timer), + container.get(TYPES.Sync_DomainEventFactory), + container.get(TYPES.Sync_DomainEventPublisher), ), ) container @@ -746,9 +752,11 @@ export class ContainerConfigLoader { .bind(TYPES.Sync_RemoveSharedVaultUser) .toConstantValue( new RemoveUserFromSharedVault( - container.get(TYPES.Sync_SharedVaultUserRepository), - container.get(TYPES.Sync_SharedVaultRepository), - container.get(TYPES.Sync_AddNotificationForUser), + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_SharedVaultRepository), + container.get(TYPES.Sync_AddNotificationForUser), + container.get(TYPES.Sync_DomainEventFactory), + container.get(TYPES.Sync_DomainEventPublisher), ), ) container diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts index a7ebdb3c9..199375221 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts @@ -9,7 +9,9 @@ import { NotificationAddedForUserEvent, RevisionsCopyRequestedEvent, TransitionStatusUpdatedEvent, + UserAddedToSharedVaultEvent, UserInvitedToSharedVaultEvent, + UserRemovedFromSharedVaultEvent, WebSocketMessageRequestedEvent, } from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' @@ -18,6 +20,44 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface' export class DomainEventFactory implements DomainEventFactoryInterface { constructor(private timer: TimerInterface) {} + createUserRemovedFromSharedVaultEvent(dto: { + sharedVaultUuid: string + userUuid: string + }): UserRemovedFromSharedVaultEvent { + return { + type: 'USER_REMOVED_FROM_SHARED_VAULT', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: dto.userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.SyncingServer, + }, + payload: dto, + } + } + createUserAddedToSharedVaultEvent(dto: { + sharedVaultUuid: string + userUuid: string + permission: string + createdAt: number + updatedAt: number + }): UserAddedToSharedVaultEvent { + return { + type: 'USER_ADDED_TO_SHARED_VAULT', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: dto.userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.SyncingServer, + }, + payload: dto, + } + } + createUserInvitedToSharedVaultEvent(dto: { invite: { uuid: string diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts index 6500a738b..8aea7f561 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts @@ -7,7 +7,9 @@ import { NotificationAddedForUserEvent, RevisionsCopyRequestedEvent, TransitionStatusUpdatedEvent, + UserAddedToSharedVaultEvent, UserInvitedToSharedVaultEvent, + UserRemovedFromSharedVaultEvent, WebSocketMessageRequestedEvent, } from '@standardnotes/domain-events' @@ -80,4 +82,15 @@ export interface DomainEventFactoryInterface { userUuid: string, dto: { originalItemUuid: string; newItemUuid: string; roleNames: string[] }, ): RevisionsCopyRequestedEvent + createUserAddedToSharedVaultEvent(dto: { + sharedVaultUuid: string + userUuid: string + permission: string + createdAt: number + updatedAt: number + }): UserAddedToSharedVaultEvent + createUserRemovedFromSharedVaultEvent(dto: { + sharedVaultUuid: string + userUuid: string + }): UserRemovedFromSharedVaultEvent } diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts index f96c05380..44b086b08 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts @@ -6,8 +6,8 @@ import { Timestamps, UniqueEntityId, Uuid, + SharedVaultUser, } from '@standardnotes/domain-core' -import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface' import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem' import { SharedVaultFilter } from './SharedVaultFilter' diff --git a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserProps.ts b/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserProps.ts deleted file mode 100644 index 113107908..000000000 --- a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserProps.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' - -export interface SharedVaultUserProps { - sharedVaultUuid: Uuid - userUuid: Uuid - permission: SharedVaultUserPermission - timestamps: Timestamps -} diff --git a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts b/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts index 79b5b514a..4428c50b0 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts @@ -1,6 +1,4 @@ -import { Uuid } from '@standardnotes/domain-core' - -import { SharedVaultUser } from './SharedVaultUser' +import { SharedVaultUser, Uuid } from '@standardnotes/domain-core' export interface SharedVaultUserRepositoryInterface { findByUuid(sharedVaultUserUuid: Uuid): Promise diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts index 57f0a6c11..62c50a5e9 100644 --- a/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts @@ -5,8 +5,8 @@ import { Result, NotificationPayload, NotificationType, + SharedVaultUser, } from '@standardnotes/domain-core' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser' import { AddNotificationsForUsers } from './AddNotificationsForUsers' diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts index bb7c4c6f4..a74a3d285 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts @@ -1,20 +1,31 @@ import { TimerInterface } from '@standardnotes/time' +import { Result, SharedVaultUser } from '@standardnotes/domain-core' +import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events' + import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { AddUserToSharedVault } from './AddUserToSharedVault' -import { Result } from '@standardnotes/domain-core' import { SharedVault } from '../../../SharedVault/SharedVault' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' +import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' describe('AddUserToSharedVault', () => { let sharedVaultRepository: SharedVaultRepositoryInterface let sharedVaultUserRepository: SharedVaultUserRepositoryInterface let timer: TimerInterface let sharedVault: SharedVault + let domainEventFactory: DomainEventFactoryInterface + let domainEventPublisher: DomainEventPublisherInterface const validUuid = '00000000-0000-0000-0000-000000000000' - const createUseCase = () => new AddUserToSharedVault(sharedVaultRepository, sharedVaultUserRepository, timer) + const createUseCase = () => + new AddUserToSharedVault( + sharedVaultRepository, + sharedVaultUserRepository, + timer, + domainEventFactory, + domainEventPublisher, + ) beforeEach(() => { sharedVault = {} as jest.Mocked @@ -27,6 +38,14 @@ describe('AddUserToSharedVault', () => { timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789) + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createUserAddedToSharedVaultEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() }) it('should return a failure result if the shared vault uuid is invalid', async () => { diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts index 7b1df1233..fc2d13f50 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts @@ -1,16 +1,26 @@ -import { Result, SharedVaultUserPermission, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { + Result, + SharedVaultUser, + SharedVaultUserPermission, + Timestamps, + UseCaseInterface, + Uuid, +} from '@standardnotes/domain-core' import { TimerInterface } from '@standardnotes/time' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { AddUserToSharedVaultDTO } from './AddUserToSharedVaultDTO' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' +import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' export class AddUserToSharedVault implements UseCaseInterface { constructor( private sharedVaultRepository: SharedVaultRepositoryInterface, private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, private timer: TimerInterface, + private domainEventFactory: DomainEventFactoryInterface, + private domainEventPublisher: DomainEventPublisherInterface, ) {} async execute(dto: AddUserToSharedVaultDTO): Promise> { @@ -57,6 +67,16 @@ export class AddUserToSharedVault implements UseCaseInterface { await this.sharedVaultUserRepository.save(sharedVaultUser) + await this.domainEventPublisher.publish( + this.domainEventFactory.createUserAddedToSharedVaultEvent({ + sharedVaultUuid: sharedVaultUser.props.sharedVaultUuid.value, + userUuid: sharedVaultUser.props.userUuid.value, + permission: sharedVaultUser.props.permission.value, + createdAt: sharedVaultUser.props.timestamps.createdAt, + updatedAt: sharedVaultUser.props.timestamps.updatedAt, + }), + ) + return Result.ok(sharedVaultUser) } } diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts index a284655b9..7d9f912aa 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts @@ -1,5 +1,6 @@ +import { SharedVaultUser } from '@standardnotes/domain-core' + import { SharedVault } from '../../../SharedVault/SharedVault' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' export interface CreateSharedVaultResult { sharedVaultUser: SharedVaultUser diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts index 3d023893f..0852e6e1b 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts @@ -3,8 +3,7 @@ import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVault import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { CreateSharedVaultFileValetToken } from './CreateSharedVaultFileValetToken' import { SharedVault } from '../../../SharedVault/SharedVault' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' -import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' describe('CreateSharedVaultFileValetToken', () => { let sharedVaultRepository: SharedVaultRepositoryInterface diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts index 831c449e2..e63c3233d 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts @@ -1,11 +1,10 @@ -import { Uuid, Timestamps, Result, SharedVaultUserPermission } from '@standardnotes/domain-core' +import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { DeleteSharedVault } from './DeleteSharedVault' import { SharedVault } from '../../../SharedVault/SharedVault' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault' describe('DeleteSharedVault', () => { diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts index 3828f02fe..51fc68a86 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts @@ -1,7 +1,7 @@ -import { Uuid, Timestamps, SharedVaultUserPermission } from '@standardnotes/domain-core' +import { Uuid, Timestamps, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core' + import { SharedVault } from '../../../SharedVault/SharedVault' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { GetSharedVaultUsers } from './GetSharedVaultUsers' diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts index 11b0adba0..2d91b2a2b 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts @@ -1,5 +1,5 @@ -import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' +import { Result, SharedVaultUser, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + import { GetSharedVaultUsersDTO } from './GetSharedVaultUsersDTO' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts index a7f674e13..70180e606 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts @@ -1,7 +1,7 @@ -import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' +import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' + import { SharedVault } from '../../../SharedVault/SharedVault' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { GetSharedVaults } from './GetSharedVaults' diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts index 11ef445a9..56ccb5ee5 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts @@ -1,5 +1,6 @@ import { TimerInterface } from '@standardnotes/time' -import { Uuid, Timestamps, Result, SharedVaultUserPermission } from '@standardnotes/domain-core' +import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core' +import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events' import { Logger } from 'winston' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' @@ -8,10 +9,8 @@ import { InviteUserToSharedVault } from './InviteUserToSharedVault' import { SharedVault } from '../../../SharedVault/SharedVault' import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient' -import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events' describe('InviteUserToSharedVault', () => { let sharedVaultRepository: SharedVaultRepositoryInterface diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts index fad9339bd..0aa883e24 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts @@ -1,11 +1,19 @@ -import { Uuid, Timestamps, Result, NotificationPayload, SharedVaultUserPermission } from '@standardnotes/domain-core' +import { + Uuid, + Timestamps, + Result, + NotificationPayload, + SharedVaultUserPermission, + SharedVaultUser, +} from '@standardnotes/domain-core' import { SharedVault } from '../../../SharedVault/SharedVault' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' -import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { RemoveUserFromSharedVault } from './RemoveUserFromSharedVault' import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser' +import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' +import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events' describe('RemoveUserFromSharedVault', () => { let sharedVaultRepository: SharedVaultRepositoryInterface @@ -13,9 +21,17 @@ describe('RemoveUserFromSharedVault', () => { let addNotificationForUser: AddNotificationForUser let sharedVault: SharedVault let sharedVaultUser: SharedVaultUser + let domainEventFactory: DomainEventFactoryInterface + let domainEventPublisher: DomainEventPublisherInterface const createUseCase = () => - new RemoveUserFromSharedVault(sharedVaultUserRepository, sharedVaultRepository, addNotificationForUser) + new RemoveUserFromSharedVault( + sharedVaultUserRepository, + sharedVaultRepository, + addNotificationForUser, + domainEventFactory, + domainEventPublisher, + ) beforeEach(() => { sharedVault = SharedVault.create({ @@ -39,6 +55,14 @@ describe('RemoveUserFromSharedVault', () => { addNotificationForUser = {} as jest.Mocked addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok()) + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createUserRemovedFromSharedVaultEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() }) it('should remove user from shared vault', async () => { diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts index 8859dc6eb..38d0cb50a 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts @@ -1,15 +1,19 @@ import { NotificationPayload, NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { RemoveUserFromSharedVaultDTO } from './RemoveUserFromSharedVaultDTO' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser' +import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' export class RemoveUserFromSharedVault implements UseCaseInterface { constructor( private sharedVaultUsersRepository: SharedVaultUserRepositoryInterface, private sharedVaultRepository: SharedVaultRepositoryInterface, private addNotificationForUser: AddNotificationForUser, + private domainEventFactory: DomainEventFactoryInterface, + private domainEventPublisher: DomainEventPublisherInterface, ) {} async execute(dto: RemoveUserFromSharedVaultDTO): Promise> { @@ -77,6 +81,13 @@ export class RemoveUserFromSharedVault implements UseCaseInterface { return Result.fail(result.getError()) } + await this.domainEventPublisher.publish( + this.domainEventFactory.createUserRemovedFromSharedVaultEvent({ + sharedVaultUuid: dto.sharedVaultUuid, + userUuid: dto.userUuid, + }), + ) + return Result.ok() } } diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts index c136cdb2d..a4fbd3953 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts @@ -1,11 +1,10 @@ import { controller, httpDelete, httpGet, results } from 'inversify-express-utils' import { inject } from 'inversify' -import { MapperInterface } from '@standardnotes/domain-core' +import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core' import { Request, Response } from 'express' import { BaseSharedVaultUsersController } from './Base/BaseSharedVaultUsersController' import TYPES from '../../Bootstrap/Types' -import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser' import { SharedVaultUserHttpRepresentation } from '../../Mapping/Http/SharedVaultUserHttpRepresentation' import { GetSharedVaultUsers } from '../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers' import { RemoveUserFromSharedVault } from '../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault' diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts index 9c2e73e6f..b70ab123f 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts @@ -1,12 +1,11 @@ import { controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils' import { inject } from 'inversify' -import { MapperInterface } from '@standardnotes/domain-core' +import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core' import { Request, Response } from 'express' import { BaseSharedVaultsController } from './Base/BaseSharedVaultsController' import TYPES from '../../Bootstrap/Types' import { SharedVault } from '../../Domain/SharedVault/SharedVault' -import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser' import { CreateSharedVault } from '../../Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault' import { CreateSharedVaultFileValetToken } from '../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken' import { DeleteSharedVault } from '../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault' diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts index d43cd777a..76caf9aed 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts @@ -128,6 +128,8 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { ) } + response.setHeader('x-invalidate-cache', response.locals.user.uuid) + return this.json({ success: true, }) diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts index 2439c2b00..e01ebafbd 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts @@ -1,9 +1,8 @@ import { Request, Response } from 'express' import { BaseHttpController, results } from 'inversify-express-utils' import { HttpStatusCode } from '@standardnotes/responses' -import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' +import { ControllerContainerInterface, MapperInterface, SharedVaultUser } from '@standardnotes/domain-core' -import { SharedVaultUser } from '../../../Domain/SharedVault/User/SharedVaultUser' import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation' import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers' import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault' @@ -66,6 +65,8 @@ export class BaseSharedVaultUsersController extends BaseHttpController { ) } + response.setHeader('x-invalidate-cache', response.locals.user.uuid) + return this.json({ success: true, }) diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts index 9f11e15ea..ad0f6e26d 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts @@ -1,14 +1,13 @@ import { Request, Response } from 'express' import { BaseHttpController, results } from 'inversify-express-utils' import { HttpStatusCode } from '@standardnotes/responses' -import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' +import { ControllerContainerInterface, MapperInterface, SharedVaultUser } from '@standardnotes/domain-core' import { Role } from '@standardnotes/security' import { GetSharedVaults } from '../../../Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults' import { SharedVault } from '../../../Domain/SharedVault/SharedVault' import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation' import { CreateSharedVault } from '../../../Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault' -import { SharedVaultUser } from '../../../Domain/SharedVault/User/SharedVaultUser' import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation' import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault' import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken' @@ -74,6 +73,8 @@ export class BaseSharedVaultsController extends BaseHttpController { ) } + response.setHeader('x-invalidate-cache', response.locals.user.uuid) + return this.json({ sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault), sharedVaultUser: this.sharedVaultUserHttpMapper.toProjection(result.getValue().sharedVaultUser), @@ -97,6 +98,8 @@ export class BaseSharedVaultsController extends BaseHttpController { ) } + response.setHeader('x-invalidate-cache', response.locals.user.uuid) + return this.json({ success: true }) } diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts index cb830e80a..89bd49eb5 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts @@ -1,8 +1,7 @@ import { Repository } from 'typeorm' -import { MapperInterface, Uuid } from '@standardnotes/domain-core' +import { MapperInterface, SharedVaultUser, Uuid } from '@standardnotes/domain-core' import { TypeORMSharedVaultUser } from './TypeORMSharedVaultUser' -import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser' import { SharedVaultUserRepositoryInterface } from '../../Domain/SharedVault/User/SharedVaultUserRepositoryInterface' export class TypeORMSharedVaultUserRepository implements SharedVaultUserRepositoryInterface { diff --git a/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts b/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts index d558bb628..d1e905571 100644 --- a/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts +++ b/packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts @@ -27,6 +27,15 @@ export class ItemBackupMapper implements MapperInterface { diff --git a/packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts b/packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts index 5e9eda145..7a594ef3b 100644 --- a/packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts +++ b/packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts @@ -4,9 +4,9 @@ import { UniqueEntityId, Uuid, SharedVaultUserPermission, + SharedVaultUser, } from '@standardnotes/domain-core' -import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser' import { TypeORMSharedVaultUser } from '../../Infra/TypeORM/TypeORMSharedVaultUser' export class SharedVaultUserPersistenceMapper implements MapperInterface {