feat: asymmetric messages

This commit is contained in:
Mo
2023-06-07 11:19:16 -05:00
parent 781e2a4685
commit 950af121cd
37 changed files with 549 additions and 152 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import '../src/Controller/ItemsController'
import '../src/Controller/SharedVaultsController'
import '../src/Controller/SharedVaultUsersController'
import '../src/Controller/SharedVaultInvitesController'
import '../src/Controller/ContactsController'
import '../src/Controller/AsymmetricMessagesController'
import helmet from 'helmet'
import * as cors from 'cors'
@@ -26,7 +26,7 @@ export class CreateSharedVaultInvites1684528646896 implements MigrationInterface
isNullable: false,
}),
new TableColumn({
name: 'inviter_uuid',
name: 'sender_uuid',
type: 'varchar',
length: '255',
isNullable: false,
@@ -1,5 +1,5 @@
import { TypeORMSharedVaultInviteRepository } from '../Domain/SharedVaultInvite/Repository/TypeORMSharedVaultInviteRepository'
import { Contact } from './../Domain/Contact/Model/Contact'
import { AsymmetricMessage } from './../Domain/AsymmetricMessage/Model/AsymmetricMessage'
import { SharedVaultInvite } from '../Domain/SharedVaultInvite/Model/SharedVaultInvite'
import { SharedVaultUser } from '../Domain/SharedVaultUser/Model/SharedVaultUser'
import { SharedVault } from '../Domain/SharedVault/Model/SharedVault'
@@ -33,16 +33,16 @@ import { SharedVaultProjection } from '../Projection/SharedVaultProjection'
import { SharedVaultProjector } from '../Projection/SharedVaultProjector'
import { SharedVaultInviteProjection } from '../Projection/SharedVaultInviteProjection'
import { SharedVaultInviteProjector } from '../Projection/SharedVaultInviteProjector'
import { ContactProjection } from '../Projection/ContactProjection'
import { ContactProjector } from '../Projection/ContactProjector'
import { AsymmetricMessageProjection } from '../Projection/AsymmetricMessageProjection'
import { AsymmetricMessageProjector } from '../Projection/AsymmetricMessageProjector'
import { SharedVaultInviteRepositoryInterface } from '../Domain/SharedVaultInvite/Repository/SharedVaultInviteRepositoryInterface'
import { ContactsRepositoryInterface } from '../Domain/Contact/Repository/ContactRepositoryInterface'
import { TypeORMContactRepository } from '../Domain/Contact/Repository/TypeORMContactRepository'
import { TypeORMAsymmetricMessageRepository } from '../Domain/AsymmetricMessage/Repository/TypeORMAsymmetricMessageRepository'
import { UserEventsRepositoryInterface } from '../Domain/UserEvent/Repository/UserEventRepositoryInterface'
import { TypeORMUserEventRepository } from '../Domain/UserEvent/Repository/TypeORMUserEventRepository'
import { UserEvent } from '../Domain/UserEvent/Model/UserEvent'
import { UserEventProjection } from '../Projection/UserEventProjection'
import { UserEventProjector } from '../Projection/UserEventProjector'
import { AsymmetricMessageRepositoryInterface } from '../Domain/AsymmetricMessage/Repository/AsymmetricMessageRepositoryInterface'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -117,9 +117,9 @@ export class CommonContainerConfigLoader {
return new TypeORMSharedVaultInviteRepository(context.container.get(TYPES.ORMSharedVaultInviteRepository))
})
container
.bind<ContactsRepositoryInterface>(TYPES.ContactRepository)
.bind<AsymmetricMessageRepositoryInterface>(TYPES.AsymmetricMessageRepository)
.toDynamicValue((context: interfaces.Context) => {
return new TypeORMContactRepository(context.container.get(TYPES.ORMContactRepository))
return new TypeORMAsymmetricMessageRepository(context.container.get(TYPES.ORMAsymmetricMessageRepository))
})
container
.bind<UserEventsRepositoryInterface>(TYPES.UserEventRepository)
@@ -139,8 +139,8 @@ export class CommonContainerConfigLoader {
.bind<Repository<SharedVaultInvite>>(TYPES.ORMSharedVaultInviteRepository)
.toDynamicValue(() => AppDataSource.getRepository(SharedVaultInvite))
container
.bind<Repository<Contact>>(TYPES.ORMContactRepository)
.toDynamicValue(() => AppDataSource.getRepository(Contact))
.bind<Repository<AsymmetricMessage>>(TYPES.ORMAsymmetricMessageRepository)
.toDynamicValue(() => AppDataSource.getRepository(AsymmetricMessage))
container
.bind<Repository<UserEvent>>(TYPES.ORMUserEventRepository)
.toDynamicValue(() => AppDataSource.getRepository(UserEvent))
@@ -166,9 +166,11 @@ export class CommonContainerConfigLoader {
.toDynamicValue(() => {
return new SharedVaultInviteProjector()
})
container.bind<ProjectorInterface<Contact, ContactProjection>>(TYPES.ContactProjector).toDynamicValue(() => {
return new ContactProjector()
})
container
.bind<ProjectorInterface<AsymmetricMessage, AsymmetricMessageProjection>>(TYPES.AsymmetricMessageProjector)
.toDynamicValue(() => {
return new AsymmetricMessageProjector()
})
container.bind<ProjectorInterface<UserEvent, UserEventProjection>>(TYPES.UserEventProjector).toDynamicValue(() => {
return new UserEventProjector()
})
@@ -1,5 +1,5 @@
import { AsymmetricMessage } from './../Domain/AsymmetricMessage/Model/AsymmetricMessage'
import { UserEvent } from './../Domain/UserEvent/Model/UserEvent'
import { Contact } from './../Domain/Contact/Model/Contact'
import { SharedVaultInvite } from '../Domain/SharedVaultInvite/Model/SharedVaultInvite'
import { SharedVaultUser } from '../Domain/SharedVaultUser/Model/SharedVaultUser'
import { DataSource, LoggerOptions } from 'typeorm'
@@ -43,7 +43,7 @@ const replicationConfig = {
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [Item, SharedVault, SharedVaultUser, SharedVaultInvite, Contact, UserEvent],
entities: [Item, SharedVault, SharedVaultUser, SharedVaultInvite, AsymmetricMessage, UserEvent],
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
@@ -1,5 +1,5 @@
import { SaveItem } from './../Domain/UseCase/SaveItem/SaveItem'
import { ContactFactory } from './../Domain/Contact/Factory/ContactFactory'
import { AsymmetricMessageFactory } from './../Domain/AsymmetricMessage/Factory/AsymmetricMessageFactory'
import { SharedVaultServiceInterface } from '../Domain/SharedVault/Service/SharedVaultServiceInterface'
import { SharedVaultUserFactory } from '../Domain/SharedVaultUser/Factory/SharedVaultUserFactory'
@@ -44,9 +44,9 @@ import { SharedVaultUserServiceInterface } from '../Domain/SharedVaultUser/Servi
import { SharedVaultInviteServiceInterface } from '../Domain/SharedVaultInvite/Service/SharedVaultInviteServiceInterface'
import { SharedVaultInviteService } from '../Domain/SharedVaultInvite/Service/SharedVaultInviteService'
import { SharedVaultInviteFactory } from '../Domain/SharedVaultInvite/Factory/SharedVaultInviteFactory'
import { ContactServiceInterface } from '../Domain/Contact/Service/ContactServiceInterface'
import { ContactService } from '../Domain/Contact/Service/ContactService'
import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface'
import { AsymmetricMessageServiceInterface } from '../Domain/AsymmetricMessage/Service/AsymmetricMessageServiceInterface'
import { AsymmetricMessageService } from '../Domain/AsymmetricMessage/Service/AsymmetricMessageService'
import { AsymmetricMessageFactoryInterface } from '../Domain/AsymmetricMessage/Factory/AsymmetricMessageFactoryInterface'
import { SharedVaultSnjsVersionFilter } from '../Domain/Item/SaveRule/SharedVaultSnjsVersionFilter'
import { CreateSharedVaultFileValetToken } from '../Domain/UseCase/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken'
import { UserEventServiceInterface } from '../Domain/UserEvent/Service/UserEventServiceInterface'
@@ -106,7 +106,7 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.ItemService),
context.container.get(TYPES.SharedVaultService),
context.container.get(TYPES.SharedVaultInviteService),
context.container.get(TYPES.ContactService),
context.container.get(TYPES.AsymmetricMessageService),
context.container.get(TYPES.UserEventService),
)
})
@@ -189,13 +189,15 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.Timer),
)
})
container.bind<ContactServiceInterface>(TYPES.ContactService).toDynamicValue((context: interfaces.Context) => {
return new ContactService(
context.container.get(TYPES.ContactRepository),
context.container.get(TYPES.ContactFactory),
context.container.get(TYPES.Timer),
)
})
container
.bind<AsymmetricMessageServiceInterface>(TYPES.AsymmetricMessageService)
.toDynamicValue((context: interfaces.Context) => {
return new AsymmetricMessageService(
context.container.get(TYPES.AsymmetricMessageRepository),
context.container.get(TYPES.AsymmetricMessageFactory),
context.container.get(TYPES.Timer),
)
})
container.bind<UserEventServiceInterface>(TYPES.UserEventService).toDynamicValue((context: interfaces.Context) => {
return new UserEventService(
context.container.get(TYPES.UserEventRepository),
@@ -217,7 +219,7 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
context.container.get(TYPES.ItemConflictProjector),
context.container.get(TYPES.SavedItemProjector),
context.container.get(TYPES.SharedVaultInviteProjector),
context.container.get(TYPES.ContactProjector),
context.container.get(TYPES.AsymmetricMessageProjector),
context.container.get(TYPES.SharedVaultProjector),
context.container.get(TYPES.UserEventProjector),
)
@@ -249,9 +251,11 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
.toDynamicValue((context: interfaces.Context) => {
return new SharedVaultInviteFactory(context.container.get(TYPES.Timer))
})
container.bind<ContactFactoryInterface>(TYPES.ContactFactory).toDynamicValue((context: interfaces.Context) => {
return new ContactFactory(context.container.get(TYPES.Timer))
})
container
.bind<AsymmetricMessageFactoryInterface>(TYPES.AsymmetricMessageFactory)
.toDynamicValue((context: interfaces.Context) => {
return new AsymmetricMessageFactory(context.container.get(TYPES.Timer))
})
container.bind<UserEventFactoryInterface>(TYPES.UserEventFactory).toDynamicValue((context: interfaces.Context) => {
return new UserEventFactory(context.container.get(TYPES.Timer))
})
@@ -11,7 +11,7 @@ const TYPES = {
ItemRepository: Symbol.for('ItemRepository'),
SharedVaultRepository: Symbol.for('SharedVaultRepository'),
SharedVaultUserRepository: Symbol.for('SharedVaultUserRepository'),
ContactRepository: Symbol.for('ContactRepository'),
AsymmetricMessageRepository: Symbol.for('AsymmetricMessageRepository'),
SharedVaultInviteRepository: Symbol.for('SharedVaultInviteRepository'),
UserEventRepository: Symbol.for('UserEventRepository'),
@@ -19,7 +19,7 @@ const TYPES = {
ORMItemRepository: Symbol.for('ORMItemRepository'),
ORMSharedVaultRepository: Symbol.for('ORMSharedVaultRepository'),
ORMSharedVaultUserRepository: Symbol.for('ORMSharedVaultUserRepository'),
ORMContactRepository: Symbol.for('ORMContactRepository'),
ORMAsymmetricMessageRepository: Symbol.for('ORMAsymmetricMessageRepository'),
ORMSharedVaultInviteRepository: Symbol.for('ORMSharedVaultInviteRepository'),
ORMUserEventRepository: Symbol.for('ORMUserEventRepository'),
@@ -32,14 +32,14 @@ const TYPES = {
ItemConflictProjector: Symbol.for('ItemConflictProjector'),
SharedVaultProjector: Symbol.for('SharedVaultProjector'),
SharedVaultUserProjector: Symbol.for('SharedVaultUserProjector'),
ContactProjector: Symbol.for('ContactProjector'),
AsymmetricMessageProjector: Symbol.for('AsymmetricMessageProjector'),
SharedVaultInviteProjector: Symbol.for('SharedVaultInviteProjector'),
UserEventProjector: Symbol.for('UserEventProjector'),
// Services
SharedVaultService: Symbol.for('SharedVaultService'),
SharedVaultUserService: Symbol.for('SharedVaultUserService'),
ContactService: Symbol.for('ContactService'),
AsymmetricMessageService: Symbol.for('AsymmetricMessageService'),
SharedVaultInviteService: Symbol.for('SharedVaultInviteService'),
UserEventService: Symbol.for('UserEventService'),
@@ -47,7 +47,7 @@ const TYPES = {
ItemFactory: Symbol.for('ItemFactory'),
SharedVaultFactory: Symbol.for('SharedVaultFactory'),
SharedVaultUserFactory: Symbol.for('SharedVaultUserFactory'),
ContactFactory: Symbol.for('ContactFactory'),
AsymmetricMessageFactory: Symbol.for('AsymmetricMessageFactory'),
SharedVaultInviteFactory: Symbol.for('SharedVaultInviteFactory'),
UserEventFactory: Symbol.for('UserEventFactory'),
@@ -78,7 +78,6 @@ const TYPES = {
SaveItem: Symbol.for('SaveItem'),
CreateSharedVaultFileReadValetToken: Symbol.for('CreateSharedVaultFileReadValetToken'),
// Handlers
UserCredentialsChangedEventHandler: Symbol.for('UserCredentialsChangedEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
@@ -30,7 +30,6 @@ import { EmailBackupRequestedEventHandler } from '../Domain/Handler/EmailBackupR
import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/ItemRevisionCreationRequestedEventHandler'
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
@@ -135,16 +134,6 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
)
})
container
.bind<UserCredentialsChangedEventHandler>(TYPES.UserCredentialsChangedEventHandler)
.toDynamicValue((context: interfaces.Context) => {
return new UserCredentialsChangedEventHandler(
context.container.get(TYPES.ContactRepository),
context.container.get(TYPES.SharedVaultInviteRepository),
context.container.get(TYPES.Timer),
)
})
container
.bind<SharedVaultFileUploadedEventHandler>(TYPES.SharedVaultFileUploadedEventHandler)
.to(SharedVaultFileUploadedEventHandler)
@@ -202,7 +191,6 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
['ACCOUNT_DELETION_REQUESTED', context.container.get(TYPES.AccountDeletionRequestedEventHandler)],
['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)],
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.SharedVaultFileRemovedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.SharedVaultFileUploadedEventHandler)],
])
@@ -0,0 +1,136 @@
import { Request, Response } from 'express'
import {
BaseHttpController,
controller,
httpPost,
results,
httpDelete,
httpGet,
httpPatch,
} from 'inversify-express-utils'
import TYPES from '../Bootstrap/Types'
import { inject } from 'inversify'
import { AsymmetricMessageServiceInterface } from '../Domain/AsymmetricMessage/Service/AsymmetricMessageServiceInterface'
import { AsymmetricMessageProjector } from '../Projection/AsymmetricMessageProjector'
@controller('/asymmetric-messages')
export class AsymmetricMessagesController extends BaseHttpController {
constructor(
@inject(TYPES.AsymmetricMessageService) private asymmetricMessageService: AsymmetricMessageServiceInterface,
@inject(TYPES.AsymmetricMessageProjector) private asymmetricMessageProjector: AsymmetricMessageProjector,
) {
super()
}
@httpGet('/', TYPES.AuthMiddleware)
public async getAsymmetricMessages(
_request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.asymmetricMessageService.getMessagesForUser({
userUuid: response.locals.user.uuid,
})
if (!result) {
return this.errorResponse(400, 'Could not get shared vault messages')
}
const projected = result.map((message) => this.asymmetricMessageProjector.projectFull(message))
return this.json({ messages: projected })
}
@httpPost('/', TYPES.AuthMiddleware)
public async createAsymmetricMessage(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.asymmetricMessageService.createMessage({
senderUuid: response.locals.user.uuid,
userUuid: request.body.messagee_uuid,
senderPublicKey: request.body.sender_public_key,
encryptedMessage: request.body.encrypted_message,
})
if (!result) {
return this.errorResponse(400, 'Could not create message')
}
return this.json({ message: this.asymmetricMessageProjector.projectFull(result) })
}
@httpPatch('/:messageUuid', TYPES.AuthMiddleware)
public async updateAsymmetricMessage(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.asymmetricMessageService.updateMessage({
senderUuid: response.locals.user.uuid,
messageUuid: request.params.messageUuid,
senderPublicKey: request.body.sender_public_key,
encryptedMessage: request.body.encrypted_message,
})
if (!result) {
return this.errorResponse(400, 'Could not update message')
}
return this.json({ message: this.asymmetricMessageProjector.projectFull(result) })
}
@httpDelete('/inbound', TYPES.AuthMiddleware)
public async deleteInboundAsymmetricMessages(
_request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
await this.asymmetricMessageService.deleteAllInboundMessages({
userUuid: response.locals.user.uuid,
})
return this.json({ success: true })
}
@httpGet('/outbound', TYPES.AuthMiddleware)
public async getOutboundAsymmetricMessages(
_request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.asymmetricMessageService.getOutboundMessagesForUser({
userUuid: response.locals.user.uuid,
})
if (!result) {
return this.errorResponse(400, 'Could not get shared vault messages')
}
const projected = result.map((message) => this.asymmetricMessageProjector.projectFull(message))
return this.json({ messages: projected })
}
@httpDelete('/:messageUuid', TYPES.AuthMiddleware)
public async deleteAsymmetricMessage(
request: Request,
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.asymmetricMessageService.deleteMessage({
originatorUuid: response.locals.user.uuid,
messageUuid: request.params.messageUuid,
})
if (!result) {
return this.errorResponse(400, 'Could not delete message')
}
return this.json({ success: true })
}
private errorResponse(status: number, message?: string, tag?: string) {
return this.json(
{
error: { message, tag },
},
status,
)
}
}
@@ -28,11 +28,11 @@ export class SharedVaultInvitesController extends BaseHttpController {
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.sharedVaultInviteService.createInvite({
originatorUuid: response.locals.user.uuid,
senderUuid: response.locals.user.uuid,
sharedVaultUuid: request.params.sharedVaultUuid,
userUuid: request.body.invitee_uuid,
inviterPublicKey: request.body.sender_public_key,
encryptedVaultKeyContent: request.body.encrypted_message,
senderPublicKey: request.body.sender_public_key,
encryptedMessage: request.body.encrypted_message,
permissions: request.body.permissions,
})
@@ -49,10 +49,10 @@ export class SharedVaultInvitesController extends BaseHttpController {
response: Response,
): Promise<results.NotFoundResult | results.JsonResult> {
const result = await this.sharedVaultInviteService.updateInvite({
originatorUuid: response.locals.user.uuid,
senderUuid: response.locals.user.uuid,
inviteUuid: request.params.inviteUuid,
inviterPublicKey: request.body.sender_public_key,
encryptedVaultKeyContent: request.body.encrypted_message,
senderPublicKey: request.body.sender_public_key,
encryptedMessage: request.body.encrypted_message,
permissions: request.body.permissions,
})
@@ -0,0 +1,32 @@
import { TimerInterface } from '@standardnotes/time'
import { AsymmetricMessageFactoryInterface } from './AsymmetricMessageFactoryInterface'
import { AsymmetricMessageHash } from './AsymmetricMessageHash'
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
export class AsymmetricMessageFactory implements AsymmetricMessageFactoryInterface {
constructor(private timer: TimerInterface) {}
create(dto: AsymmetricMessageHash): AsymmetricMessage {
const newAsymmetricMessage = new AsymmetricMessage()
newAsymmetricMessage.uuid = dto.uuid
newAsymmetricMessage.userUuid = dto.user_uuid
newAsymmetricMessage.senderUuid = dto.sender_uuid
newAsymmetricMessage.senderPublicKey = dto.sender_public_key
newAsymmetricMessage.encryptedMessage = dto.encrypted_message
const now = this.timer.getTimestampInMicroseconds()
newAsymmetricMessage.updatedAtTimestamp = now
newAsymmetricMessage.createdAtTimestamp = now
if (dto.created_at_timestamp) {
newAsymmetricMessage.createdAtTimestamp = dto.created_at_timestamp
}
return newAsymmetricMessage
}
createStub(dto: AsymmetricMessageHash): AsymmetricMessage {
const item = this.create(dto)
return item
}
}
@@ -0,0 +1,7 @@
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
import { AsymmetricMessageHash } from './AsymmetricMessageHash'
export interface AsymmetricMessageFactoryInterface {
create(dto: AsymmetricMessageHash): AsymmetricMessage
createStub(dto: AsymmetricMessageHash): AsymmetricMessage
}
@@ -0,0 +1,9 @@
export type AsymmetricMessageHash = {
uuid: string
user_uuid: string
sender_uuid: string
sender_public_key: string
encrypted_message: string
created_at_timestamp?: number
updated_at_timestamp?: number
}
@@ -0,0 +1,40 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'asymmetric_messages' })
export class AsymmetricMessage {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'user_uuid',
length: 36,
})
declare userUuid: string
@Column({
name: 'sender_uuid',
})
declare senderUuid: string
@Column({
name: 'sender_public_key',
})
declare senderPublicKey: string
@Column({
name: 'encrypted_message',
})
declare encryptedMessage: string
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
declare updatedAtTimestamp: number
}
@@ -0,0 +1,19 @@
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
export type AsymmetricMessageQuery = {
userUuid: string
}
export type AsymmetricMessageFindAllForUserQuery = {
userUuid?: string
lastSyncTime?: number
senderUuid?: string
}
export interface AsymmetricMessageRepositoryInterface {
findByUuid(asymmetricMessageUuid: string): Promise<AsymmetricMessage | null>
create(sharedVault: AsymmetricMessage): Promise<AsymmetricMessage>
save(asymmetricMessage: AsymmetricMessage): Promise<AsymmetricMessage>
remove(sharedVault: AsymmetricMessage): Promise<AsymmetricMessage>
findAll(query: AsymmetricMessageFindAllForUserQuery): Promise<AsymmetricMessage[]>
}
@@ -0,0 +1,57 @@
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
import { Repository, SelectQueryBuilder } from 'typeorm'
import {
AsymmetricMessageFindAllForUserQuery,
AsymmetricMessageRepositoryInterface,
} from './AsymmetricMessageRepositoryInterface'
export class TypeORMAsymmetricMessageRepository implements AsymmetricMessageRepositoryInterface {
constructor(private ormRepository: Repository<AsymmetricMessage>) {}
async create(asymmetricMessage: AsymmetricMessage): Promise<AsymmetricMessage> {
return this.ormRepository.save(asymmetricMessage)
}
async save(asymmetricMessage: AsymmetricMessage): Promise<AsymmetricMessage> {
return this.ormRepository.save(asymmetricMessage)
}
findByUuid(uuid: string): Promise<AsymmetricMessage | null> {
return this.ormRepository
.createQueryBuilder('asymmetric_message')
.where('asymmetric_message.uuid = :uuid', {
uuid,
})
.getOne()
}
async remove(asymmetricMessage: AsymmetricMessage): Promise<AsymmetricMessage> {
return this.ormRepository.remove(asymmetricMessage)
}
async findAll(query: AsymmetricMessageFindAllForUserQuery): Promise<AsymmetricMessage[]> {
return this.createFindAllQueryBuilder(query).getMany()
}
private createFindAllQueryBuilder(
query: AsymmetricMessageFindAllForUserQuery,
): SelectQueryBuilder<AsymmetricMessage> {
const queryBuilder = this.ormRepository.createQueryBuilder('asymmetric_message')
if (query.userUuid) {
queryBuilder.where('asymmetric_message.user_uuid = :userUuid', { userUuid: query.userUuid })
} else if (query.senderUuid) {
queryBuilder.where('asymmetric_message.sender_uuid = :senderUuid', {
senderUuid: query.senderUuid,
})
}
if (query.lastSyncTime) {
queryBuilder.andWhere('asymmetric_message.updated_at_timestamp > :lastSyncTime', {
lastSyncTime: query.lastSyncTime,
})
}
return queryBuilder
}
}
@@ -0,0 +1,82 @@
import { UpdateAsymmetricMessageDTO } from './UpdateAsymmetricMessageDTO'
import { TimerInterface } from '@standardnotes/time'
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
import { AsymmetricMessageFactoryInterface } from '../Factory/AsymmetricMessageFactoryInterface'
import { v4 as uuidv4 } from 'uuid'
import { AsymmetricMessageRepositoryInterface } from '../Repository/AsymmetricMessageRepositoryInterface'
import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface'
import { GetUserAsymmetricMessagesDTO } from './GetUserAsymmetricMessagesDTO'
import { CreateAsymmetricMessageDTO } from './CreateAsymmetricMessageDTO'
export class AsymmetricMessageService implements AsymmetricMessageServiceInterface {
constructor(
private asymmetricMessageRepository: AsymmetricMessageRepositoryInterface,
private asymmetricMessageFactory: AsymmetricMessageFactoryInterface,
private timer: TimerInterface,
) {}
async createMessage(dto: CreateAsymmetricMessageDTO): Promise<AsymmetricMessage | null> {
const timestamp = this.timer.getTimestampInMicroseconds()
const asymmetricMessage = this.asymmetricMessageFactory.create({
uuid: uuidv4(),
user_uuid: dto.userUuid,
sender_uuid: dto.senderUuid,
sender_public_key: dto.senderPublicKey,
encrypted_message: dto.encryptedMessage,
created_at_timestamp: timestamp,
updated_at_timestamp: timestamp,
})
return this.asymmetricMessageRepository.create(asymmetricMessage)
}
async updateMessage(dto: UpdateAsymmetricMessageDTO): Promise<AsymmetricMessage | null> {
const asymmetricMessage = await this.asymmetricMessageRepository.findByUuid(dto.messageUuid)
if (!asymmetricMessage || asymmetricMessage.senderUuid !== dto.senderUuid) {
return null
}
asymmetricMessage.senderPublicKey = dto.senderPublicKey
asymmetricMessage.encryptedMessage = dto.encryptedMessage
asymmetricMessage.updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
return this.asymmetricMessageRepository.save(asymmetricMessage)
}
getMessagesForUser(dto: GetUserAsymmetricMessagesDTO): Promise<AsymmetricMessage[]> {
return this.asymmetricMessageRepository.findAll(dto)
}
getOutboundMessagesForUser(dto: { userUuid: string }): Promise<AsymmetricMessage[]> {
return this.asymmetricMessageRepository.findAll({
senderUuid: dto.userUuid,
})
}
async deleteAllInboundMessages(dto: { userUuid: string }): Promise<void> {
const inboundMessages = await this.asymmetricMessageRepository.findAll({
userUuid: dto.userUuid,
})
for (const message of inboundMessages) {
await this.asymmetricMessageRepository.remove(message)
}
}
async deleteMessage(dto: { originatorUuid: string; messageUuid: string }): Promise<boolean> {
const message = await this.asymmetricMessageRepository.findByUuid(dto.messageUuid)
if (!message) {
return false
}
const isAuthorized = message.senderUuid === dto.originatorUuid || message.userUuid === dto.originatorUuid
if (!isAuthorized) {
return false
}
await this.asymmetricMessageRepository.remove(message)
return true
}
}
@@ -0,0 +1,15 @@
import { UpdateAsymmetricMessageDTO } from './UpdateAsymmetricMessageDTO'
import { AsymmetricMessage } from '../Model/AsymmetricMessage'
import { GetUserAsymmetricMessagesDTO } from './GetUserAsymmetricMessagesDTO'
import { CreateAsymmetricMessageDTO } from './CreateAsymmetricMessageDTO'
export interface AsymmetricMessageServiceInterface {
createMessage(dto: CreateAsymmetricMessageDTO): Promise<AsymmetricMessage | null>
updateMessage(dto: UpdateAsymmetricMessageDTO): Promise<AsymmetricMessage | null>
getMessagesForUser(dto: GetUserAsymmetricMessagesDTO): Promise<AsymmetricMessage[]>
getOutboundMessagesForUser(dto: { userUuid: string }): Promise<AsymmetricMessage[]>
deleteAllInboundMessages(dto: { userUuid: string }): Promise<void>
deleteMessage(dto: { originatorUuid: string; messageUuid: string }): Promise<boolean>
}
@@ -0,0 +1,6 @@
export type CreateAsymmetricMessageDTO = {
userUuid: string
senderUuid: string
senderPublicKey: string
encryptedMessage: string
}
@@ -0,0 +1,4 @@
export interface GetUserAsymmetricMessagesDTO {
userUuid: string
lastSyncTime?: number
}
@@ -0,0 +1,6 @@
export type UpdateAsymmetricMessageDTO = {
messageUuid: string
senderUuid: string
senderPublicKey: string
encryptedMessage: string
}
@@ -1,48 +0,0 @@
import {
DomainEventHandlerInterface,
UserCredentialsChangedEvent,
UserCredentialsChangedEventPayload,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { ContactsRepositoryInterface } from '../Contact/Repository/ContactRepositoryInterface'
import { SharedVaultInviteRepositoryInterface } from '../SharedVaultInvite/Repository/SharedVaultInviteRepositoryInterface'
export class UserCredentialsChangedEventHandler implements DomainEventHandlerInterface {
constructor(
private contactRepository: ContactsRepositoryInterface,
private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
private timer: TimerInterface,
) {}
async handle(event: UserCredentialsChangedEvent): Promise<void> {
await this.updatePublicKeyOfAllContacts(event.payload)
await this.nullifyPendingInboundSharedVaultInvites(event.payload)
}
private async updatePublicKeyOfAllContacts(payload: UserCredentialsChangedEventPayload): Promise<void> {
const contacts = await this.contactRepository.findAll({ contactUuid: payload.userUuid })
for (const contact of contacts) {
if (contact.contactPublicKey === payload.newPublicKey) {
continue
}
contact.contactPublicKey = payload.newPublicKey
contact.contactSigningPublicKey = payload.newSigningPublicKey
contact.updatedAtTimestamp = this.timer.getTimestampInMicroseconds()
await this.contactRepository.save(contact)
}
}
private async nullifyPendingInboundSharedVaultInvites(payload: UserCredentialsChangedEventPayload): Promise<void> {
const inboundInvites = await this.sharedVaultInviteRepository.findAll({
userUuid: payload.userUuid,
})
for (const invite of inboundInvites) {
await this.sharedVaultInviteRepository.remove(invite)
}
}
}
@@ -1,4 +1,4 @@
import { ContactProjection } from '../../../Projection/ContactProjection'
import { AsymmetricMessageProjection } from './../../../Projection/AsymmetricMessageProjection'
import { SharedVaultInviteProjection } from '../../../Projection/SharedVaultInviteProjection'
import { SharedVaultProjection } from '../../../Projection/SharedVaultProjection'
import { ItemConflictProjection } from '../../../Projection/ItemConflictProjection'
@@ -12,7 +12,7 @@ export type SyncResponse20200115 = {
conflicts: Array<ItemConflictProjection>
sync_token: string
cursor_token?: string
contacts: Array<ContactProjection>
asymmetric_messages: Array<AsymmetricMessageProjection>
shared_vaults: Array<SharedVaultProjection>
shared_vault_invites: Array<SharedVaultInviteProjection>
user_events: Array<UserEventProjection>
@@ -1,5 +1,6 @@
import { AsymmetricMessageProjection } from './../../../Projection/AsymmetricMessageProjection'
import { AsymmetricMessage } from './../../AsymmetricMessage/Model/AsymmetricMessage'
import { SharedVaultInvite } from '../../SharedVaultInvite/Model/SharedVaultInvite'
import { Contact } from './../../Contact/Model/Contact'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { SyncItemsResponse } from '../../UseCase/SyncItemsResponse'
import { Item } from '../Item'
@@ -9,7 +10,6 @@ import { ItemProjection } from '../../../Projection/ItemProjection'
import { SyncResponse20200115 } from './SyncResponse20200115'
import { SyncResponseFactoryInterface } from './SyncResponseFactoryInterface'
import { SavedItemProjection } from '../../../Projection/SavedItemProjection'
import { ContactProjection } from '../../../Projection/ContactProjection'
import { SharedVaultInviteProjection } from '../../../Projection/SharedVaultInviteProjection'
import { SharedVaultProjection } from '../../../Projection/SharedVaultProjection'
import { SharedVault } from '../../SharedVault/Model/SharedVault'
@@ -22,7 +22,7 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface
private itemConflictProjector: ProjectorInterface<ItemConflict, ItemConflictProjection>,
private savedItemProjector: ProjectorInterface<Item, SavedItemProjection>,
private sharedVaultInviteProjector: ProjectorInterface<SharedVaultInvite, SharedVaultInviteProjection>,
private contactProjector: ProjectorInterface<Contact, ContactProjection>,
private asymmetricMessageProjector: ProjectorInterface<AsymmetricMessage, AsymmetricMessageProjection>,
private sharedVaultProjector: ProjectorInterface<SharedVault, SharedVaultProjection>,
private userEventProjector: ProjectorInterface<UserEvent, UserEventProjection>,
) {}
@@ -53,9 +53,9 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface
sharedVaultInvites.push(this.sharedVaultInviteProjector.projectFull(sharedVaultInvite))
}
const contacts = []
for (const contact of syncItemsResponse.contacts) {
contacts.push(this.contactProjector.projectFull(contact))
const asymmetricMessages = []
for (const contact of syncItemsResponse.asymmetricMessages) {
asymmetricMessages.push(this.asymmetricMessageProjector.projectFull(contact))
}
const userEvents = []
@@ -69,10 +69,10 @@ export class SyncResponseFactory20200115 implements SyncResponseFactoryInterface
conflicts,
sync_token: syncItemsResponse.syncToken,
cursor_token: syncItemsResponse.cursorToken,
asymmetric_messages: asymmetricMessages,
user_events: userEvents,
shared_vaults: sharedVaults,
shared_vault_invites: sharedVaultInvites,
user_events: userEvents,
contacts,
}
}
}
@@ -11,9 +11,9 @@ export class SharedVaultInviteFactory implements SharedVaultInviteFactoryInterfa
newSharedVaultInvite.uuid = dto.uuid
newSharedVaultInvite.userUuid = dto.user_uuid
newSharedVaultInvite.sharedVaultUuid = dto.shared_vault_uuid
newSharedVaultInvite.inviterUuid = dto.inviter_uuid
newSharedVaultInvite.inviterPublicKey = dto.sender_public_key
newSharedVaultInvite.encryptedVaultKeyContent = dto.encrypted_message
newSharedVaultInvite.senderUuid = dto.sender_uuid
newSharedVaultInvite.senderPublicKey = dto.sender_public_key
newSharedVaultInvite.encryptedMessage = dto.encrypted_message
newSharedVaultInvite.permissions = dto.permissions
const now = this.timer.getTimestampInMicroseconds()
@@ -4,7 +4,7 @@ export type SharedVaultInviteHash = {
uuid: string
user_uuid: string
shared_vault_uuid: string
inviter_uuid: string
sender_uuid: string
sender_public_key: string
encrypted_message: string
permissions: SharedVaultUserPermission
@@ -19,19 +19,19 @@ export class SharedVaultInvite {
declare userUuid: string
@Column({
name: 'inviter_uuid',
name: 'sender_uuid',
})
declare inviterUuid: string
declare senderUuid: string
@Column({
name: 'sender_public_key',
})
declare inviterPublicKey: string
declare senderPublicKey: string
@Column({
name: 'encrypted_message',
})
declare encryptedVaultKeyContent: string
declare encryptedMessage: string
@Column({
name: 'permissions',
@@ -7,7 +7,7 @@ export type SharedVaultInviteQuery = {
export type SharedVaultInviteFindAllForUserQuery = {
userUuid?: string
lastSyncTime?: number
inviterUuid?: string
senderUuid?: string
}
export type SharedVaultInviteFindAllForSharedVault = {
@@ -54,9 +54,9 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
if (query.userUuid) {
queryBuilder.where('shared_vault_invite.user_uuid = :userUuid', { userUuid: query.userUuid })
} else if (query.inviterUuid) {
queryBuilder.where('shared_vault_invite.inviter_uuid = :inviterUuid', {
inviterUuid: query.inviterUuid,
} else if (query.senderUuid) {
queryBuilder.where('shared_vault_invite.sender_uuid = :senderUuid', {
senderUuid: query.senderUuid,
})
}
@@ -1,10 +1,10 @@
import { SharedVaultUserPermission } from '../../SharedVaultUser/Model/SharedVaultUserPermission'
export type CreateInviteDTO = {
originatorUuid: string
sharedVaultUuid: string
userUuid: string
inviterPublicKey: string
encryptedVaultKeyContent: string
sharedVaultUuid: string
senderUuid: string
senderPublicKey: string
encryptedMessage: string
permissions: SharedVaultUserPermission
}
@@ -21,7 +21,7 @@ export class SharedVaultInviteService implements SharedVaultInviteServiceInterfa
async createInvite(dto: CreateInviteDTO): Promise<SharedVaultInvite | null> {
const sharedVault = await this.sharedVaultRepository.findByUuid(dto.sharedVaultUuid)
if (!sharedVault || sharedVault.userUuid !== dto.originatorUuid) {
if (!sharedVault || sharedVault.userUuid !== dto.senderUuid) {
return null
}
@@ -31,9 +31,9 @@ export class SharedVaultInviteService implements SharedVaultInviteServiceInterfa
uuid: uuidv4(),
user_uuid: dto.userUuid,
shared_vault_uuid: dto.sharedVaultUuid,
inviter_uuid: dto.originatorUuid,
sender_public_key: dto.inviterPublicKey,
encrypted_message: dto.encryptedVaultKeyContent,
sender_uuid: dto.senderUuid,
sender_public_key: dto.senderPublicKey,
encrypted_message: dto.encryptedMessage,
permissions: dto.permissions,
created_at_timestamp: timestamp,
updated_at_timestamp: timestamp,
@@ -44,12 +44,12 @@ export class SharedVaultInviteService implements SharedVaultInviteServiceInterfa
async updateInvite(dto: UpdateInviteDTO): Promise<SharedVaultInvite | null> {
const sharedVaultInvite = await this.sharedVaultInviteRepository.findByUuid(dto.inviteUuid)
if (!sharedVaultInvite || sharedVaultInvite.inviterUuid !== dto.originatorUuid) {
if (!sharedVaultInvite || sharedVaultInvite.senderUuid !== dto.senderUuid) {
return null
}
sharedVaultInvite.inviterPublicKey = dto.inviterPublicKey
sharedVaultInvite.encryptedVaultKeyContent = dto.encryptedVaultKeyContent
sharedVaultInvite.senderPublicKey = dto.senderPublicKey
sharedVaultInvite.encryptedMessage = dto.encryptedMessage
if (dto.permissions) {
sharedVaultInvite.permissions = dto.permissions
}
@@ -64,7 +64,7 @@ export class SharedVaultInviteService implements SharedVaultInviteServiceInterfa
getOutboundInvitesForUser(dto: { userUuid: string }): Promise<SharedVaultInvite[]> {
return this.sharedVaultInviteRepository.findAll({
inviterUuid: dto.userUuid,
senderUuid: dto.userUuid,
})
}
@@ -145,7 +145,7 @@ export class SharedVaultInviteService implements SharedVaultInviteServiceInterfa
return false
}
const isAuthorized = invite.inviterUuid === dto.originatorUuid || invite.userUuid === dto.originatorUuid
const isAuthorized = invite.senderUuid === dto.originatorUuid || invite.userUuid === dto.originatorUuid
if (!isAuthorized) {
return false
}
@@ -1,9 +1,9 @@
import { SharedVaultUserPermission } from '../../SharedVaultUser/Model/SharedVaultUserPermission'
export type UpdateInviteDTO = {
originatorUuid: string
inviteUuid: string
inviterPublicKey: string
encryptedVaultKeyContent: string
senderUuid: string
senderPublicKey: string
encryptedMessage: string
permissions?: SharedVaultUserPermission
}
@@ -1,4 +1,3 @@
import { ContactServiceInterface } from './../Contact/Service/ContactServiceInterface'
import { Item } from '../Item/Item'
import { ItemConflict } from '../Item/ItemConflict'
import { ItemServiceInterface } from '../Item/ItemServiceInterface'
@@ -8,13 +7,14 @@ import { UseCaseInterface } from './UseCaseInterface'
import { SharedVaultInviteServiceInterface } from '../SharedVaultInvite/Service/SharedVaultInviteServiceInterface'
import { SharedVaultServiceInterface } from '../SharedVault/Service/SharedVaultServiceInterface'
import { UserEventServiceInterface } from '../UserEvent/Service/UserEventServiceInterface'
import { AsymmetricMessageServiceInterface } from '../AsymmetricMessage/Service/AsymmetricMessageServiceInterface'
export class SyncItems implements UseCaseInterface {
constructor(
private itemService: ItemServiceInterface,
private sharedVaultService: SharedVaultServiceInterface,
private sharedVaultInviteService: SharedVaultInviteServiceInterface,
private contactService: ContactServiceInterface,
private asymmetricMessageService: AsymmetricMessageServiceInterface,
private userEventService: UserEventServiceInterface,
) {}
@@ -58,7 +58,7 @@ export class SyncItems implements UseCaseInterface {
lastSyncTime,
})
const contacts = await this.contactService.getUserContacts({
const asymmetricMessages = await this.asymmetricMessageService.getMessagesForUser({
userUuid: dto.userUuid,
lastSyncTime: lastSyncTime,
})
@@ -77,7 +77,7 @@ export class SyncItems implements UseCaseInterface {
sharedVaults,
sharedVaultInvites,
userEvents,
contacts,
asymmetricMessages,
}
return syncResponse
@@ -1,5 +1,5 @@
import { AsymmetricMessage } from './../AsymmetricMessage/Model/AsymmetricMessage'
import { SharedVaultInvite } from '../SharedVaultInvite/Model/SharedVaultInvite'
import { Contact } from '../Contact/Model/Contact'
import { Item } from '../Item/Item'
import { ItemConflict } from '../Item/ItemConflict'
import { SharedVault } from '../SharedVault/Model/SharedVault'
@@ -12,7 +12,7 @@ export type SyncItemsResponse = {
sharedVaults: Array<SharedVault>
sharedVaultInvites: Array<SharedVaultInvite>
userEvents: Array<UserEvent>
contacts: Array<Contact>
asymmetricMessages: Array<AsymmetricMessage>
syncToken: string
cursorToken?: string
}
@@ -0,0 +1,9 @@
export type AsymmetricMessageProjection = {
uuid: string
user_uuid: string
sender_uuid: string
sender_public_key: string
encrypted_message: string
created_at_timestamp: number
updated_at_timestamp: number
}
@@ -0,0 +1,30 @@
import { AsymmetricMessage } from '../Domain/AsymmetricMessage/Model/AsymmetricMessage'
import { ProjectorInterface } from './ProjectorInterface'
import { AsymmetricMessageProjection } from './AsymmetricMessageProjection'
export class AsymmetricMessageProjector implements ProjectorInterface<AsymmetricMessage, AsymmetricMessageProjection> {
projectSimple(_asymmetricMessage: AsymmetricMessage): AsymmetricMessageProjection {
throw Error('not implemented')
}
projectCustom(_projectionType: string, asymmetricMessage: AsymmetricMessage): AsymmetricMessageProjection {
const fullProjection = this.projectFull(asymmetricMessage)
return {
...fullProjection,
user_uuid: asymmetricMessage.userUuid,
}
}
projectFull(asymmetricMessage: AsymmetricMessage): AsymmetricMessageProjection {
return {
uuid: asymmetricMessage.uuid,
user_uuid: asymmetricMessage.userUuid,
sender_uuid: asymmetricMessage.senderUuid,
sender_public_key: asymmetricMessage.senderPublicKey,
encrypted_message: asymmetricMessage.encryptedMessage,
created_at_timestamp: asymmetricMessage.createdAtTimestamp,
updated_at_timestamp: asymmetricMessage.updatedAtTimestamp,
}
}
}
@@ -2,7 +2,7 @@ export type SharedVaultInviteProjection = {
uuid: string
shared_vault_uuid: string
user_uuid: string
inviter_uuid: string
sender_uuid: string
sender_public_key: string
encrypted_message: string
permissions: string
@@ -21,9 +21,9 @@ export class SharedVaultInviteProjector implements ProjectorInterface<SharedVaul
uuid: sharedVaultInvite.uuid,
shared_vault_uuid: sharedVaultInvite.sharedVaultUuid,
user_uuid: sharedVaultInvite.userUuid,
inviter_uuid: sharedVaultInvite.inviterUuid,
sender_public_key: sharedVaultInvite.inviterPublicKey,
encrypted_message: sharedVaultInvite.encryptedVaultKeyContent,
sender_uuid: sharedVaultInvite.senderUuid,
sender_public_key: sharedVaultInvite.senderPublicKey,
encrypted_message: sharedVaultInvite.encryptedMessage,
permissions: sharedVaultInvite.permissions,
created_at_timestamp: sharedVaultInvite.createdAtTimestamp,
updated_at_timestamp: sharedVaultInvite.updatedAtTimestamp,