wip: default rotation

This commit is contained in:
Mo
2023-05-19 12:59:28 -05:00
parent 417b0257cf
commit c416ed6078
40 changed files with 571 additions and 131 deletions
@@ -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)
-2
View File
@@ -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)
-1
View File
@@ -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)],
+1
View File
@@ -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(
+21 -6
View File
@@ -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[]>
}
@@ -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>
@@ -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>
}
@@ -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,
}
}
}