From c416ed6078bb8b5c5cd3f3d3ffcfbe68937ebe1b Mon Sep 17 00:00:00 2001 From: Mo Date: Fri, 19 May 2023 12:59:28 -0500 Subject: [PATCH] wip: default rotation --- packages/api-gateway/src/Controller/index.ts | 1 + .../src/Controller/v1/ContactsController.ts | 17 +++++ .../src/Controller/v1/UsersController.ts | 9 +-- packages/auth/src/Bootstrap/Container.ts | 2 - packages/auth/src/Bootstrap/Types.ts | 1 - .../auth/src/Controller/UsersController.ts | 53 +-------------- .../src/Domain/Event/DomainEventFactory.ts | 19 ++++++ .../Event/DomainEventFactoryInterface.ts | 2 + .../ChangeCredentials/ChangeCredentials.ts | 15 ++++- .../ChangeCredentials/ChangePkcCredentials.ts | 45 ------------- .../common/src/Domain/Content/ContentType.ts | 2 +- .../Event/UserCredentialsChangedEvent.ts | 7 ++ .../UserCredentialsChangedEventPayload.ts | 4 ++ packages/domain-events/src/Domain/index.ts | 2 + .../event-store/src/Bootstrap/Container.ts | 1 + packages/syncing-server/bin/server.ts | 1 + .../mysql/1684173017359-create_group.ts | 1 + .../mysql/1684503299320-create_contacts.ts | 51 +++++++++++++++ .../Bootstrap/ServerContainerConfigLoader.ts | 47 ++++++++++++++ .../syncing-server/src/Bootstrap/Types.ts | 27 ++++++-- .../src/Controller/ContactsController.ts | 64 +++++++++++++++++++ .../src/Controller/GroupsController.ts | 2 +- .../Domain/Contact/Factory/ContactFactory.ts | 31 +++++++++ .../Factory/ContactFactoryInterface.ts | 7 ++ .../src/Domain/Contact/Factory/ContactHash.ts | 8 +++ .../src/Domain/Contact/Model/Contact.ts | 36 +++++++++++ .../Repository/ContactRepositoryInterface.ts | 15 +++++ .../Repository/TypeORMContactRepository.ts | 52 +++++++++++++++ .../Domain/Contact/Service/ContactService.ts | 52 +++++++++++++++ .../Service/ContactServiceInterface.ts | 9 +++ .../GroupUserKeyRepositoryInterface.ts | 3 +- .../TypeORMGroupUserKeyRepository.ts | 15 ++++- .../Service/GroupUserKeyService.ts | 3 +- .../UserCredentialsChangedEventHandler.ts | 17 +++++ .../Item/SyncResponse/SyncResponse20200115.ts | 2 + .../SyncResponseFactory20200115.ts | 17 +++-- .../src/Domain/UseCase/SyncItems.ts | 23 +++++-- .../src/Domain/UseCase/SyncItemsResponse.ts | 2 + .../src/Projection/ContactProjection.ts | 8 +++ .../src/Projection/ContactProjector.ts | 29 +++++++++ 40 files changed, 571 insertions(+), 131 deletions(-) create mode 100644 packages/api-gateway/src/Controller/v1/ContactsController.ts delete mode 100644 packages/auth/src/Domain/UseCase/ChangeCredentials/ChangePkcCredentials.ts create mode 100644 packages/domain-events/src/Domain/Event/UserCredentialsChangedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/UserCredentialsChangedEventPayload.ts create mode 100644 packages/syncing-server/migrations/mysql/1684503299320-create_contacts.ts create mode 100644 packages/syncing-server/src/Controller/ContactsController.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Factory/ContactFactory.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Factory/ContactFactoryInterface.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Factory/ContactHash.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Model/Contact.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Repository/ContactRepositoryInterface.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Repository/TypeORMContactRepository.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Service/ContactService.ts create mode 100644 packages/syncing-server/src/Domain/Contact/Service/ContactServiceInterface.ts create mode 100644 packages/syncing-server/src/Domain/Handler/UserCredentialsChangedEventHandler.ts create mode 100644 packages/syncing-server/src/Projection/ContactProjection.ts create mode 100644 packages/syncing-server/src/Projection/ContactProjector.ts diff --git a/packages/api-gateway/src/Controller/index.ts b/packages/api-gateway/src/Controller/index.ts index b7e2107e3..0232ed39b 100644 --- a/packages/api-gateway/src/Controller/index.ts +++ b/packages/api-gateway/src/Controller/index.ts @@ -10,6 +10,7 @@ export * from './v1/FilesController' export * from './v1/InvoicesController' export * from './v1/ItemsController' export * from './v1/LinksController' +export * from './v1/ContactsController' export * from './v1/GroupsController' export * from './v1/OfflineController' export * from './v1/PaymentsController' diff --git a/packages/api-gateway/src/Controller/v1/ContactsController.ts b/packages/api-gateway/src/Controller/v1/ContactsController.ts new file mode 100644 index 000000000..fcc7e2c9b --- /dev/null +++ b/packages/api-gateway/src/Controller/v1/ContactsController.ts @@ -0,0 +1,17 @@ +import { Request, Response } from 'express' +import { inject } from 'inversify' +import { BaseHttpController, controller, all } from 'inversify-express-utils' +import { TYPES } from '../../Bootstrap/Types' +import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface' + +@controller('/v1/contacts') +export class ContactsController extends BaseHttpController { + constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) { + super() + } + + @all('*', TYPES.AuthMiddleware) + async subscriptions(request: Request, response: Response): Promise { + await this.httpService.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body) + } +} diff --git a/packages/api-gateway/src/Controller/v1/UsersController.ts b/packages/api-gateway/src/Controller/v1/UsersController.ts index 0fb28d03c..cb2017c1a 100644 --- a/packages/api-gateway/src/Controller/v1/UsersController.ts +++ b/packages/api-gateway/src/Controller/v1/UsersController.ts @@ -64,14 +64,9 @@ export class UsersController extends BaseHttpController { ) } - @httpGet('/:userUuid/attributes/credentials', TYPES.AuthMiddleware) + @httpGet('/:userUuid', TYPES.AuthMiddleware) async getPkcCredentials(request: Request, response: Response): Promise { - await this.httpService.callAuthServer( - request, - response, - `users/${request.params.userUuid}/attributes/credentials`, - request.body, - ) + await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}`, request.body) } @httpGet('/:userId/params', TYPES.AuthMiddleware) diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 1f6dea2a0..ae9a15d85 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -226,7 +226,6 @@ import { TypeORMEphemeralSessionRepository } from '../Infra/TypeORM/TypeORMEphem import { TypeORMOfflineSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMOfflineSubscriptionTokenRepository' import { TypeORMPKCERepository } from '../Infra/TypeORM/TypeORMPKCERepository' import { TypeORMSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMSubscriptionTokenRepository' -import { ChangePkcCredentials } from '../Domain/UseCase/ChangeCredentials/ChangePkcCredentials' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -735,7 +734,6 @@ export class ContainerConfigLoader { container.bind(TYPES.DeletePreviousSessionsForUser).to(DeletePreviousSessionsForUser) container.bind(TYPES.DeleteSessionForUser).to(DeleteSessionForUser) container.bind(TYPES.ChangeCredentials).to(ChangeCredentials) - container.bind(TYPES.ChangePkcCredentials).to(ChangePkcCredentials) container.bind(TYPES.GetSettings).to(GetSettings) container.bind(TYPES.GetSetting).to(GetSetting) container.bind(TYPES.GetUserFeatures).to(GetUserFeatures) diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index d16bda289..40241fd3e 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -116,7 +116,6 @@ const TYPES = { DeletePreviousSessionsForUser: Symbol.for('DeletePreviousSessionsForUser'), DeleteSessionForUser: Symbol.for('DeleteSessionForUser'), ChangeCredentials: Symbol.for('ChangePassword'), - ChangePkcCredentials: Symbol.for('ChangePkcCredentials'), GetSettings: Symbol.for('GetSettings'), GetSetting: Symbol.for('GetSetting'), GetUserFeatures: Symbol.for('GetUserFeatures'), diff --git a/packages/auth/src/Controller/UsersController.ts b/packages/auth/src/Controller/UsersController.ts index b46c9504b..d9875eb6c 100644 --- a/packages/auth/src/Controller/UsersController.ts +++ b/packages/auth/src/Controller/UsersController.ts @@ -20,7 +20,6 @@ import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts' import { IncreaseLoginAttempts } from '../Domain/UseCase/IncreaseLoginAttempts' import { ChangeCredentials } from '../Domain/UseCase/ChangeCredentials/ChangeCredentials' import { GetUser } from '../Domain/UseCase/GetUser' -import { ChangePkcCredentials } from '../Domain/UseCase/ChangeCredentials/ChangePkcCredentials' @controller('/users') export class UsersController extends BaseHttpController { @@ -33,7 +32,6 @@ export class UsersController extends BaseHttpController { @inject(TYPES.ClearLoginAttempts) private clearLoginAttempts: ClearLoginAttempts, @inject(TYPES.IncreaseLoginAttempts) private increaseLoginAttempts: IncreaseLoginAttempts, @inject(TYPES.ChangeCredentials) private changeCredentialsUseCase: ChangeCredentials, - @inject(TYPES.ChangePkcCredentials) private changePkcCredentialsUseCase: ChangePkcCredentials, ) { super() } @@ -155,7 +153,7 @@ export class UsersController extends BaseHttpController { return this.json(result, 400) } - @httpGet('/:userId/attributes/credentials', TYPES.AuthMiddleware) + @httpGet('/:userId', TYPES.AuthMiddleware) async getPkcCredentials(request: Request, response: Response): Promise { if (request.params.userId !== response.locals.user.uuid) { return this.json( @@ -177,59 +175,12 @@ export class UsersController extends BaseHttpController { } return this.json({ + email: result.user.email, public_key: result.user.publicKey, encrypted_private_key: result.user.encryptedPrivateKey, }) } - @httpPut('/:userId/attributes/credentials/pkc', TYPES.AuthMiddleware) - async changePkcCredentials(request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { - return this.json( - { - error: { - tag: ErrorTag.ReadOnlyAccess, - message: 'Session has read-only access.', - }, - }, - 401, - ) - } - - if (!request.body.new_public_key || !request.body.new_encrypted_private_key) { - return this.json( - { - error: { - message: 'Missing new pkc parameters.', - }, - }, - 400, - ) - } - - const changePkcCredentialsResult = await this.changePkcCredentialsUseCase.execute({ - user: response.locals.user, - apiVersion: request.body.api, - updatedWithUserAgent: request.headers['user-agent'], - publicKey: request.body.new_public_key, - encryptedPrivateKey: request.body.new_encrypted_private_key, - }) - - if (!changePkcCredentialsResult.success) { - return this.json( - { - error: { - message: changePkcCredentialsResult.errorMessage, - }, - }, - 401, - ) - } - - response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.send(changePkcCredentialsResult.authResponse) - } - @httpPut('/:userId/attributes/credentials', TYPES.AuthMiddleware) async changeCredentials(request: Request, response: Response): Promise { if (response.locals.readOnlyAccess) { diff --git a/packages/auth/src/Domain/Event/DomainEventFactory.ts b/packages/auth/src/Domain/Event/DomainEventFactory.ts index 1e46bde5d..3fe98adb2 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactory.ts +++ b/packages/auth/src/Domain/Event/DomainEventFactory.ts @@ -20,6 +20,7 @@ import { StatisticPersistenceRequestedEvent, SessionCreatedEvent, SessionRefreshedEvent, + UserCredentialsChangedEvent, } from '@standardnotes/domain-events' import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates' import { TimerInterface } from '@standardnotes/time' @@ -314,6 +315,24 @@ export class DomainEventFactory implements DomainEventFactoryInterface { } } + createUserCredentialsChangedEvent(userUuid: string, newPublicKey: string): UserCredentialsChangedEvent { + return { + type: 'USER_CREDENTIALS_CHANGED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.Auth, + }, + payload: { + userUuid, + newPublicKey, + }, + } + } + createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent { return { type: 'USER_EMAIL_CHANGED', diff --git a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts index 228cb5c3f..8b32a9492 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts @@ -18,6 +18,7 @@ import { StatisticPersistenceRequestedEvent, SessionCreatedEvent, SessionRefreshedEvent, + UserCredentialsChangedEvent, } from '@standardnotes/domain-events' import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType' @@ -47,6 +48,7 @@ export interface DomainEventFactoryInterface { regularSubscriptionUuid: string | undefined }): AccountDeletionRequestedEvent createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent + createCredentialsChangedEvent(userUuid: string, newPublicKey: string): UserCredentialsChangedEvent createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent createUserDisabledSessionUserAgentLoggingEvent(dto: { userUuid: string diff --git a/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangeCredentials.ts b/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangeCredentials.ts index 52371505d..3b0de388c 100644 --- a/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangeCredentials.ts +++ b/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangeCredentials.ts @@ -9,7 +9,11 @@ import { ChangeCredentialsDTO } from './ChangeCredentialsDTO' import { ChangeCredentialsResponse } from './ChangeCredentialsResponse' import { UseCaseInterface } from '../UseCaseInterface' import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' -import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events' +import { + DomainEventPublisherInterface, + UserCredentialsChangedEvent, + UserEmailChangedEvent, +} from '@standardnotes/domain-events' import { TimerInterface } from '@standardnotes/time' import { Username } from '@standardnotes/domain-core' @@ -62,9 +66,12 @@ export class ChangeCredentials implements UseCaseInterface { dto.user.email = newUsername.value } - if (dto.publicKey) { + let userCredentialsChangedEvent: UserCredentialsChangedEvent | undefined = undefined + if (dto.publicKey && dto.user.publicKey !== dto.publicKey) { dto.user.publicKey = dto.publicKey + userCredentialsChangedEvent = this.domainEventFactory.createCredentialsChangedEvent(dto.user.uuid, dto.publicKey) } + if (dto.encryptedPrivateKey) { dto.user.encryptedPrivateKey = dto.encryptedPrivateKey } @@ -87,6 +94,10 @@ export class ChangeCredentials implements UseCaseInterface { await this.domainEventPublisher.publish(userEmailChangedEvent) } + if (userCredentialsChangedEvent != undefined) { + await this.domainEventPublisher.publish(userCredentialsChangedEvent) + } + const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion) return { diff --git a/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangePkcCredentials.ts b/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangePkcCredentials.ts deleted file mode 100644 index 40f07a44f..000000000 --- a/packages/auth/src/Domain/UseCase/ChangeCredentials/ChangePkcCredentials.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { inject, injectable } from 'inversify' -import TYPES from '../../../Bootstrap/Types' -import { AuthResponseFactoryResolverInterface } from '../../Auth/AuthResponseFactoryResolverInterface' -import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' -import { ChangeCredentialsResponse } from './ChangeCredentialsResponse' -import { UseCaseInterface } from '../UseCaseInterface' -import { TimerInterface } from '@standardnotes/time' -import { User } from '../../User/User' - -@injectable() -export class ChangePkcCredentials implements UseCaseInterface { - constructor( - @inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.AuthResponseFactoryResolver) - private authResponseFactoryResolver: AuthResponseFactoryResolverInterface, - @inject(TYPES.Timer) private timer: TimerInterface, - ) {} - - async execute(dto: { - user: User - publicKey: string - encryptedPrivateKey: string - apiVersion: string - updatedWithUserAgent: string - }): Promise { - dto.user.publicKey = dto.publicKey - dto.user.encryptedPrivateKey = dto.encryptedPrivateKey - dto.user.updatedAt = this.timer.getUTCDate() - - const updatedUser = await this.userRepository.save(dto.user) - - const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion) - - return { - success: true, - authResponse: await authResponseFactory.createResponse({ - user: updatedUser, - apiVersion: dto.apiVersion, - userAgent: dto.updatedWithUserAgent, - ephemeralSession: false, - readonlyAccess: false, - }), - } - } -} diff --git a/packages/common/src/Domain/Content/ContentType.ts b/packages/common/src/Domain/Content/ContentType.ts index 135c7320a..c3ba12fe4 100644 --- a/packages/common/src/Domain/Content/ContentType.ts +++ b/packages/common/src/Domain/Content/ContentType.ts @@ -4,7 +4,7 @@ export enum ContentType { Item = 'SF|Item', SharedItemsKey = 'SN|SharedItemsKey', KeypairArchive = 'SN|KeypairArchive', - Contact = 'SN|Contact', + TrustedContact = 'SN|TrustedContact', RootKey = 'SN|RootKey|NoSync', ItemsKey = 'SN|ItemsKey', EncryptedStorage = 'SN|EncryptedStorage', diff --git a/packages/domain-events/src/Domain/Event/UserCredentialsChangedEvent.ts b/packages/domain-events/src/Domain/Event/UserCredentialsChangedEvent.ts new file mode 100644 index 000000000..18b4e17bc --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserCredentialsChangedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { UserCredentialsChangedEventPayload } from './UserCredentialsChangedEventPayload' + +export interface UserCredentialsChangedEvent extends DomainEventInterface { + type: 'USER_CREDENTIALS_CHANGED' + payload: UserCredentialsChangedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/UserCredentialsChangedEventPayload.ts b/packages/domain-events/src/Domain/Event/UserCredentialsChangedEventPayload.ts new file mode 100644 index 000000000..e60f02324 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/UserCredentialsChangedEventPayload.ts @@ -0,0 +1,4 @@ +export interface UserCredentialsChangedEventPayload { + userUuid: string + newPublicKey: string +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index 6dbe1734b..814d412ca 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -84,6 +84,8 @@ export * from './Event/SubscriptionSyncRequestedEvent' export * from './Event/SubscriptionSyncRequestedEventPayload' export * from './Event/UserDisabledSessionUserAgentLoggingEvent' export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload' +export * from './Event/UserCredentialsChangedEvent' +export * from './Event/UserCredentialsChangedEventPayload' export * from './Event/UserEmailChangedEvent' export * from './Event/UserEmailChangedEventPayload' export * from './Event/UserRegisteredEvent' diff --git a/packages/event-store/src/Bootstrap/Container.ts b/packages/event-store/src/Bootstrap/Container.ts index 65ff97aad..0d62f5bdd 100644 --- a/packages/event-store/src/Bootstrap/Container.ts +++ b/packages/event-store/src/Bootstrap/Container.ts @@ -74,6 +74,7 @@ export class ContainerConfigLoader { ['SUBSCRIPTION_EXPIRED', container.get(TYPES.EventHandler)], ['EXTENSION_KEY_GRANTED', container.get(TYPES.EventHandler)], ['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)], + ['USER_CREDENTIALS_CHANGED', container.get(TYPES.EventHandler)], ['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)], ['FILE_UPLOADED', container.get(TYPES.EventHandler)], ['FILE_REMOVED', container.get(TYPES.EventHandler)], diff --git a/packages/syncing-server/bin/server.ts b/packages/syncing-server/bin/server.ts index f9f9d8ac0..4806d4ae1 100644 --- a/packages/syncing-server/bin/server.ts +++ b/packages/syncing-server/bin/server.ts @@ -7,6 +7,7 @@ import '../src/Controller/ItemsController' import '../src/Controller/LinksController' import '../src/Controller/GroupsController' import '../src/Controller/GroupUserKeysController' +import '../src/Controller/ContactsController' import helmet from 'helmet' import * as cors from 'cors' diff --git a/packages/syncing-server/migrations/mysql/1684173017359-create_group.ts b/packages/syncing-server/migrations/mysql/1684173017359-create_group.ts index 9b9f8bbe3..31cb749f3 100644 --- a/packages/syncing-server/migrations/mysql/1684173017359-create_group.ts +++ b/packages/syncing-server/migrations/mysql/1684173017359-create_group.ts @@ -2,6 +2,7 @@ import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm' export class CreateGroup1684173017359 implements MigrationInterface { name = 'createGroup1684173017359' + public async up(queryRunner: QueryRunner): Promise { const table = new Table({ name: 'groups', diff --git a/packages/syncing-server/migrations/mysql/1684503299320-create_contacts.ts b/packages/syncing-server/migrations/mysql/1684503299320-create_contacts.ts new file mode 100644 index 000000000..832bb4b9c --- /dev/null +++ b/packages/syncing-server/migrations/mysql/1684503299320-create_contacts.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm' + +export class CreateContacts1684503299320 implements MigrationInterface { + name = 'createContacts1684503299320' + + public async up(queryRunner: QueryRunner): Promise { + const table = new Table({ + name: 'contacts', + columns: [ + new TableColumn({ + name: 'uuid', + type: 'varchar', + length: '36', + isPrimary: true, + }), + new TableColumn({ + name: 'user_uuid', + type: 'varchar', + length: '36', + isNullable: false, + }), + new TableColumn({ + name: 'contact_uuid', + type: 'varchar', + length: '36', + isNullable: false, + }), + new TableColumn({ + name: 'contact_public_key', + type: 'varchar', + length: '255', + isNullable: false, + }), + new TableColumn({ + name: 'created_at_timestamp', + type: 'bigint', + }), + new TableColumn({ + name: 'updated_at_timestamp', + type: 'bigint', + }), + ], + }) + + await queryRunner.createTable(table) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('contacts') + } +} diff --git a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts index a9ed766ee..0b437f617 100644 --- a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts +++ b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts @@ -1,3 +1,8 @@ +import { + DomainEventHandlerInterface, + DomainEventMessageHandlerInterface, + DomainEventSubscriberFactoryInterface, +} from '@standardnotes/domain-events' import { GroupServiceInterface } from './../Domain/Group/Service/GroupServiceInterface' import { GroupUserKeyFactory } from './../Domain/GroupUserKey/GroupUserKeyFactory' import { GroupFactoryInterface } from '../Domain/Group/Factory/GroupFactoryInterface' @@ -45,6 +50,12 @@ import { GroupFactory } from '../Domain/Group/Factory/GroupFactory' import { GroupService } from '../Domain/Group/Service/GroupService' import { GroupUserKeyService } from '../Domain/GroupUserKey/Service/GroupUserKeyService' import { GroupUserKeyServiceInterface } from '../Domain/GroupUserKey/Service/GroupUserKeyServiceInterface' +import { + SQSDomainEventSubscriberFactory, + SQSEventMessageHandler, + SQSNewRelicEventMessageHandler, +} from '@standardnotes/domain-events-infra' +import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler' export class ServerContainerConfigLoader extends CommonContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -206,6 +217,42 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { return new GroupUserKeyFactory(context.container.get(TYPES.Timer)) }) + container + .bind(TYPES.UserCredentialsChangedEventHandler) + .toDynamicValue((context: interfaces.Context) => { + return new UserCredentialsChangedEventHandler( + context.container.get(TYPES.ContactRepository), + context.container.get(TYPES.Timer), + ) + }) + + const eventHandlers: Map = new Map([ + ['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)], + ]) + + container + .bind(TYPES.DomainEventMessageHandler) + .toDynamicValue((context: interfaces.Context) => { + const env: Env = context.container.get(TYPES.Env) + + const handler = + env.get('NEW_RELIC_ENABLED', true) === 'true' + ? new SQSNewRelicEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger)) + : new SQSEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger)) + + return handler + }) + + container + .bind(TYPES.DomainEventSubscriberFactory) + .toDynamicValue((context: interfaces.Context) => { + return new SQSDomainEventSubscriberFactory( + context.container.get(TYPES.SQS), + context.container.get(TYPES.SQS_QUEUE_URL), + context.container.get(TYPES.DomainEventMessageHandler), + ) + }) + container .bind(TYPES.OwnershipFilter) .toDynamicValue( diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 45a56b1b0..7fbfd0d02 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -6,24 +6,39 @@ const TYPES = { SQS: Symbol.for('SQS'), S3: Symbol.for('S3'), Env: Symbol.for('Env'), + // Repositories ItemRepository: Symbol.for('ItemRepository'), ItemLinkRepository: Symbol.for('ItemLinkRepository'), GroupRepository: Symbol.for('GroupRepository'), GroupUserKeyRepository: Symbol.for('GroupUserKeyRepository'), + ContactRepository: Symbol.for('ContactRepository'), + // ORM ORMItemRepository: Symbol.for('ORMItemRepository'), ORMItemLinkRepository: Symbol.for('ORMItemLinkRepository'), ORMGroupRepository: Symbol.for('ORMGroupRepository'), ORMGroupUserKeyRepository: Symbol.for('ORMGroupUserKeyRepository'), + ORMContactRepository: Symbol.for('ORMContactRepository'), + // Middleware AuthMiddleware: Symbol.for('AuthMiddleware'), + // Projectors ItemProjector: Symbol.for('ItemProjector'), SavedItemProjector: Symbol.for('SavedItemProjector'), ItemConflictProjector: Symbol.for('ItemConflictProjector'), GroupProjector: Symbol.for('GroupProjector'), GroupUserKeyProjector: Symbol.for('GroupUserKeyProjector'), + ContactProjector: Symbol.for('ContactProjector'), + + // factories + ItemFactory: Symbol.for('ItemFactory'), + ItemLinkFactory: Symbol.for('ItemLinkFactory'), + GroupFactory: Symbol.for('GroupFactory'), + GroupUserKeyFactory: Symbol.for('GroupUserKeyFactory'), + ContactFactory: Symbol.for('ContactFactory'), + // env vars REDIS_URL: Symbol.for('REDIS_URL'), SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'), @@ -53,11 +68,17 @@ const TYPES = { GetUserItemLinks: Symbol.for('GetUserItemLinks'), CreateSharedFileValetToken: Symbol.for('CreateSharedFileValetToken'), // Handlers + UserCredentialsChangedEventHandler: Symbol.for('UserCredentialsChangedEventHandler'), AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'), DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'), EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'), ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'), + // Services + GroupService: Symbol.for('GroupService'), + GroupUserKeyService: Symbol.for('GroupUserKeyService'), + ContactService: Symbol.for('ContactService'), + ContentDecoder: Symbol.for('ContentDecoder'), DomainEventPublisher: Symbol.for('DomainEventPublisher'), DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'), @@ -79,14 +100,8 @@ const TYPES = { UuidFilter: Symbol.for('UuidFilter'), ContentTypeFilter: Symbol.for('ContentTypeFilter'), ContentFilter: Symbol.for('ContentFilter'), - ItemFactory: Symbol.for('ItemFactory'), - ItemLinkFactory: Symbol.for('ItemLinkFactory'), - GroupFactory: Symbol.for('GroupFactory'), - GroupUserKeyFactory: Symbol.for('GroupUserKeyFactory'), ItemTransferCalculator: Symbol.for('ItemTransferCalculator'), ValetTokenEncoder: Symbol.for('ValetTokenEncoder'), - GroupService: Symbol.for('GroupService'), - GroupUserKeyService: Symbol.for('GroupUserKeyService'), } export default TYPES diff --git a/packages/syncing-server/src/Controller/ContactsController.ts b/packages/syncing-server/src/Controller/ContactsController.ts new file mode 100644 index 000000000..151a0c5ba --- /dev/null +++ b/packages/syncing-server/src/Controller/ContactsController.ts @@ -0,0 +1,64 @@ +import { ContactServiceInterface } from './../Domain/Contact/Service/ContactServiceInterface' +import { Request, Response } from 'express' +import { BaseHttpController, controller, httpPost, results, httpDelete } from 'inversify-express-utils' +import TYPES from '../Bootstrap/Types' +import { inject } from 'inversify' +import { ProjectorInterface } from '../Projection/ProjectorInterface' +import { Contact } from '../Domain/Contact/Model/Contact' +import { ContactProjection } from '../Projection/ContactProjection' + +@controller('/contacts') +export class ContactsController extends BaseHttpController { + constructor( + @inject(TYPES.ContactService) private contactService: ContactServiceInterface, + @inject(TYPES.ContactProjector) private contactProjector: ProjectorInterface, + ) { + super() + } + + @httpPost('/', TYPES.AuthMiddleware) + public async createContact( + request: Request, + response: Response, + ): Promise { + const result = await this.contactService.createContact({ + userUuid: response.locals.user.uuid, + contactUuid: request.body.contact_uuid, + contactPublicKey: request.body.contact_public_key, + }) + + if (!result) { + return this.errorResponse(500, 'Could not create contact') + } + + return this.json({ + contact: this.contactProjector.projectFull(result), + }) + } + + @httpDelete('/:contactUuid', TYPES.AuthMiddleware) + public async deleteContact( + request: Request, + response: Response, + ): Promise { + const result = await this.contactService.deleteContact({ + originatorUuid: response.locals.user.uuid, + contactUuid: request.params.contactUuid, + }) + + if (!result) { + return this.errorResponse(500, 'Could not delete contact') + } + + return this.json({ success: true }) + } + + private errorResponse(status: number, message?: string, tag?: string) { + return this.json( + { + error: { message, tag }, + }, + status, + ) + } +} diff --git a/packages/syncing-server/src/Controller/GroupsController.ts b/packages/syncing-server/src/Controller/GroupsController.ts index fa6422d42..687c2f53d 100644 --- a/packages/syncing-server/src/Controller/GroupsController.ts +++ b/packages/syncing-server/src/Controller/GroupsController.ts @@ -86,7 +86,7 @@ export class GroupsController extends BaseHttpController { return this.json({ success: true }) } - @httpPatch('/all-user-keys', TYPES.AuthMiddleware) + @httpPatch('/user-keys', TYPES.AuthMiddleware) public async updateAllUserKeysOfUser( request: Request, response: Response, diff --git a/packages/syncing-server/src/Domain/Contact/Factory/ContactFactory.ts b/packages/syncing-server/src/Domain/Contact/Factory/ContactFactory.ts new file mode 100644 index 000000000..a3176360a --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Factory/ContactFactory.ts @@ -0,0 +1,31 @@ +import { TimerInterface } from '@standardnotes/time' +import { ContactFactoryInterface } from './ContactFactoryInterface' +import { ContactHash } from './ContactHash' +import { Contact } from '../Model/Contact' + +export class ContactFactory implements ContactFactoryInterface { + constructor(private timer: TimerInterface) {} + + create(dto: { userUuid: string; contactHash: ContactHash }): Contact { + const newContact = new Contact() + newContact.uuid = dto.contactHash.uuid + newContact.userUuid = dto.userUuid + newContact.contactUuid = dto.contactHash.contact_uuid + newContact.contactPublicKey = dto.contactHash.contact_public_key + + const now = this.timer.getTimestampInSeconds() + newContact.updatedAtTimestamp = now + newContact.createdAtTimestamp = now + + if (dto.contactHash.created_at_timestamp) { + newContact.createdAtTimestamp = dto.contactHash.created_at_timestamp + } + + return newContact + } + + createStub(dto: { userUuid: string; contactHash: ContactHash }): Contact { + const item = this.create(dto) + return item + } +} diff --git a/packages/syncing-server/src/Domain/Contact/Factory/ContactFactoryInterface.ts b/packages/syncing-server/src/Domain/Contact/Factory/ContactFactoryInterface.ts new file mode 100644 index 000000000..d1c844b70 --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Factory/ContactFactoryInterface.ts @@ -0,0 +1,7 @@ +import { Contact } from '../Model/Contact' +import { ContactHash } from './ContactHash' + +export interface ContactFactoryInterface { + create(dto: { userUuid: string; contactHash: ContactHash }): Contact + createStub(dto: { userUuid: string; contactHash: ContactHash }): Contact +} diff --git a/packages/syncing-server/src/Domain/Contact/Factory/ContactHash.ts b/packages/syncing-server/src/Domain/Contact/Factory/ContactHash.ts new file mode 100644 index 000000000..6289ffa6d --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Factory/ContactHash.ts @@ -0,0 +1,8 @@ +export type ContactHash = { + uuid: string + user_uuid: string + contact_uuid: string + contact_public_key: string + created_at_timestamp?: number + updated_at_timestamp?: number +} diff --git a/packages/syncing-server/src/Domain/Contact/Model/Contact.ts b/packages/syncing-server/src/Domain/Contact/Model/Contact.ts new file mode 100644 index 000000000..83240fcbb --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Model/Contact.ts @@ -0,0 +1,36 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'contacts' }) +export class Contact { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + name: 'user_uuid', + length: 36, + }) + declare userUuid: string + + @Column({ + name: 'contact_uuid', + length: 36, + }) + declare contactUuid: string + + @Column({ + name: 'contact_public_key', + }) + declare contactPublicKey: 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/syncing-server/src/Domain/Contact/Repository/ContactRepositoryInterface.ts b/packages/syncing-server/src/Domain/Contact/Repository/ContactRepositoryInterface.ts new file mode 100644 index 000000000..c87ac7255 --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Repository/ContactRepositoryInterface.ts @@ -0,0 +1,15 @@ +import { Contact } from '../Model/Contact' + +export type ContactQuery = { + userUuid?: string + contactUuid?: string + lastSyncTime?: number +} + +export interface ContactsRepositoryInterface { + findByUuid(uuid: string): Promise + create(contact: Contact): Promise + save(contact: Contact): Promise + remove(contact: Contact): Promise + findAll(query: ContactQuery): Promise +} diff --git a/packages/syncing-server/src/Domain/Contact/Repository/TypeORMContactRepository.ts b/packages/syncing-server/src/Domain/Contact/Repository/TypeORMContactRepository.ts new file mode 100644 index 000000000..670583bc1 --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Repository/TypeORMContactRepository.ts @@ -0,0 +1,52 @@ +import { Repository, SelectQueryBuilder } from 'typeorm' +import { ContactQuery, ContactsRepositoryInterface } from './ContactRepositoryInterface' +import { Contact } from '../Model/Contact' + +export class TypeORMContactRepository implements ContactsRepositoryInterface { + constructor(private ormRepository: Repository) {} + + async create(contact: Contact): Promise { + return this.ormRepository.save(contact) + } + + async save(contact: Contact): Promise { + return this.ormRepository.save(contact) + } + + findByUuid(uuid: string): Promise { + return this.ormRepository + .createQueryBuilder('contact') + .where('contact.uuid = :uuid', { + uuid, + }) + .getOne() + } + + async remove(contact: Contact): Promise { + return this.ormRepository.remove(contact) + } + + async findAll(query: ContactQuery): Promise { + return this.createFindAllQueryBuilder(query).getMany() + } + + private createFindAllQueryBuilder(query: ContactQuery): SelectQueryBuilder { + const queryBuilder = this.ormRepository.createQueryBuilder('contact') + + if (query.userUuid) { + queryBuilder.where('contact.user_uuid = :userUuid', { userUuid: query.userUuid }) + } + + if (query.contactUuid) { + queryBuilder.andWhere('contact.contact_uuid = :contactUuid', { contactUuid: query.contactUuid }) + } + + if (query.lastSyncTime) { + queryBuilder.andWhere('group_user_key.updated_at_timestamp > :lastSyncTime', { + lastSyncTime: query.lastSyncTime, + }) + } + + return queryBuilder + } +} diff --git a/packages/syncing-server/src/Domain/Contact/Service/ContactService.ts b/packages/syncing-server/src/Domain/Contact/Service/ContactService.ts new file mode 100644 index 000000000..ce9f31d63 --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Service/ContactService.ts @@ -0,0 +1,52 @@ +import { v4 as uuidv4 } from 'uuid' +import { TimerInterface } from '@standardnotes/time' + +import { Contact } from '../Model/Contact' +import { ContactsRepositoryInterface } from '../Repository/ContactRepositoryInterface' +import { ContactServiceInterface } from './ContactServiceInterface' +import { ContactFactoryInterface } from '../Factory/ContactFactoryInterface' + +export class ContactService implements ContactServiceInterface { + constructor( + private contactRepository: ContactsRepositoryInterface, + private contactFactory: ContactFactoryInterface, + private timer: TimerInterface, + ) {} + + async createContact(dto: { + userUuid: string + contactUuid: string + contactPublicKey: string + }): Promise { + const contact = this.contactFactory.create({ + userUuid: dto.userUuid, + contactHash: { + uuid: uuidv4(), + user_uuid: dto.userUuid, + contact_uuid: dto.contactUuid, + contact_public_key: dto.contactPublicKey, + created_at_timestamp: this.timer.getTimestampInSeconds(), + updated_at_timestamp: this.timer.getTimestampInSeconds(), + }, + }) + + const savedContact = await this.contactRepository.create(contact) + + return savedContact + } + + async deleteContact(dto: { contactUuid: string; originatorUuid: string }): Promise { + const contact = await this.contactRepository.findByUuid(dto.contactUuid) + if (!contact || contact.userUuid !== dto.originatorUuid) { + return false + } + + await this.contactRepository.remove(contact) + + return true + } + + async getUserContacts(dto: { userUuid: string; lastSyncTime?: number }): Promise { + return this.contactRepository.findAll({ userUuid: dto.userUuid, lastSyncTime: dto.lastSyncTime }) + } +} diff --git a/packages/syncing-server/src/Domain/Contact/Service/ContactServiceInterface.ts b/packages/syncing-server/src/Domain/Contact/Service/ContactServiceInterface.ts new file mode 100644 index 000000000..ee50d2406 --- /dev/null +++ b/packages/syncing-server/src/Domain/Contact/Service/ContactServiceInterface.ts @@ -0,0 +1,9 @@ +import { Contact } from '../Model/Contact' + +export interface ContactServiceInterface { + createContact(dto: { userUuid: string; contactUuid: string; contactPublicKey: string }): Promise + + deleteContact(dto: { contactUuid: string; originatorUuid: string }): Promise + + getUserContacts(dto: { userUuid: string; lastSyncTime?: number }): Promise +} diff --git a/packages/syncing-server/src/Domain/GroupUserKey/Repository/GroupUserKeyRepositoryInterface.ts b/packages/syncing-server/src/Domain/GroupUserKey/Repository/GroupUserKeyRepositoryInterface.ts index 1de9f93e5..aed3e6278 100644 --- a/packages/syncing-server/src/Domain/GroupUserKey/Repository/GroupUserKeyRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/GroupUserKey/Repository/GroupUserKeyRepositoryInterface.ts @@ -8,6 +8,7 @@ export type GroupUserKeyFindAllForUserQuery = { userUuid: string lastSyncTime?: number senderUuid?: string + includeSentAndReceived?: boolean } export type GroupUserKeyFindAllForGroup = { @@ -16,7 +17,7 @@ export type GroupUserKeyFindAllForGroup = { export interface GroupUserKeyRepositoryInterface { findByUuid(groupUserUuid: string): Promise - findByUserAndUuid(userUuid: string, userKeyUuid: string): Promise + findAsSenderOrRecipientByUuid(userUuid: string, userKeyUuid: string): Promise create(group: GroupUserKey): Promise save(groupUser: GroupUserKey): Promise remove(group: GroupUserKey): Promise diff --git a/packages/syncing-server/src/Domain/GroupUserKey/Repository/TypeORMGroupUserKeyRepository.ts b/packages/syncing-server/src/Domain/GroupUserKey/Repository/TypeORMGroupUserKeyRepository.ts index 0feb966d3..38197d75f 100644 --- a/packages/syncing-server/src/Domain/GroupUserKey/Repository/TypeORMGroupUserKeyRepository.ts +++ b/packages/syncing-server/src/Domain/GroupUserKey/Repository/TypeORMGroupUserKeyRepository.ts @@ -1,5 +1,5 @@ import { GroupUserKey } from '../Model/GroupUserKey' -import { Repository, SelectQueryBuilder } from 'typeorm' +import { Brackets, Repository, SelectQueryBuilder } from 'typeorm' import { GroupUserKeyFindAllForGroup, GroupUserKeyFindAllForUserQuery, @@ -26,10 +26,15 @@ export class TypeORMGroupUserKeyRepository implements GroupUserKeyRepositoryInte .getOne() } - findByUserAndUuid(userUuid: string, userKeyUuid: string): Promise { + findAsSenderOrRecipientByUuid(userUuid: string, userKeyUuid: string): Promise { return this.ormRepository .createQueryBuilder('group_user_key') - .where('group_user_key.user_uuid = :userUuid', { userUuid }) + .where( + new Brackets((qb) => { + qb.where('group_user_key.user_uuid = :userUuid', { userUuid }) + qb.orWhere('group_user_key.sender_uuid = :userUuid', { userUuid }) + }), + ) .andWhere('group_user_key.uuid = :userKeyUuid', { userKeyUuid }) .getOne() } @@ -62,6 +67,10 @@ export class TypeORMGroupUserKeyRepository implements GroupUserKeyRepositoryInte queryBuilder.where('group_user_key.user_uuid = :userUuid', { userUuid: query.userUuid }) + if (query.includeSentAndReceived) { + queryBuilder.orWhere('group_user_key.sender_uuid = :userUuid', { userUuid: query.userUuid }) + } + if (query.lastSyncTime) { queryBuilder.andWhere('group_user_key.updated_at_timestamp > :lastSyncTime', { lastSyncTime: query.lastSyncTime, diff --git a/packages/syncing-server/src/Domain/GroupUserKey/Service/GroupUserKeyService.ts b/packages/syncing-server/src/Domain/GroupUserKey/Service/GroupUserKeyService.ts index 74fdb749d..896e4a43e 100644 --- a/packages/syncing-server/src/Domain/GroupUserKey/Service/GroupUserKeyService.ts +++ b/packages/syncing-server/src/Domain/GroupUserKey/Service/GroupUserKeyService.ts @@ -87,7 +87,7 @@ export class GroupUserKeyService implements GroupUserKeyServiceInterface { }[] }): Promise { for (const updatedKey of dto.updatedKeys) { - const userKey = await this.groupUserRepository.findByUserAndUuid(dto.userUuid, updatedKey.uuid) + const userKey = await this.groupUserRepository.findAsSenderOrRecipientByUuid(dto.userUuid, updatedKey.uuid) if (!userKey) { continue } @@ -170,6 +170,7 @@ export class GroupUserKeyService implements GroupUserKeyServiceInterface { async getAllUserKeysForUser(dto: { userUuid: string }): Promise { return this.groupUserRepository.findAllForUser({ userUuid: dto.userUuid, + includeSentAndReceived: true, }) } diff --git a/packages/syncing-server/src/Domain/Handler/UserCredentialsChangedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/UserCredentialsChangedEventHandler.ts new file mode 100644 index 000000000..4c90f583d --- /dev/null +++ b/packages/syncing-server/src/Domain/Handler/UserCredentialsChangedEventHandler.ts @@ -0,0 +1,17 @@ +import { DomainEventHandlerInterface, UserCredentialsChangedEvent } from '@standardnotes/domain-events' +import { TimerInterface } from '@standardnotes/time' +import { ContactsRepositoryInterface } from '../Contact/Repository/ContactRepositoryInterface' + +export class UserCredentialsChangedEventHandler implements DomainEventHandlerInterface { + constructor(private contactRepository: ContactsRepositoryInterface, private timer: TimerInterface) {} + + async handle(event: UserCredentialsChangedEvent): Promise { + const contacts = await this.contactRepository.findAll({ contactUuid: event.payload.userUuid }) + + for (const contact of contacts) { + contact.contactPublicKey = event.payload.newPublicKey + contact.updatedAtTimestamp = this.timer.getTimestampInMicroseconds() + await this.contactRepository.save(contact) + } + } +} diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts index 23e94fb27..0be4f4e5d 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponse20200115.ts @@ -1,3 +1,4 @@ +import { ContactProjection } from '../../../Projection/ContactProjection' import { GroupUserKeyProjection } from '../../../Projection/GroupUserKeyProjection' import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection' import { ItemProjection } from '../../../Projection/ItemProjection' @@ -10,4 +11,5 @@ export type SyncResponse20200115 = { sync_token: string cursor_token?: string group_keys: Array + contacts: Array } diff --git a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts index 5e443a2e7..889acbfa7 100644 --- a/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts +++ b/packages/syncing-server/src/Domain/Item/SyncResponse/SyncResponseFactory20200115.ts @@ -1,3 +1,4 @@ +import { Contact } from './../../Contact/Model/Contact' import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { SyncItemsResponse } from '../../UseCase/SyncItemsResponse' import { Item } from '../Item' @@ -9,6 +10,7 @@ import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface' import { SavedItemProjection } from '../../../Projection/SavedItemProjection' import { GroupUserKey } from '../../GroupUserKey/Model/GroupUserKey' import { GroupUserKeyProjection } from '../../../Projection/GroupUserKeyProjection' +import { ContactProjection } from '../../../Projection/ContactProjection' export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface { constructor( @@ -16,27 +18,33 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface private itemConflictProjector: ProjectorInterface, private savedItemProjector: ProjectorInterface, private groupUserKeyProjector: ProjectorInterface, + private contactProjector: ProjectorInterface, ) {} async createResponse(syncItemsResponse: SyncItemsResponse): Promise { const retrievedItems = [] for (const item of syncItemsResponse.retrievedItems) { - retrievedItems.push(await this.itemProjector.projectFull(item)) + retrievedItems.push(this.itemProjector.projectFull(item)) } const savedItems = [] for (const item of syncItemsResponse.savedItems) { - savedItems.push(await this.savedItemProjector.projectFull(item)) + savedItems.push(this.savedItemProjector.projectFull(item)) } const conflicts = [] for (const itemConflict of syncItemsResponse.conflicts) { - conflicts.push(await this.itemConflictProjector.projectFull(itemConflict)) + conflicts.push(this.itemConflictProjector.projectFull(itemConflict)) } const groupKeys = [] for (const groupKey of syncItemsResponse.groupKeys) { - groupKeys.push(await this.groupUserKeyProjector.projectFull(groupKey)) + groupKeys.push(this.groupUserKeyProjector.projectFull(groupKey)) + } + + const contacts = [] + for (const contact of syncItemsResponse.contacts) { + contacts.push(this.contactProjector.projectFull(contact)) } return { @@ -46,6 +54,7 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface sync_token: syncItemsResponse.syncToken, cursor_token: syncItemsResponse.cursorToken, group_keys: groupKeys, + contacts, } } } diff --git a/packages/syncing-server/src/Domain/UseCase/SyncItems.ts b/packages/syncing-server/src/Domain/UseCase/SyncItems.ts index f78cb689b..abaf1b0af 100644 --- a/packages/syncing-server/src/Domain/UseCase/SyncItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/SyncItems.ts @@ -1,3 +1,4 @@ +import { ContactServiceInterface } from './../Contact/Service/ContactServiceInterface' import { GroupUserKey } from '../GroupUserKey/Model/GroupUserKey' import { GroupUserKeyServiceInterface } from '../GroupUserKey/Service/GroupUserKeyServiceInterface' import { Item } from '../Item/Item' @@ -8,7 +9,11 @@ import { SyncItemsResponse } from './SyncItemsResponse' import { UseCaseInterface } from './UseCaseInterface' export class SyncItems implements UseCaseInterface { - constructor(private itemService: ItemServiceInterface, private groupUserService: GroupUserKeyServiceInterface) {} + constructor( + private itemService: ItemServiceInterface, + private groupUserService: GroupUserKeyServiceInterface, + private contactService: ContactServiceInterface, + ) {} async execute(dto: SyncItemsDTO): Promise { const getItemsResult = await this.itemService.getItems({ @@ -34,20 +39,25 @@ export class SyncItems implements UseCaseInterface { retrievedItems = await this.itemService.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems) } + const lastSyncTime = this.itemService.getLastSyncTime({ + syncToken: dto.syncToken, + cursorToken: dto.cursorToken, + }) + let newUserKeys: GroupUserKey[] = [] const isNotPerformingGroupSpecificSync = dto.groupUuids == undefined || dto.groupUuids.length === 0 if (isNotPerformingGroupSpecificSync) { - const lastSyncTime = this.itemService.getLastSyncTime({ - syncToken: dto.syncToken, - cursorToken: dto.cursorToken, - }) - newUserKeys = await this.groupUserService.getGroupUserKeysForUser({ userUuid: dto.userUuid, lastSyncTime, }) } + const contacts = await this.contactService.getUserContacts({ + userUuid: dto.userUuid, + lastSyncTime: lastSyncTime, + }) + const syncResponse: SyncItemsResponse = { retrievedItems, syncToken: saveItemsResult.syncToken, @@ -55,6 +65,7 @@ export class SyncItems implements UseCaseInterface { conflicts: saveItemsResult.conflicts, cursorToken: getItemsResult.cursorToken, groupKeys: newUserKeys, + contacts, } return syncResponse diff --git a/packages/syncing-server/src/Domain/UseCase/SyncItemsResponse.ts b/packages/syncing-server/src/Domain/UseCase/SyncItemsResponse.ts index 86bf620ca..2b109cb9c 100644 --- a/packages/syncing-server/src/Domain/UseCase/SyncItemsResponse.ts +++ b/packages/syncing-server/src/Domain/UseCase/SyncItemsResponse.ts @@ -1,3 +1,4 @@ +import { Contact } from '../Contact/Model/Contact' import { GroupUserKey } from '../GroupUserKey/Model/GroupUserKey' import { Item } from '../Item/Item' import { ItemConflict } from '../Item/ItemConflict' @@ -7,6 +8,7 @@ export type SyncItemsResponse = { savedItems: Array conflicts: Array groupKeys: Array + contacts: Array syncToken: string cursorToken?: string } diff --git a/packages/syncing-server/src/Projection/ContactProjection.ts b/packages/syncing-server/src/Projection/ContactProjection.ts new file mode 100644 index 000000000..f27926a5b --- /dev/null +++ b/packages/syncing-server/src/Projection/ContactProjection.ts @@ -0,0 +1,8 @@ +export type ContactProjection = { + uuid: string + user_uuid: string + contact_uuid: string + contact_public_key: string + created_at_timestamp: number + updated_at_timestamp: number +} diff --git a/packages/syncing-server/src/Projection/ContactProjector.ts b/packages/syncing-server/src/Projection/ContactProjector.ts new file mode 100644 index 000000000..9b9858802 --- /dev/null +++ b/packages/syncing-server/src/Projection/ContactProjector.ts @@ -0,0 +1,29 @@ +import { Contact } from '../Domain/Contact/Model/Contact' +import { ProjectorInterface } from './ProjectorInterface' +import { ContactProjection } from './ContactProjection' + +export class ContactProjector implements ProjectorInterface { + projectSimple(_userKey: Contact): ContactProjection { + throw Error('not implemented') + } + + projectCustom(_projectionType: string, userKey: Contact): ContactProjection { + const fullProjection = this.projectFull(userKey) + + return { + ...fullProjection, + user_uuid: userKey.userUuid, + } + } + + projectFull(userKey: Contact): ContactProjection { + return { + uuid: userKey.uuid, + user_uuid: userKey.userUuid, + contact_uuid: userKey.contactUuid, + contact_public_key: userKey.contactPublicKey, + created_at_timestamp: userKey.createdAtTimestamp, + updated_at_timestamp: userKey.updatedAtTimestamp, + } + } +}