BaseItemsController.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import { ControllerContainerInterface, MapperInterface, Validator } from '@standardnotes/domain-core'
  2. import { BaseHttpController, results } from 'inversify-express-utils'
  3. import { Request, Response } from 'express'
  4. import { HttpStatusCode } from '@standardnotes/responses'
  5. import { Item } from '../../../Domain/Item/Item'
  6. import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
  7. import { CheckIntegrity } from '../../../Domain/UseCase/Syncing/CheckIntegrity/CheckIntegrity'
  8. import { GetItem } from '../../../Domain/UseCase/Syncing/GetItem/GetItem'
  9. import { ApiVersion } from '../../../Domain/Api/ApiVersion'
  10. import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
  11. import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
  12. import { ItemHash } from '../../../Domain/Item/ItemHash'
  13. import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
  14. import { Metric } from '../../../Domain/Metrics/Metric'
  15. import { Logger } from 'winston'
  16. export class BaseItemsController extends BaseHttpController {
  17. constructor(
  18. protected checkForTrafficAbuse: CheckForTrafficAbuse,
  19. protected syncItems: SyncItems,
  20. protected checkIntegrity: CheckIntegrity,
  21. protected getItem: GetItem,
  22. protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
  23. protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
  24. protected logger: Logger,
  25. protected strictAbuseProtection: boolean,
  26. protected itemOperationsAbuseTimeframeLengthInMinutes: number,
  27. protected itemOperationsAbuseThreshold: number,
  28. protected payloadSizeAbuseThreshold: number,
  29. protected payloadSizeAbuseTimeframeLengthInMinutes: number,
  30. private controllerContainer?: ControllerContainerInterface,
  31. ) {
  32. super()
  33. if (this.controllerContainer !== undefined) {
  34. this.controllerContainer.register('sync.items.sync', this.sync.bind(this))
  35. this.controllerContainer.register('sync.items.check_integrity', this.checkItemsIntegrity.bind(this))
  36. this.controllerContainer.register('sync.items.get_item', this.getSingleItem.bind(this))
  37. }
  38. }
  39. async sync(request: Request, response: Response): Promise<results.JsonResult> {
  40. const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
  41. metricToCheck: Metric.NAMES.ItemOperation,
  42. userUuid: response.locals.user.uuid,
  43. threshold: this.itemOperationsAbuseThreshold,
  44. timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
  45. })
  46. if (checkForItemOperationsAbuseResult.isFailed()) {
  47. this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
  48. userId: response.locals.user.uuid,
  49. })
  50. if (this.strictAbuseProtection) {
  51. return this.json({ error: { message: checkForItemOperationsAbuseResult.getError() } }, 429)
  52. }
  53. }
  54. const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
  55. metricToCheck: Metric.NAMES.ContentSizeUtilized,
  56. userUuid: response.locals.user.uuid,
  57. threshold: this.payloadSizeAbuseThreshold,
  58. timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
  59. })
  60. if (checkForPayloadSizeAbuseResult.isFailed()) {
  61. this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
  62. userId: response.locals.user.uuid,
  63. })
  64. if (this.strictAbuseProtection) {
  65. return this.json({ error: { message: checkForPayloadSizeAbuseResult.getError() } }, 429)
  66. }
  67. }
  68. const itemHashes: ItemHash[] = []
  69. if ('items' in request.body) {
  70. for (const itemHashInput of request.body.items) {
  71. const itemHashOrError = ItemHash.create({
  72. ...itemHashInput,
  73. user_uuid: response.locals.user.uuid,
  74. key_system_identifier: itemHashInput.key_system_identifier ?? null,
  75. shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null,
  76. })
  77. if (itemHashOrError.isFailed()) {
  78. return this.json({ error: { message: itemHashOrError.getError() } }, HttpStatusCode.BadRequest)
  79. }
  80. itemHashes.push(itemHashOrError.getValue())
  81. }
  82. }
  83. let sharedVaultUuids: string[] | undefined = undefined
  84. if ('shared_vault_uuids' in request.body) {
  85. const sharedVaultUuidsValidation = Validator.isNotEmpty(request.body.shared_vault_uuids)
  86. if (!sharedVaultUuidsValidation.isFailed()) {
  87. sharedVaultUuids = request.body.shared_vault_uuids
  88. }
  89. }
  90. const syncResult = await this.syncItems.execute({
  91. userUuid: response.locals.user.uuid,
  92. itemHashes,
  93. computeIntegrityHash: request.body.compute_integrity === true,
  94. syncToken: request.body.sync_token,
  95. cursorToken: request.body.cursor_token,
  96. limit: request.body.limit,
  97. contentType: request.body.content_type,
  98. apiVersion: request.body.api ?? ApiVersion.v20161215,
  99. snjsVersion: <string>request.headers['x-snjs-version'],
  100. readOnlyAccess: response.locals.readOnlyAccess,
  101. sessionUuid: response.locals.session ? response.locals.session.uuid : null,
  102. sharedVaultUuids,
  103. isFreeUser: response.locals.isFreeUser,
  104. })
  105. if (syncResult.isFailed()) {
  106. return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)
  107. }
  108. const syncResponse = await this.syncResponseFactoryResolver
  109. .resolveSyncResponseFactoryVersion(request.body.api)
  110. .createResponse(syncResult.getValue())
  111. return this.json(syncResponse)
  112. }
  113. async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
  114. let integrityPayloads = []
  115. if ('integrityPayloads' in request.body) {
  116. integrityPayloads = request.body.integrityPayloads
  117. }
  118. const result = await this.checkIntegrity.execute({
  119. userUuid: response.locals.user.uuid,
  120. integrityPayloads,
  121. })
  122. if (result.isFailed()) {
  123. return this.json({ error: { message: result.getError() } }, HttpStatusCode.BadRequest)
  124. }
  125. return this.json({
  126. mismatches: result.getValue(),
  127. })
  128. }
  129. async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
  130. const result = await this.getItem.execute({
  131. userUuid: response.locals.user.uuid,
  132. itemUuid: request.params.uuid,
  133. })
  134. if (result.isFailed()) {
  135. return this.json(
  136. {
  137. error: { message: 'Item not found' },
  138. },
  139. 404,
  140. )
  141. }
  142. return this.json({ item: this.itemHttpMapper.toProjection(result.getValue()) })
  143. }
  144. }