mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
wip: default rotation
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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<void> {
|
||||
await this.httpService.callSyncingServer(request, response, request.path.replace('/v1/', ''), request.body)
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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)
|
||||
|
||||
@@ -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<DeletePreviousSessionsForUser>(TYPES.DeletePreviousSessionsForUser).to(DeletePreviousSessionsForUser)
|
||||
container.bind<DeleteSessionForUser>(TYPES.DeleteSessionForUser).to(DeleteSessionForUser)
|
||||
container.bind<ChangeCredentials>(TYPES.ChangeCredentials).to(ChangeCredentials)
|
||||
container.bind<ChangePkcCredentials>(TYPES.ChangePkcCredentials).to(ChangePkcCredentials)
|
||||
container.bind<GetSettings>(TYPES.GetSettings).to(GetSettings)
|
||||
container.bind<GetSetting>(TYPES.GetSetting).to(GetSetting)
|
||||
container.bind<GetUserFeatures>(TYPES.GetUserFeatures).to(GetUserFeatures)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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<results.JsonResult | void> {
|
||||
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<results.JsonResult | void> {
|
||||
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: <string>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<results.JsonResult | void> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ChangeCredentialsResponse> {
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { UserCredentialsChangedEventPayload } from './UserCredentialsChangedEventPayload'
|
||||
|
||||
export interface UserCredentialsChangedEvent extends DomainEventInterface {
|
||||
type: 'USER_CREDENTIALS_CHANGED'
|
||||
payload: UserCredentialsChangedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UserCredentialsChangedEventPayload {
|
||||
userUuid: string
|
||||
newPublicKey: string
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'
|
||||
|
||||
export class CreateGroup1684173017359 implements MigrationInterface {
|
||||
name = 'createGroup1684173017359'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const table = new Table({
|
||||
name: 'groups',
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'
|
||||
|
||||
export class CreateContacts1684503299320 implements MigrationInterface {
|
||||
name = 'createContacts1684503299320'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.dropTable('contacts')
|
||||
}
|
||||
}
|
||||
@@ -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<UserCredentialsChangedEventHandler>(TYPES.UserCredentialsChangedEventHandler)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new UserCredentialsChangedEventHandler(
|
||||
context.container.get(TYPES.ContactRepository),
|
||||
context.container.get(TYPES.Timer),
|
||||
)
|
||||
})
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)],
|
||||
])
|
||||
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(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<DomainEventSubscriberFactoryInterface>(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<OwnershipFilter>(TYPES.OwnershipFilter)
|
||||
.toDynamicValue(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Contact, ContactProjection>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
public async createContact(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.NotFoundResult | results.JsonResult> {
|
||||
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<results.NotFoundResult | results.JsonResult> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Contact | null>
|
||||
create(contact: Contact): Promise<Contact>
|
||||
save(contact: Contact): Promise<Contact>
|
||||
remove(contact: Contact): Promise<Contact>
|
||||
findAll(query: ContactQuery): Promise<Contact[]>
|
||||
}
|
||||
@@ -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<Contact>) {}
|
||||
|
||||
async create(contact: Contact): Promise<Contact> {
|
||||
return this.ormRepository.save(contact)
|
||||
}
|
||||
|
||||
async save(contact: Contact): Promise<Contact> {
|
||||
return this.ormRepository.save(contact)
|
||||
}
|
||||
|
||||
findByUuid(uuid: string): Promise<Contact | null> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('contact')
|
||||
.where('contact.uuid = :uuid', {
|
||||
uuid,
|
||||
})
|
||||
.getOne()
|
||||
}
|
||||
|
||||
async remove(contact: Contact): Promise<Contact> {
|
||||
return this.ormRepository.remove(contact)
|
||||
}
|
||||
|
||||
async findAll(query: ContactQuery): Promise<Contact[]> {
|
||||
return this.createFindAllQueryBuilder(query).getMany()
|
||||
}
|
||||
|
||||
private createFindAllQueryBuilder(query: ContactQuery): SelectQueryBuilder<Contact> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<Contact | null> {
|
||||
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<boolean> {
|
||||
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<Contact[]> {
|
||||
return this.contactRepository.findAll({ userUuid: dto.userUuid, lastSyncTime: dto.lastSyncTime })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Contact } from '../Model/Contact'
|
||||
|
||||
export interface ContactServiceInterface {
|
||||
createContact(dto: { userUuid: string; contactUuid: string; contactPublicKey: string }): Promise<Contact | null>
|
||||
|
||||
deleteContact(dto: { contactUuid: string; originatorUuid: string }): Promise<boolean>
|
||||
|
||||
getUserContacts(dto: { userUuid: string; lastSyncTime?: number }): Promise<Contact[]>
|
||||
}
|
||||
+2
-1
@@ -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<GroupUserKey | null>
|
||||
findByUserAndUuid(userUuid: string, userKeyUuid: string): Promise<GroupUserKey | null>
|
||||
findAsSenderOrRecipientByUuid(userUuid: string, userKeyUuid: string): Promise<GroupUserKey | null>
|
||||
create(group: GroupUserKey): Promise<GroupUserKey>
|
||||
save(groupUser: GroupUserKey): Promise<GroupUserKey>
|
||||
remove(group: GroupUserKey): Promise<GroupUserKey>
|
||||
|
||||
+12
-3
@@ -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<GroupUserKey | null> {
|
||||
findAsSenderOrRecipientByUuid(userUuid: string, userKeyUuid: string): Promise<GroupUserKey | null> {
|
||||
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,
|
||||
|
||||
@@ -87,7 +87,7 @@ export class GroupUserKeyService implements GroupUserKeyServiceInterface {
|
||||
}[]
|
||||
}): Promise<boolean> {
|
||||
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<GroupUserKey[]> {
|
||||
return this.groupUserRepository.findAllForUser({
|
||||
userUuid: dto.userUuid,
|
||||
includeSentAndReceived: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GroupUserKeyProjection>
|
||||
contacts: Array<ContactProjection>
|
||||
}
|
||||
|
||||
+13
-4
@@ -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<ItemConflict, ItemConflictProjection>,
|
||||
private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
|
||||
private groupUserKeyProjector: ProjectorInterface<GroupUserKey, GroupUserKeyProjection>,
|
||||
private contactProjector: ProjectorInterface<Contact, ContactProjection>,
|
||||
) {}
|
||||
|
||||
async createResponse(syncItemsResponse: SyncItemsResponse): Promise<SyncResponse20200115> {
|
||||
const retrievedItems = []
|
||||
for (const item of syncItemsResponse.retrievedItems) {
|
||||
retrievedItems.push(<ItemProjection>await this.itemProjector.projectFull(item))
|
||||
retrievedItems.push(<ItemProjection>this.itemProjector.projectFull(item))
|
||||
}
|
||||
|
||||
const savedItems = []
|
||||
for (const item of syncItemsResponse.savedItems) {
|
||||
savedItems.push(<SavedItemProjection>await this.savedItemProjector.projectFull(item))
|
||||
savedItems.push(<SavedItemProjection>this.savedItemProjector.projectFull(item))
|
||||
}
|
||||
|
||||
const conflicts = []
|
||||
for (const itemConflict of syncItemsResponse.conflicts) {
|
||||
conflicts.push(<ItemConflictProjection>await this.itemConflictProjector.projectFull(itemConflict))
|
||||
conflicts.push(<ItemConflictProjection>this.itemConflictProjector.projectFull(itemConflict))
|
||||
}
|
||||
|
||||
const groupKeys = []
|
||||
for (const groupKey of syncItemsResponse.groupKeys) {
|
||||
groupKeys.push(<GroupUserKeyProjection>await this.groupUserKeyProjector.projectFull(groupKey))
|
||||
groupKeys.push(<GroupUserKeyProjection>this.groupUserKeyProjector.projectFull(groupKey))
|
||||
}
|
||||
|
||||
const contacts = []
|
||||
for (const contact of syncItemsResponse.contacts) {
|
||||
contacts.push(<ContactProjection>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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SyncItemsResponse> {
|
||||
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
|
||||
|
||||
@@ -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<Item>
|
||||
conflicts: Array<ItemConflict>
|
||||
groupKeys: Array<GroupUserKey>
|
||||
contacts: Array<Contact>
|
||||
syncToken: string
|
||||
cursorToken?: string
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Contact } from '../Domain/Contact/Model/Contact'
|
||||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
import { ContactProjection } from './ContactProjection'
|
||||
|
||||
export class ContactProjector implements ProjectorInterface<Contact, ContactProjection> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user