Compare commits

..

32 Commits

Author SHA1 Message Date
standardci
8097d7239d chore(release): publish new version
- @standardnotes/analytics@2.34.8
 - @standardnotes/api-gateway@1.89.6
 - @standardnotes/auth-server@1.177.3
 - @standardnotes/domain-events-infra@1.22.7
 - @standardnotes/domain-events@2.139.1
 - @standardnotes/files-server@1.37.2
 - @standardnotes/home-server@1.22.27
 - @standardnotes/revisions-server@1.51.8
 - @standardnotes/scheduler-server@1.27.13
 - @standardnotes/syncing-server@1.129.4
 - @standardnotes/websockets-server@1.22.4
2023-12-26 11:09:32 +00:00
Karol Sójko
7962b245b5 fix: stop revisions propagation (#1008) 2023-12-26 11:40:09 +01:00
standardci
ce0450becf chore(release): publish new version
- @standardnotes/api-gateway@1.89.5
 - @standardnotes/auth-server@1.177.2
 - @standardnotes/home-server@1.22.26
 - @standardnotes/syncing-server@1.129.3
2023-12-22 14:03:05 +00:00
Karol Sójko
0762ed1127 fix: logs severity 2023-12-22 14:42:41 +01:00
standardci
3563a0b64a chore(release): publish new version
- @standardnotes/api-gateway@1.89.4
 - @standardnotes/auth-server@1.177.1
 - @standardnotes/home-server@1.22.25
2023-12-22 11:50:04 +00:00
Karol Sójko
0342cdcfc0 fix(auth): specs issue 2023-12-22 12:19:44 +01:00
Karol Sójko
b1cb6a1d21 fix: case sensitive typo 2023-12-22 12:14:58 +01:00
standardci
615e4184f6 chore(release): publish new version
- @standardnotes/api-gateway@1.89.3
 - @standardnotes/home-server@1.22.24
 - @standardnotes/syncing-server@1.129.2
2023-12-22 10:35:31 +00:00
Karol Sójko
54091f23da fix: add more readonly access debug logs 2023-12-22 11:14:54 +01:00
Karol Sójko
e4a8324db2 fix(api-gateway): add debug log for operating on readonly access 2023-12-22 11:08:46 +01:00
standardci
01bb55be39 chore(release): publish new version
- @standardnotes/api-gateway@1.89.2
 - @standardnotes/home-server@1.22.23
2023-12-21 15:35:54 +00:00
Karol Sójko
e551a364f6 fix(api-gateway): missing readonly access for demo when utilizing grpc workflow (#1005) 2023-12-21 16:14:55 +01:00
standardci
7e65f85377 chore(release): publish new version
- @standardnotes/files-server@1.37.1
 - @standardnotes/home-server@1.22.22
2023-12-14 12:29:00 +00:00
Karol Sójko
e6a21606a3 fix(files): list files command 2023-12-14 13:08:18 +01:00
standardci
f275b48770 chore(release): publish new version
- @standardnotes/analytics@2.34.7
 - @standardnotes/api-gateway@1.89.1
 - @standardnotes/auth-server@1.177.0
 - @standardnotes/domain-events-infra@1.22.6
 - @standardnotes/domain-events@2.139.0
 - @standardnotes/files-server@1.37.0
 - @standardnotes/home-server@1.22.21
 - @standardnotes/revisions-server@1.51.7
 - @standardnotes/scheduler-server@1.27.12
 - @standardnotes/syncing-server@1.129.1
 - @standardnotes/websockets-server@1.22.3
2023-12-14 11:51:47 +00:00
Karol Sójko
de4fcf9a4c feat: add procedure for recalculating file quota for user (#980)
* fix(auth): safe guard file upload bytes used to be positive intiger

* feat: add procedure for recalculating file quota for user

* add more meta to logs
2023-12-14 12:31:19 +01:00
standardci
a1455d281f chore(release): publish new version
- @standardnotes/api-gateway@1.89.0
 - @standardnotes/auth-server@1.176.5
 - @standardnotes/home-server@1.22.20
 - @standardnotes/syncing-server@1.129.0
2023-12-12 12:18:49 +00:00
Karol Sójko
cfbe2bbac6 fix(auth): add user uuid context to sign in emails log context 2023-12-12 12:58:11 +01:00
Karol Sójko
398c10ce4b feat(syncing-server): add extended revisions frequency for free users (#965) 2023-12-12 12:42:31 +01:00
standardci
c7d21b092d chore(release): publish new version
- @standardnotes/auth-server@1.176.4
 - @standardnotes/home-server@1.22.19
2023-12-11 13:20:06 +00:00
Karol Sójko
031fa71e7d fix(auth): generate new recovery codes when enabling mfa (#964) 2023-12-11 13:59:57 +01:00
standardci
948e843ad6 chore(release): publish new version
- @standardnotes/analytics@2.34.6
 - @standardnotes/api-gateway@1.88.4
 - @standardnotes/auth-server@1.176.3
 - @standardnotes/domain-events-infra@1.22.5
 - @standardnotes/domain-events@2.138.2
 - @standardnotes/files-server@1.36.6
 - @standardnotes/home-server@1.22.18
 - @standardnotes/revisions-server@1.51.6
 - @standardnotes/scheduler-server@1.27.11
 - @standardnotes/syncing-server@1.128.2
 - @standardnotes/websockets-server@1.22.2
2023-12-11 11:25:27 +00:00
Karol Sójko
7b0ea0a069 fix(syncing-server): add user uuid for the emails requesting backup 2023-12-11 12:05:16 +01:00
standardci
8887b6e642 chore(release): publish new version
- @standardnotes/auth-server@1.176.2
 - @standardnotes/home-server@1.22.17
 - @standardnotes/syncing-server@1.128.1
2023-12-11 10:15:36 +00:00
Karol Sójko
597ff13393 fix(syncing-server): logs meta for email with backup requested 2023-12-11 10:55:16 +01:00
Karol Sójko
4ab61b94a4 fix(auth): error log meta on triggering email backups 2023-12-11 10:52:17 +01:00
standardci
e19652d62a chore(release): publish new version
- @standardnotes/api-gateway@1.88.3
 - @standardnotes/home-server@1.22.16
2023-12-08 16:15:29 +00:00
Karol Sójko
a341e78909 fix(api-gateway): add extra meta to logs 2023-12-08 16:55:18 +01:00
standardci
48e52ac48c chore(release): publish new version
- @standardnotes/home-server@1.22.15
 - @standardnotes/syncing-server@1.128.0
2023-12-08 12:30:03 +00:00
Karol Sójko
6dbb87708f feat(syncing-server): send websocket event to shared vault members upon items change in shared vault (#961) 2023-12-08 13:09:35 +01:00
standardci
d15d51eae6 chore(release): publish new version
- @standardnotes/api-gateway@1.88.2
 - @standardnotes/home-server@1.22.14
2023-12-07 13:48:28 +00:00
Karol Sójko
0058368681 fix(api-gateway): add userId to logs in error handler if possible 2023-12-07 14:28:12 +01:00
103 changed files with 1822 additions and 32 deletions

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.34.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.7...@standardnotes/analytics@2.34.8) (2023-12-26)
**Note:** Version bump only for package @standardnotes/analytics
## [2.34.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.6...@standardnotes/analytics@2.34.7) (2023-12-14)
**Note:** Version bump only for package @standardnotes/analytics
## [2.34.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.5...@standardnotes/analytics@2.34.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.34.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.4...@standardnotes/analytics@2.34.5) (2023-12-07)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.34.5",
"version": "2.34.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,61 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.89.6](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.5...@standardnotes/api-gateway@1.89.6) (2023-12-26)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.89.5](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.4...@standardnotes/api-gateway@1.89.5) (2023-12-22)
### Bug Fixes
* logs severity ([0762ed1](https://github.com/standardnotes/server/commit/0762ed1127a5de295bd50c14afad31c7fb88a853))
## [1.89.4](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.3...@standardnotes/api-gateway@1.89.4) (2023-12-22)
### Bug Fixes
* case sensitive typo ([b1cb6a1](https://github.com/standardnotes/server/commit/b1cb6a1d21479b67d9826b55e1221a803e2f41a7))
## [1.89.3](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.2...@standardnotes/api-gateway@1.89.3) (2023-12-22)
### Bug Fixes
* add more readonly access debug logs ([54091f2](https://github.com/standardnotes/server/commit/54091f23da33a5d1df2ecffdfa559e3fa4c562f6))
* **api-gateway:** add debug log for operating on readonly access ([e4a8324](https://github.com/standardnotes/server/commit/e4a8324db26454a7587f37096653ad9565541295))
## [1.89.2](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.1...@standardnotes/api-gateway@1.89.2) (2023-12-21)
### Bug Fixes
* **api-gateway:** missing readonly access for demo when utilizing grpc workflow ([#1005](https://github.com/standardnotes/server/issues/1005)) ([e551a36](https://github.com/standardnotes/server/commit/e551a364f63e28c9329dbce492488b9f112e3473))
## [1.89.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.0...@standardnotes/api-gateway@1.89.1) (2023-12-14)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.89.0](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.4...@standardnotes/api-gateway@1.89.0) (2023-12-12)
### Features
* **syncing-server:** add extended revisions frequency for free users ([#965](https://github.com/standardnotes/server/issues/965)) ([398c10c](https://github.com/standardnotes/server/commit/398c10ce4b8e357728a8b4f354b3bf6ccc8e438d))
## [1.88.4](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.3...@standardnotes/api-gateway@1.88.4) (2023-12-11)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.88.3](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.2...@standardnotes/api-gateway@1.88.3) (2023-12-08)
### Bug Fixes
* **api-gateway:** add extra meta to logs ([a341e78](https://github.com/standardnotes/server/commit/a341e789093556f09c2a337e39a8053abdcf587b))
## [1.88.2](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.1...@standardnotes/api-gateway@1.88.2) (2023-12-07)
### Bug Fixes
* **api-gateway:** add userId to logs in error handler if possible ([0058368](https://github.com/standardnotes/server/commit/005836868126ae5fa4c4468644704938aea0f4ec))
## [1.88.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.0...@standardnotes/api-gateway@1.88.1) (2023-12-07)
### Bug Fixes

View File

@@ -96,6 +96,7 @@ void container.load().then((container) => {
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
logger.debug(
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.88.1",
"version": "1.89.6",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -74,6 +74,13 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
if (response.locals.readOnlyAccess) {
this.logger.debug('User operates on read-only access', {
codeTag: 'AuthMiddleware',
userId: response.locals.user.uuid,
})
}
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
} catch (error) {
let detailedErrorMessage = (error as Error).message

View File

@@ -5,6 +5,7 @@ import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { Logger } from 'winston'
import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
import { RoleName } from '@standardnotes/domain-core'
export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
constructor(
@@ -96,6 +97,8 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.isFreeUser =
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
} catch (error) {
this.logger.error(
`Could not pass the request to websocket connection validation on underlying service: ${

View File

@@ -240,6 +240,9 @@ export class HttpServiceProxy implements ServiceProxyInterface {
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
},
)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)

View File

@@ -275,6 +275,9 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
},
)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)

View File

@@ -27,10 +27,17 @@ export class GRPCSyncingServerServiceProxy {
const metadata = new Metadata()
metadata.set('x-user-uuid', response.locals.user.uuid)
metadata.set('x-snjs-version', request.headers['x-snjs-version'] as string)
metadata.set('x-read-only-access', response.locals.readonlyAccess ? 'true' : 'false')
metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false')
if (response.locals.readOnlyAccess) {
this.logger.debug('Syncing with read-only access', {
codeTag: 'GRPCSyncingServerServiceProxy',
userId: response.locals.user.uuid,
})
}
if (response.locals.session) {
metadata.set('x-session-uuid', response.locals.session.uuid)
}
metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false')
this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
if (error) {

View File

@@ -3,6 +3,53 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.177.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.2...@standardnotes/auth-server@1.177.3) (2023-12-26)
### Bug Fixes
* stop revisions propagation ([#1008](https://github.com/standardnotes/server/issues/1008)) ([7962b24](https://github.com/standardnotes/server/commit/7962b245b51703ff7d33728dee117b2074f2692a))
## [1.177.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.1...@standardnotes/auth-server@1.177.2) (2023-12-22)
### Bug Fixes
* logs severity ([0762ed1](https://github.com/standardnotes/server/commit/0762ed1127a5de295bd50c14afad31c7fb88a853))
## [1.177.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.0...@standardnotes/auth-server@1.177.1) (2023-12-22)
### Bug Fixes
* **auth:** specs issue ([0342cdc](https://github.com/standardnotes/server/commit/0342cdcfc06bd3bb0192a428c30c5af15e5eb0d7))
* case sensitive typo ([b1cb6a1](https://github.com/standardnotes/server/commit/b1cb6a1d21479b67d9826b55e1221a803e2f41a7))
# [1.177.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.5...@standardnotes/auth-server@1.177.0) (2023-12-14)
### Features
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
## [1.176.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.4...@standardnotes/auth-server@1.176.5) (2023-12-12)
### Bug Fixes
* **auth:** add user uuid context to sign in emails log context ([cfbe2bb](https://github.com/standardnotes/server/commit/cfbe2bbac60e9014d7ba0967e4b996fba7dc8629))
## [1.176.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.3...@standardnotes/auth-server@1.176.4) (2023-12-11)
### Bug Fixes
* **auth:** generate new recovery codes when enabling mfa ([#964](https://github.com/standardnotes/server/issues/964)) ([031fa71](https://github.com/standardnotes/server/commit/031fa71e7d86221ec7fb0f4b21c62454646564e2))
## [1.176.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.2...@standardnotes/auth-server@1.176.3) (2023-12-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.176.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.1...@standardnotes/auth-server@1.176.2) (2023-12-11)
### Bug Fixes
* **auth:** error log meta on triggering email backups ([4ab61b9](https://github.com/standardnotes/server/commit/4ab61b94a4aee361399a76c9f2b6b977c4832b06))
## [1.176.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.0...@standardnotes/auth-server@1.176.1) (2023-12-07)
### Bug Fixes

View File

@@ -0,0 +1,45 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { FixStorageQuotaForUser } from '../src/Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
const inputArgs = process.argv.slice(2)
const userEmail = inputArgs[0]
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Auth_Logger)
logger.info('Starting storage quota fix...', {
userId: userEmail,
})
const fixStorageQuota = container.get<FixStorageQuotaForUser>(TYPES.Auth_FixStorageQuotaForUser)
Promise.resolve(
fixStorageQuota.execute({
userEmail,
}),
)
.then(() => {
logger.info('Storage quota fixed', {
userId: userEmail,
})
process.exit(0)
})
.catch((error) => {
logger.error(`Could not fix storage quota: ${error.message}`, {
userId: userEmail,
})
process.exit(1)
})
})

View File

@@ -0,0 +1,71 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
import { SubscriptionPlanName } from '@standardnotes/domain-core'
const requestCleanup = async (
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const proSubscriptionPlan = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.ProPlan).getValue()
const plusSubscriptionPlan = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue()
const totalSubscriptions = await userSubscriptionRepository.countByPlanName([
proSubscriptionPlan,
plusSubscriptionPlan,
])
const limitPerPage = 100
const numberOfPages = Math.ceil(totalSubscriptions / limitPerPage)
for (let i = 0; i < numberOfPages; i++) {
const subscriptions = await userSubscriptionRepository.findByPlanName(
[proSubscriptionPlan, plusSubscriptionPlan],
i * limitPerPage,
limitPerPage,
)
for (const subscription of subscriptions) {
await domainEventPublisher.publish(
domainEventFactory.createRevisionsCleanupRequestedEvent({
userUuid: subscription.userUuid,
}),
)
}
}
}
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Auth_Logger)
logger.info('Starting revisions cleanup triggering...')
const domainEventFactory = container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory)
const domainEventPublisher = container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher)
const userSubscriptionRepository = container.get<UserSubscriptionRepositoryInterface>(
TYPES.Auth_UserSubscriptionRepository,
)
Promise.resolve(requestCleanup(userSubscriptionRepository, domainEventFactory, domainEventPublisher))
.then(() => {
logger.info('Finished revisions cleanup triggering')
process.exit(0)
})
.catch((error) => {
logger.error(`Failed to trigger revisions cleanup: ${(error as Error).message}`)
process.exit(1)
})
})

View File

@@ -0,0 +1,11 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/fix_quota.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index

View File

@@ -0,0 +1,11 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/revisions_cleanup.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index

View File

@@ -5,48 +5,49 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-web' )
echo "[Docker] Starting Web..."
exec node docker/entrypoint-server.js
;;
'start-worker' )
echo "[Docker] Starting Worker..."
exec node docker/entrypoint-worker.js
;;
'cleanup' )
echo "[Docker] Starting Cleanup..."
exec node docker/entrypoint-cleanup.js
;;
'stats' )
echo "[Docker] Starting Persisting Stats..."
exec node docker/entrypoint-stats.js
;;
'email-daily-backup' )
echo "[Docker] Starting Email Daily Backup..."
exec node docker/entrypoint-backup.js daily
;;
'email-weekly-backup' )
echo "[Docker] Starting Email Weekly Backup..."
exec node docker/entrypoint-backup.js weekly
;;
'email-backup' )
echo "[Docker] Starting Email Backup For Single User..."
EMAIL=$1 && shift 1
exec node docker/entrypoint-user-email-backup.js $EMAIL
;;
'fix-quota' )
EMAIL=$1 && shift 1
exec node docker/entrypoint-fix-quota.js $EMAIL
;;
'delete-accounts' )
echo "[Docker] Starting Accounts Deleting from CSV..."
FILE_NAME=$1 && shift 1
MODE=$1 && shift 1
exec node docker/entrypoint-delete-accounts.js $FILE_NAME $MODE
;;
'revisions-cleanup' )
exec node docker/entrypoint-revisions-cleanup.js
;;
* )
echo "[Docker] Unknown command"
;;

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.176.1",
"version": "1.177.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -282,6 +282,8 @@ import { S3CsvFileReader } from '../Infra/S3/S3CsvFileReader'
import { DeleteAccountsFromCSVFile } from '../Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile'
import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler/AccountDeletionVerificationPassedEventHandler'
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -1269,6 +1271,7 @@ export class ContainerConfigLoader {
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
),
)
container
@@ -1284,6 +1287,20 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
container
.bind<FixStorageQuotaForUser>(TYPES.Auth_FixStorageQuotaForUser)
.toConstantValue(
new FixStorageQuotaForUser(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
container.get<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
if (!isConfiguredForHomeServer) {
container
.bind<DeleteAccountsFromCSVFile>(TYPES.Auth_DeleteAccountsFromCSVFile)
@@ -1540,6 +1557,14 @@ export class ContainerConfigLoader {
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
),
)
container
.bind<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler)
.toConstantValue(
new FileQuotaRecalculatedEventHandler(
container.get<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get<winston.Logger>(TYPES.Auth_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
@@ -1577,6 +1602,10 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
],
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
[
'FILE_QUOTA_RECALCULATED',
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
],
])
if (isConfiguredForHomeServer) {

View File

@@ -170,6 +170,7 @@ const TYPES = {
Auth_TriggerEmailBackupForAllUsers: Symbol.for('Auth_TriggerEmailBackupForAllUsers'),
Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'),
Auth_RenewSharedSubscriptions: Symbol.for('Auth_RenewSharedSubscriptions'),
Auth_FixStorageQuotaForUser: Symbol.for('Auth_FixStorageQuotaForUser'),
// Handlers
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
Auth_AccountDeletionVerificationPassedEventHandler: Symbol.for('Auth_AccountDeletionVerificationPassedEventHandler'),
@@ -203,6 +204,7 @@ const TYPES = {
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
),
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),

View File

@@ -27,6 +27,7 @@ describe('AuthenticationMethodResolver', () => {
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.info = jest.fn()
user = {} as jest.Mocked<User>

View File

@@ -36,6 +36,10 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
}
const userUuid = userUuidOrError.getValue()
this.logger.debug('User utilizing JWT authentication method.', {
userId: userUuid.value,
})
return {
type: 'jwt',
user: await this.userRepository.findOneByUuid(userUuid),

View File

@@ -21,6 +21,8 @@ import {
SessionCreatedEvent,
SessionRefreshedEvent,
AccountDeletionVerificationRequestedEvent,
FileQuotaRecalculationRequestedEvent,
RevisionsCleanupRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -34,6 +36,36 @@ import { KeyParamsData } from '@standardnotes/responses'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
createRevisionsCleanupRequestedEvent(dto: { userUuid: string }): RevisionsCleanupRequestedEvent {
return {
type: 'REVISIONS_CLEANUP_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
return {
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createAccountDeletionVerificationRequestedEvent(dto: {
userUuid: string
email: string
@@ -159,6 +191,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
level: string
body: string
subject: string
userUuid?: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_REQUESTED',

View File

@@ -19,11 +19,15 @@ import {
SessionCreatedEvent,
SessionRefreshedEvent,
AccountDeletionVerificationRequestedEvent,
FileQuotaRecalculationRequestedEvent,
RevisionsCleanupRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
import { KeyParamsData } from '@standardnotes/responses'
export interface DomainEventFactoryInterface {
createRevisionsCleanupRequestedEvent(dto: { userUuid: string }): RevisionsCleanupRequestedEvent
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
createEmailRequestedEvent(dto: {
userEmail: string
@@ -31,6 +35,7 @@ export interface DomainEventFactoryInterface {
level: string
body: string
subject: string
userUuid?: string
}): EmailRequestedEvent
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
createUserRegisteredEvent(dto: {

View File

@@ -0,0 +1,38 @@
import { DomainEventHandlerInterface, FileQuotaRecalculatedEvent } from '@standardnotes/domain-events'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { Logger } from 'winston'
export class FileQuotaRecalculatedEventHandler implements DomainEventHandlerInterface {
constructor(
private updateStorageQuota: UpdateStorageQuotaUsedForUser,
private logger: Logger,
) {}
async handle(event: FileQuotaRecalculatedEvent): Promise<void> {
this.logger.info('Updating storage quota for user...', {
userId: event.payload.userUuid,
totalFileByteSize: event.payload.totalFileByteSize,
codeTag: 'FileQuotaRecalculatedEventHandler',
})
const result = await this.updateStorageQuota.execute({
userUuid: event.payload.userUuid,
bytesUsed: event.payload.totalFileByteSize,
})
if (result.isFailed()) {
this.logger.error('Could not update storage quota', {
userId: event.payload.userUuid,
codeTag: 'FileQuotaRecalculatedEventHandler',
})
return
}
this.logger.info('Storage quota updated', {
userId: event.payload.userUuid,
totalFileByteSize: event.payload.totalFileByteSize,
codeTag: 'FileQuotaRecalculatedEventHandler',
})
}
}

View File

@@ -35,6 +35,7 @@ export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerI
subject: getSubject(),
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
userEmail: user.email,
userUuid: user.uuid,
}),
)
}

View File

@@ -1,3 +1,4 @@
import { SubscriptionPlanName } from '@standardnotes/domain-core'
import { UserSubscription } from './UserSubscription'
import { UserSubscriptionType } from './UserSubscriptionType'
@@ -7,6 +8,8 @@ export interface UserSubscriptionRepositoryInterface {
findOneByUserUuid(userUuid: string): Promise<UserSubscription | null>
findOneByUserUuidAndType(userUuid: string, type: UserSubscriptionType): Promise<UserSubscription | null>
findByUserUuid(userUuid: string): Promise<UserSubscription[]>
countByPlanName(planNames: SubscriptionPlanName[]): Promise<number>
findByPlanName(planNames: SubscriptionPlanName[], offset: number, limit: number): Promise<UserSubscription[]>
findOneByUserUuidAndSubscriptionId(userUuid: string, subscriptionId: number): Promise<UserSubscription | null>
findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>
findBySubscriptionId(subscriptionId: number): Promise<UserSubscription[]>

View File

@@ -0,0 +1,204 @@
import { DomainEventPublisherInterface, FileQuotaRecalculationRequestedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
import { FixStorageQuotaForUser } from './FixStorageQuotaForUser'
import { User } from '../../User/User'
import { Result } from '@standardnotes/domain-core'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
describe('FixStorageQuotaForUser', () => {
let userRepository: UserRepositoryInterface
let getRegularSubscription: GetRegularSubscriptionForUser
let getSharedSubscriptionForUser: GetSharedSubscriptionForUser
let setSubscriptonSettingValue: SetSubscriptionSettingValue
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let logger: Logger
const createUseCase = () =>
new FixStorageQuotaForUser(
userRepository,
getRegularSubscription,
getSharedSubscriptionForUser,
setSubscriptonSettingValue,
listSharedSubscriptionInvitations,
domainEventFactory,
domainEventPublisher,
logger,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<User>)
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
getRegularSubscription.execute = jest.fn().mockReturnValue(
Result.ok({
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<UserSubscription>),
)
getSharedSubscriptionForUser = {} as jest.Mocked<GetSharedSubscriptionForUser>
getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(
Result.ok({
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<UserSubscription>),
)
setSubscriptonSettingValue = {} as jest.Mocked<SetSubscriptionSettingValue>
setSubscriptonSettingValue.execute = jest.fn().mockReturnValue(Result.ok(Result.ok()))
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
invitations: [
{
uuid: '00000000-0000-0000-0000-000000000000',
status: InvitationStatus.Accepted,
inviteeIdentifier: 'test2@test.te',
} as jest.Mocked<SharedSubscriptionInvitation>,
],
})
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createFileQuotaRecalculationRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<FileQuotaRecalculationRequestedEvent>)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
})
it('should return error result if user cannot be found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error result if regular subscription cannot be found', async () => {
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('test'))
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error result if shared subscription cannot be found', async () => {
getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(Result.fail('test'))
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error result if setting value cannot be set', async () => {
setSubscriptonSettingValue.execute = jest.fn().mockReturnValue(Result.fail('test'))
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should reset storage quota and ask for recalculation for user and all its shared subscriptions', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
})
it('should return error if the username is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: '',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if the invitee username is invalid', async () => {
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
invitations: [
{
uuid: '00000000-0000-0000-0000-000000000000',
status: InvitationStatus.Accepted,
inviteeIdentifier: '',
} as jest.Mocked<SharedSubscriptionInvitation>,
],
})
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if the invitee cannot be found', async () => {
userRepository.findOneByUsernameOrEmail = jest
.fn()
.mockReturnValueOnce({
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<User>)
.mockReturnValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if fails to reset storage quota for the invitee', async () => {
setSubscriptonSettingValue.execute = jest
.fn()
.mockReturnValueOnce(Result.ok())
.mockReturnValueOnce(Result.fail('test'))
const useCase = createUseCase()
const result = await useCase.execute({
userEmail: 'test@test.te',
})
expect(result.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,121 @@
import { Result, SettingName, UseCaseInterface, Username } from '@standardnotes/domain-core'
import { FixStorageQuotaForUserDTO } from './FixStorageQuotaForUserDTO'
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { Logger } from 'winston'
export class FixStorageQuotaForUser implements UseCaseInterface<void> {
constructor(
private userRepository: UserRepositoryInterface,
private getRegularSubscription: GetRegularSubscriptionForUser,
private getSharedSubscriptionForUser: GetSharedSubscriptionForUser,
private setSubscriptonSettingValue: SetSubscriptionSettingValue,
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private logger: Logger,
) {}
async execute(dto: FixStorageQuotaForUserDTO): Promise<Result<void>> {
const usernameOrError = Username.create(dto.userEmail)
if (usernameOrError.isFailed()) {
return Result.fail(usernameOrError.getError())
}
const username = usernameOrError.getValue()
const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user === null) {
return Result.fail(`Could not find user with email: ${username.value}`)
}
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
userUuid: user.uuid,
})
if (regularSubscriptionOrError.isFailed()) {
return Result.fail(`Could not find regular user subscription for user with uuid: ${user.uuid}`)
}
const regularSubscription = regularSubscriptionOrError.getValue()
const result = await this.setSubscriptonSettingValue.execute({
userSubscriptionUuid: regularSubscription.uuid,
settingName: SettingName.NAMES.FileUploadBytesUsed,
value: '0',
})
if (result.isFailed()) {
return Result.fail(result.getError())
}
this.logger.info('Resetted storage quota for user', {
userId: user.uuid,
})
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileQuotaRecalculationRequestedEvent({
userUuid: user.uuid,
}),
)
this.logger.info('Requested storage quota recalculation for user', {
userId: user.uuid,
})
const invitationsResult = await this.listSharedSubscriptionInvitations.execute({
inviterEmail: user.email,
})
const acceptedInvitations = invitationsResult.invitations.filter(
(invitation) => invitation.status === InvitationStatus.Accepted,
)
for (const invitation of acceptedInvitations) {
const inviteeUsernameOrError = Username.create(invitation.inviteeIdentifier)
if (inviteeUsernameOrError.isFailed()) {
return Result.fail(inviteeUsernameOrError.getError())
}
const inviteeUsername = inviteeUsernameOrError.getValue()
const invitee = await this.userRepository.findOneByUsernameOrEmail(inviteeUsername)
if (invitee === null) {
return Result.fail(`Could not find user with email: ${inviteeUsername.value}`)
}
const invitationSubscriptionOrError = await this.getSharedSubscriptionForUser.execute({
userUuid: invitee.uuid,
})
if (invitationSubscriptionOrError.isFailed()) {
return Result.fail(`Could not find shared subscription for user with email: ${invitation.inviteeIdentifier}`)
}
const invitationSubscription = invitationSubscriptionOrError.getValue()
const result = await this.setSubscriptonSettingValue.execute({
userSubscriptionUuid: invitationSubscription.uuid,
settingName: SettingName.NAMES.FileUploadBytesUsed,
value: '0',
})
if (result.isFailed()) {
return Result.fail(result.getError())
}
this.logger.info('Resetted storage quota for user', {
userId: invitee.uuid,
})
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileQuotaRecalculationRequestedEvent({
userUuid: invitee.uuid,
}),
)
this.logger.info('Requested storage quota recalculation for user', {
userId: invitee.uuid,
})
}
return Result.ok()
}
}

View File

@@ -0,0 +1,3 @@
export interface FixStorageQuotaForUserDTO {
userEmail: string
}

View File

@@ -131,6 +131,7 @@ export class SignIn implements UseCaseInterface {
),
messageIdentifier: 'SIGN_IN',
subject: getSubject(user.email),
userUuid: user.uuid,
}),
)
} catch (error) {

View File

@@ -39,7 +39,9 @@ export class TriggerEmailBackupForAllUsers implements UseCaseInterface<void> {
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Failed to trigger email backup for user ${setting.props.userUuid.value}`)
this.logger.error(`Failed to trigger email backup for user: ${result.getError()}`, {
userId: setting.props.userUuid.value,
})
failedUsers++
}
}

View File

@@ -7,6 +7,7 @@ import {
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
import { SettingName, Result } from '@standardnotes/domain-core'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
import { TriggerPostSettingUpdateActions } from './TriggerPostSettingUpdateActions'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
@@ -15,11 +16,20 @@ describe('TriggerPostSettingUpdateActions', () => {
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let triggerEmailBackupForUser: TriggerEmailBackupForUser
let generateRecoveryCodes: GenerateRecoveryCodes
const createUseCase = () =>
new TriggerPostSettingUpdateActions(domainEventPublisher, domainEventFactory, triggerEmailBackupForUser)
new TriggerPostSettingUpdateActions(
domainEventPublisher,
domainEventFactory,
triggerEmailBackupForUser,
generateRecoveryCodes,
)
beforeEach(() => {
generateRecoveryCodes = {} as jest.Mocked<GenerateRecoveryCodes>
generateRecoveryCodes.execute = jest.fn().mockReturnValue(Result.ok())
triggerEmailBackupForUser = {} as jest.Mocked<TriggerEmailBackupForUser>
triggerEmailBackupForUser.execute = jest.fn().mockReturnValue(Result.ok())
@@ -101,4 +111,15 @@ describe('TriggerPostSettingUpdateActions', () => {
username: 'test@test.te',
})
})
it('should generate new recovery codes upon enabling mfa setting', async () => {
await createUseCase().execute({
updatedSettingName: SettingName.NAMES.MfaSecret,
userUuid: '4-5-6',
userEmail: 'test@test.te',
unencryptedValue: '123',
})
expect(generateRecoveryCodes.execute).toHaveBeenCalled()
})
})

View File

@@ -5,6 +5,7 @@ import { EmailBackupFrequency, LogSessionUserAgentOption } from '@standardnotes/
import { TriggerPostSettingUpdateActionsDTO } from './TriggerPostSettingUpdateActionsDTO'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
@@ -18,6 +19,7 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private triggerEmailBackupForUser: TriggerEmailBackupForUser,
private generateRecoveryCodes: GenerateRecoveryCodes,
) {}
async execute(dto: TriggerPostSettingUpdateActionsDTO): Promise<Result<void>> {
@@ -35,6 +37,12 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
await this.triggerSessionUserAgentCleanup(dto.userEmail, dto.userUuid)
}
if (this.isEnablingMFASetting(dto.updatedSettingName, dto.unencryptedValue)) {
await this.generateRecoveryCodes.execute({
userUuid: dto.userUuid,
})
}
return Result.ok()
}
@@ -54,6 +62,10 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
)
}
private isEnablingMFASetting(settingName: string, newValue: string | null): boolean {
return settingName === SettingName.NAMES.MfaSecret && newValue !== null
}
private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
}

View File

@@ -163,6 +163,20 @@ describe('UpdateStorageQuotaUsedForUser', () => {
})
})
it('should not subtract below 0', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: -1234,
})
expect(result.isFailed()).toBeFalsy()
expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({
settingName: 'FILE_UPLOAD_BYTES_USED',
value: '0',
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
})
})
it('should update a bytes used setting on both regular and shared subscription', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',

View File

@@ -68,10 +68,13 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
bytesAlreadyUsed = bytesUsedSetting.setting.props.value as string
}
const bytesUsedNewTotal = +bytesAlreadyUsed + bytesUsed
const bytesUsedValue = bytesUsedNewTotal < 0 ? 0 : bytesUsedNewTotal
const result = await this.setSubscriptonSettingValue.execute({
userSubscriptionUuid: subscription.uuid,
settingName: SettingName.NAMES.FileUploadBytesUsed,
value: (+bytesAlreadyUsed + bytesUsed).toString(),
value: bytesUsedValue.toString(),
})
/* istanbul ignore next */

View File

@@ -6,6 +6,7 @@ import TYPES from '../../Bootstrap/Types'
import { UserSubscription } from '../../Domain/Subscription/UserSubscription'
import { UserSubscriptionRepositoryInterface } from '../../Domain/Subscription/UserSubscriptionRepositoryInterface'
import { UserSubscriptionType } from '../../Domain/Subscription/UserSubscriptionType'
import { SubscriptionPlanName } from '@standardnotes/domain-core'
@injectable()
export class TypeORMUserSubscriptionRepository implements UserSubscriptionRepositoryInterface {
@@ -15,6 +16,27 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
) {}
async countByPlanName(planNames: SubscriptionPlanName[]): Promise<number> {
return await this.ormRepository
.createQueryBuilder()
.where('plan_name IN (:...planNames)', {
planNames: planNames.map((planName) => planName.value),
})
.getCount()
}
async findByPlanName(planNames: SubscriptionPlanName[], offset: number, limit: number): Promise<UserSubscription[]> {
return await this.ormRepository
.createQueryBuilder()
.where('plan_name IN (:...planNames)', {
planNames: planNames.map((planName) => planName.value),
})
.orderBy('created_at', 'ASC')
.skip(offset)
.take(limit)
.getMany()
}
async countActiveSubscriptions(): Promise<number> {
return await this.ormRepository
.createQueryBuilder()

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.6...@standardnotes/domain-events-infra@1.22.7) (2023-12-26)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.22.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.5...@standardnotes/domain-events-infra@1.22.6) (2023-12-14)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.22.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.4...@standardnotes/domain-events-infra@1.22.5) (2023-12-11)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.22.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.3...@standardnotes/domain-events-infra@1.22.4) (2023-12-01)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.22.4",
"version": "1.22.7",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.139.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.139.0...@standardnotes/domain-events@2.139.1) (2023-12-26)
### Bug Fixes
* stop revisions propagation ([#1008](https://github.com/standardnotes/server/issues/1008)) ([7962b24](https://github.com/standardnotes/server/commit/7962b245b51703ff7d33728dee117b2074f2692a))
# [2.139.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.2...@standardnotes/domain-events@2.139.0) (2023-12-14)
### Features
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
## [2.138.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.1...@standardnotes/domain-events@2.138.2) (2023-12-11)
### Bug Fixes
* **syncing-server:** add user uuid for the emails requesting backup ([7b0ea0a](https://github.com/standardnotes/server/commit/7b0ea0a06975902e01951b13c84e941827dedd84))
## [2.138.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.0...@standardnotes/domain-events@2.138.1) (2023-11-28)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.138.1",
"version": "2.139.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -12,4 +12,5 @@ export interface EmailRequestedEventPayload {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { FileQuotaRecalculatedEventPayload } from './FileQuotaRecalculatedEventPayload'
export interface FileQuotaRecalculatedEvent extends DomainEventInterface {
type: 'FILE_QUOTA_RECALCULATED'
payload: FileQuotaRecalculatedEventPayload
}

View File

@@ -0,0 +1,4 @@
export interface FileQuotaRecalculatedEventPayload {
userUuid: string
totalFileByteSize: number
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { FileQuotaRecalculationRequestedEventPayload } from './FileQuotaRecalculationRequestedEventPayload'
export interface FileQuotaRecalculationRequestedEvent extends DomainEventInterface {
type: 'FILE_QUOTA_RECALCULATION_REQUESTED'
payload: FileQuotaRecalculationRequestedEventPayload
}

View File

@@ -0,0 +1,3 @@
export interface FileQuotaRecalculationRequestedEventPayload {
userUuid: string
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { ItemDeletedEventPayload } from './ItemDeletedEventPayload'
export interface ItemDeletedEvent extends DomainEventInterface {
type: 'ITEM_DELETED'
payload: ItemDeletedEventPayload
}

View File

@@ -0,0 +1,4 @@
export interface ItemDeletedEventPayload {
userUuid: string
itemUuid: string
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { RevisionsCleanupRequestedEventPayload } from './RevisionsCleanupRequestedEventPayload'
export interface RevisionsCleanupRequestedEvent extends DomainEventInterface {
type: 'REVISIONS_CLEANUP_REQUESTED'
payload: RevisionsCleanupRequestedEventPayload
}

View File

@@ -0,0 +1,3 @@
export interface RevisionsCleanupRequestedEventPayload {
userUuid: string
}

View File

@@ -30,12 +30,18 @@ export * from './Event/ExitDiscountWithdrawRequestedEvent'
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
export * from './Event/ExtensionKeyGrantedEvent'
export * from './Event/ExtensionKeyGrantedEventPayload'
export * from './Event/FileQuotaRecalculatedEvent'
export * from './Event/FileQuotaRecalculatedEventPayload'
export * from './Event/FileQuotaRecalculationRequestedEvent'
export * from './Event/FileQuotaRecalculationRequestedEventPayload'
export * from './Event/FileRemovedEvent'
export * from './Event/FileRemovedEventPayload'
export * from './Event/FileUploadedEvent'
export * from './Event/FileUploadedEventPayload'
export * from './Event/ItemDumpedEvent'
export * from './Event/ItemDumpedEventPayload'
export * from './Event/ItemDeletedEvent'
export * from './Event/ItemDeletedEventPayload'
export * from './Event/ItemRemovedFromSharedVaultEvent'
export * from './Event/ItemRemovedFromSharedVaultEventPayload'
export * from './Event/ItemRevisionCreationRequestedEvent'
@@ -66,6 +72,8 @@ export * from './Event/PredicateVerifiedEvent'
export * from './Event/PredicateVerifiedEventPayload'
export * from './Event/RefundProcessedEvent'
export * from './Event/RefundProcessedEventPayload'
export * from './Event/RevisionsCleanupRequestedEvent'
export * from './Event/RevisionsCleanupRequestedEventPayload'
export * from './Event/RevisionsCopyRequestedEvent'
export * from './Event/RevisionsCopyRequestedEventPayload'
export * from './Event/SessionCreatedEvent'

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.1...@standardnotes/files-server@1.37.2) (2023-12-26)
**Note:** Version bump only for package @standardnotes/files-server
## [1.37.1](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.0...@standardnotes/files-server@1.37.1) (2023-12-14)
### Bug Fixes
* **files:** list files command ([e6a2160](https://github.com/standardnotes/server/commit/e6a21606a3070bbfbf2de9e4b10daa50c43c3786))
# [1.37.0](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.6...@standardnotes/files-server@1.37.0) (2023-12-14)
### Features
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
## [1.36.6](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.5...@standardnotes/files-server@1.36.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/files-server
## [1.36.5](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.4...@standardnotes/files-server@1.36.5) (2023-12-07)
### Bug Fixes

View File

@@ -5,12 +5,10 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-web' )
echo "Starting Web..."
exec node docker/entrypoint-server.js
;;
'start-worker' )
echo "Starting Worker..."
exec node docker/entrypoint-worker.js
;;

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.36.5",
"version": "1.37.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -52,6 +52,8 @@ import { S3FileMover } from '../Infra/S3/S3FileMover'
import { FSFileMover } from '../Infra/FS/FSFileMover'
import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
import { RecalculateQuota } from '../Domain/UseCase/RecalculateQuota/RecalculateQuota'
import { FileQuotaRecalculationRequestedEventHandler } from '../Domain/Handler/FileQuotaRecalculationRequestedEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -244,6 +246,15 @@ export class ContainerConfigLoader {
),
)
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
container
.bind<RecalculateQuota>(TYPES.Files_RecalculateQuota)
.toConstantValue(
new RecalculateQuota(
container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
),
)
// middleware
container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
@@ -274,6 +285,14 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
container
.bind<FileQuotaRecalculationRequestedEventHandler>(TYPES.Files_FileQuotaRecalculationRequestedEventHandler)
.toConstantValue(
new FileQuotaRecalculationRequestedEventHandler(
container.get<RecalculateQuota>(TYPES.Files_RecalculateQuota),
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Files_AccountDeletionRequestedEventHandler)],
@@ -281,6 +300,12 @@ export class ContainerConfigLoader {
'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
container.get(TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler),
],
[
'FILE_QUOTA_RECALCULATION_REQUESTED',
container.get<FileQuotaRecalculationRequestedEventHandler>(
TYPES.Files_FileQuotaRecalculationRequestedEventHandler,
),
],
])
if (isConfiguredForHomeServer) {

View File

@@ -15,6 +15,7 @@ const TYPES = {
Files_RemoveFile: Symbol.for('Files_RemoveFile'),
Files_MoveFile: Symbol.for('Files_MoveFile'),
Files_MarkFilesToBeRemoved: Symbol.for('Files_MarkFilesToBeRemoved'),
Files_RecalculateQuota: Symbol.for('Files_RecalculateQuota'),
// services
Files_ValetTokenDecoder: Symbol.for('Files_ValetTokenDecoder'),
@@ -57,6 +58,7 @@ const TYPES = {
Files_SharedSubscriptionInvitationCanceledEventHandler: Symbol.for(
'Files_SharedSubscriptionInvitationCanceledEventHandler',
),
Files_FileQuotaRecalculationRequestedEventHandler: Symbol.for('Files_FileQuotaRecalculationRequestedEventHandler'),
}
export default TYPES

View File

@@ -5,6 +5,7 @@ import {
SharedVaultFileUploadedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileMovedEvent,
FileQuotaRecalculatedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
@@ -13,6 +14,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(private timer: TimerInterface) {}
createFileQuotaRecalculatedEvent(payload: {
userUuid: string
totalFileByteSize: number
}): FileQuotaRecalculatedEvent {
return {
type: 'FILE_QUOTA_RECALCULATED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: payload.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Files,
},
payload,
}
}
createFileRemovedEvent(payload: {
userUuid: string
filePath: string

View File

@@ -4,9 +4,11 @@ import {
SharedVaultFileRemovedEvent,
SharedVaultFileUploadedEvent,
SharedVaultFileMovedEvent,
FileQuotaRecalculatedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createFileQuotaRecalculatedEvent(payload: { userUuid: string; totalFileByteSize: number }): FileQuotaRecalculatedEvent
createFileUploadedEvent(payload: {
userUuid: string
filePath: string

View File

@@ -0,0 +1,33 @@
import { DomainEventHandlerInterface, FileQuotaRecalculationRequestedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { RecalculateQuota } from '../UseCase/RecalculateQuota/RecalculateQuota'
export class FileQuotaRecalculationRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private recalculateQuota: RecalculateQuota,
private logger: Logger,
) {}
async handle(event: FileQuotaRecalculationRequestedEvent): Promise<void> {
this.logger.info('Recalculating quota for user...', {
userId: event.payload.userUuid,
})
const result = await this.recalculateQuota.execute({
userUuid: event.payload.userUuid,
})
if (result.isFailed()) {
this.logger.error('Could not recalculate quota', {
userId: event.payload.userUuid,
})
return
}
this.logger.info('Quota recalculated', {
userId: event.payload.userUuid,
})
}
}

View File

@@ -3,4 +3,5 @@ import { Readable } from 'stream'
export interface FileDownloaderInterface {
createDownloadStream(filePath: string, startRange: number, endRange: number): Promise<Readable>
getFileSize(filePath: string): Promise<number>
listFiles(userUuid: string): Promise<{ name: string; size: number }[]>
}

View File

@@ -0,0 +1,55 @@
import { DomainEventPublisherInterface, FileQuotaRecalculatedEvent } from '@standardnotes/domain-events'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
import { RecalculateQuota } from './RecalculateQuota'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
describe('RecalculateQuota', () => {
let fileDownloader: FileDownloaderInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
const createUseCase = () => new RecalculateQuota(fileDownloader, domainEventPublisher, domainEventFactory)
beforeEach(() => {
fileDownloader = {} as jest.Mocked<FileDownloaderInterface>
fileDownloader.listFiles = jest.fn().mockResolvedValue([
{
name: 'test-file',
size: 123,
},
])
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createFileQuotaRecalculatedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<FileQuotaRecalculatedEvent>)
})
it('publishes a file quota recalculated event', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(domainEventFactory.createFileQuotaRecalculatedEvent).toHaveBeenCalledWith({
userUuid: '00000000-0000-0000-0000-000000000000',
totalFileByteSize: 123,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('returns a failure result if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid-user-uuid',
})
expect(result.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,37 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { RecalculateQuotaDTO } from './RecalculateQuotaDTO'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
export class RecalculateQuota implements UseCaseInterface<void> {
constructor(
private fileDownloader: FileDownloaderInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
) {}
async execute(dto: RecalculateQuotaDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const filesList = await this.fileDownloader.listFiles(userUuid.value)
let totalFileByteSize = 0
for (const file of filesList) {
totalFileByteSize += file.size
}
const event = this.domainEventFactory.createFileQuotaRecalculatedEvent({
userUuid: dto.userUuid,
totalFileByteSize,
})
await this.domainEventPublisher.publish(event)
return Result.ok()
}
}

View File

@@ -0,0 +1,3 @@
export interface RecalculateQuotaDTO {
userUuid: string
}

View File

@@ -9,6 +9,21 @@ import TYPES from '../../Bootstrap/Types'
export class FSFileDownloader implements FileDownloaderInterface {
constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
async listFiles(userUuid: string): Promise<{ name: string; size: number }[]> {
const filesList = []
const files = await promises.readdir(`${this.fileUploadPath}/${userUuid}`)
for (const file of files) {
const fileStat = await promises.stat(`${this.fileUploadPath}/${userUuid}/${file}`)
filesList.push({
name: file,
size: fileStat.size,
})
}
return filesList
}
async getFileSize(filePath: string): Promise<number> {
return (await promises.stat(`${this.fileUploadPath}/${filePath}`)).size
}

View File

@@ -1,4 +1,4 @@
import { GetObjectCommand, HeadObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
import { inject, injectable } from 'inversify'
import { Readable } from 'stream'
@@ -34,4 +34,26 @@ export class S3FileDownloader implements FileDownloaderInterface {
return head.ContentLength as number
}
async listFiles(userUuid: string): Promise<{ name: string; size: number }[]> {
const objectsList = await this.s3Client.send(
new ListObjectsV2Command({
Bucket: this.s3BuckeName,
Prefix: userUuid,
}),
)
const filesList = []
for (const object of objectsList.Contents ?? []) {
if (!object.Key) {
continue
}
filesList.push({
name: object.Key,
size: object.Size ?? 0,
})
}
return filesList
}
}

View File

@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.27](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.26...@standardnotes/home-server@1.22.27) (2023-12-26)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.26](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.25...@standardnotes/home-server@1.22.26) (2023-12-22)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.25](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.24...@standardnotes/home-server@1.22.25) (2023-12-22)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.24](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.23...@standardnotes/home-server@1.22.24) (2023-12-22)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.23](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.22...@standardnotes/home-server@1.22.23) (2023-12-21)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.22](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.21...@standardnotes/home-server@1.22.22) (2023-12-14)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.21](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.20...@standardnotes/home-server@1.22.21) (2023-12-14)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.20](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.19...@standardnotes/home-server@1.22.20) (2023-12-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.19](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.18...@standardnotes/home-server@1.22.19) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.18](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.17...@standardnotes/home-server@1.22.18) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.17](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.16...@standardnotes/home-server@1.22.17) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.16](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.15...@standardnotes/home-server@1.22.16) (2023-12-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.15](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.14...@standardnotes/home-server@1.22.15) (2023-12-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.14](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.13...@standardnotes/home-server@1.22.14) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.13](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.12...@standardnotes/home-server@1.22.13) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.22.13",
"version": "1.22.27",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.51.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.7...@standardnotes/revisions-server@1.51.8) (2023-12-26)
### Bug Fixes
* stop revisions propagation ([#1008](https://github.com/standardnotes/server/issues/1008)) ([7962b24](https://github.com/standardnotes/server/commit/7962b245b51703ff7d33728dee117b2074f2692a))
## [1.51.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.6...@standardnotes/revisions-server@1.51.7) (2023-12-14)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.51.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.5...@standardnotes/revisions-server@1.51.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.51.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.4...@standardnotes/revisions-server@1.51.5) (2023-12-07)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.51.5",
"version": "1.51.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -52,6 +52,8 @@ import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevision
import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler'
import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
import { CreateRevisionFromDump } from '../Domain/UseCase/CreateRevisionFromDump/CreateRevisionFromDump'
import { DeleteRevisions } from '../Domain/UseCase/DeleteRevisions/DeleteRevisions'
import { ItemDeletedEventHandler } from '../Domain/Handler/ItemDeletedEventHandler'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -226,6 +228,11 @@ export class ContainerConfigLoader {
.toConstantValue(
new DeleteRevision(container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)),
)
container
.bind<DeleteRevisions>(TYPES.Revisions_DeleteRevisions)
.toConstantValue(
new DeleteRevisions(container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)),
)
container
.bind<CopyRevisions>(TYPES.Revisions_CopyRevisions)
.toConstantValue(
@@ -311,6 +318,14 @@ export class ContainerConfigLoader {
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
container
.bind<ItemDeletedEventHandler>(TYPES.Revisions_ItemDeletedEventHandler)
.toConstantValue(
new ItemDeletedEventHandler(
container.get<DeleteRevisions>(TYPES.Revisions_DeleteRevisions),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
@@ -318,6 +333,7 @@ export class ContainerConfigLoader {
['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)],
['SHARED_VAULT_REMOVED', container.get(TYPES.Revisions_SharedVaultRemovedEventHandler)],
['ITEM_DELETED', container.get(TYPES.Revisions_ItemDeletedEventHandler)],
])
if (isConfiguredForHomeServer) {

View File

@@ -33,6 +33,7 @@ const TYPES = {
Revisions_GetRevisionsMetada: Symbol.for('Revisions_GetRevisionsMetada'),
Revisions_GetRevision: Symbol.for('Revisions_GetRevision'),
Revisions_DeleteRevision: Symbol.for('Revisions_DeleteRevision'),
Revisions_DeleteRevisions: Symbol.for('Revisions_DeleteRevisions'),
Revisions_CopyRevisions: Symbol.for('Revisions_CopyRevisions'),
Revisions_GetRequiredRoleToViewRevision: Symbol.for('Revisions_GetRequiredRoleToViewRevision'),
Revisions_RemoveRevisionsFromSharedVault: Symbol.for('Revisions_RemoveRevisionsFromSharedVault'),
@@ -47,6 +48,7 @@ const TYPES = {
Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
Revisions_ItemRemovedFromSharedVaultEventHandler: Symbol.for('Revisions_ItemRemovedFromSharedVaultEventHandler'),
Revisions_SharedVaultRemovedEventHandler: Symbol.for('Revisions_SharedVaultRemovedEventHandler'),
Revisions_ItemDeletedEventHandler: Symbol.for('Revisions_ItemDeletedEventHandler'),
// Services
Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
Revisions_DomainEventSubscriber: Symbol.for('Revisions_DomainEventSubscriber'),

View File

@@ -0,0 +1,21 @@
import { DomainEventHandlerInterface, ItemDeletedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { DeleteRevisions } from '../UseCase/DeleteRevisions/DeleteRevisions'
export class ItemDeletedEventHandler implements DomainEventHandlerInterface {
constructor(
private deleteRevisions: DeleteRevisions,
private logger: Logger,
) {}
async handle(event: ItemDeletedEvent): Promise<void> {
const result = await this.deleteRevisions.execute({ itemUuid: event.payload.itemUuid })
if (result.isFailed()) {
this.logger.error(`Could not delete revisions for item ${event.payload.itemUuid}: ${result.getError()}`, {
userId: event.payload.userUuid,
})
}
}
}

View File

@@ -6,6 +6,7 @@ import { RevisionMetadata } from './RevisionMetadata'
export interface RevisionRepositoryInterface {
countByUserUuid(userUuid: Uuid): Promise<number>
removeByUserUuid(userUuid: Uuid): Promise<void>
removeByItemUuid(itemUuid: Uuid): Promise<void>
removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Revision | null>
findByItemUuid(itemUuid: Uuid): Promise<Array<Revision>>

View File

@@ -0,0 +1,32 @@
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
import { DeleteRevisions } from './DeleteRevisions'
describe('DeleteRevisions', () => {
let revisionRepository: RevisionRepositoryInterface
const createUseCase = () => new DeleteRevisions(revisionRepository)
beforeEach(() => {
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
revisionRepository.removeByItemUuid = jest.fn()
})
it('should remove revisions by item uuid', async () => {
const useCase = createUseCase()
const itemUuid = '00000000-0000-0000-0000-000000000000'
const result = await useCase.execute({ itemUuid })
expect(result.isFailed()).toBe(false)
expect(revisionRepository.removeByItemUuid).toHaveBeenCalled()
})
it('should return failed result if item uuid is invalid', async () => {
const useCase = createUseCase()
const itemUuid = 'invalid'
const result = await useCase.execute({ itemUuid })
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -0,0 +1,19 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
import { DeleteRevisionsDTO } from './DeleteRevisionsDTO'
export class DeleteRevisions implements UseCaseInterface<void> {
constructor(private revisionRepository: RevisionRepositoryInterface) {}
async execute(dto: DeleteRevisionsDTO): Promise<Result<void>> {
const itemUuidOrError = Uuid.create(dto.itemUuid)
if (itemUuidOrError.isFailed()) {
return Result.fail(`Could not delete revisions: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
await this.revisionRepository.removeByItemUuid(itemUuid)
return Result.ok()
}
}

View File

@@ -0,0 +1,3 @@
export interface DeleteRevisionsDTO {
itemUuid: string
}

View File

@@ -15,6 +15,15 @@ export class SQLRevisionRepository implements RevisionRepositoryInterface {
protected logger: Logger,
) {}
async removeByItemUuid(itemUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions_revisions')
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.execute()
}
async removeByUserUuid(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.27.13](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.12...@standardnotes/scheduler-server@1.27.13) (2023-12-26)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.27.12](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.11...@standardnotes/scheduler-server@1.27.12) (2023-12-14)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.27.11](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.10...@standardnotes/scheduler-server@1.27.11) (2023-12-11)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.27.10](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.9...@standardnotes/scheduler-server@1.27.10) (2023-12-07)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.27.10",
"version": "1.27.13",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.129.4](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.3...@standardnotes/syncing-server@1.129.4) (2023-12-26)
### Bug Fixes
* stop revisions propagation ([#1008](https://github.com/standardnotes/server/issues/1008)) ([7962b24](https://github.com/standardnotes/server/commit/7962b245b51703ff7d33728dee117b2074f2692a))
## [1.129.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.2...@standardnotes/syncing-server@1.129.3) (2023-12-22)
### Bug Fixes
* logs severity ([0762ed1](https://github.com/standardnotes/server/commit/0762ed1127a5de295bd50c14afad31c7fb88a853))
## [1.129.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.1...@standardnotes/syncing-server@1.129.2) (2023-12-22)
### Bug Fixes
* add more readonly access debug logs ([54091f2](https://github.com/standardnotes/server/commit/54091f23da33a5d1df2ecffdfa559e3fa4c562f6))
## [1.129.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.0...@standardnotes/syncing-server@1.129.1) (2023-12-14)
**Note:** Version bump only for package @standardnotes/syncing-server
# [1.129.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.2...@standardnotes/syncing-server@1.129.0) (2023-12-12)
### Features
* **syncing-server:** add extended revisions frequency for free users ([#965](https://github.com/standardnotes/server/issues/965)) ([398c10c](https://github.com/standardnotes/server/commit/398c10ce4b8e357728a8b4f354b3bf6ccc8e438d))
## [1.128.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.1...@standardnotes/syncing-server@1.128.2) (2023-12-11)
### Bug Fixes
* **syncing-server:** add user uuid for the emails requesting backup ([7b0ea0a](https://github.com/standardnotes/server/commit/7b0ea0a06975902e01951b13c84e941827dedd84))
## [1.128.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.0...@standardnotes/syncing-server@1.128.1) (2023-12-11)
### Bug Fixes
* **syncing-server:** logs meta for email with backup requested ([597ff13](https://github.com/standardnotes/server/commit/597ff13393965a6d6f3a35e12d41d648543d35b7))
# [1.128.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.9...@standardnotes/syncing-server@1.128.0) (2023-12-08)
### Features
* **syncing-server:** send websocket event to shared vault members upon items change in shared vault ([#961](https://github.com/standardnotes/server/issues/961)) ([6dbb877](https://github.com/standardnotes/server/commit/6dbb87708faf6c6f4ec28b45570390b6c816a7a2))
## [1.127.9](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.8...@standardnotes/syncing-server@1.127.9) (2023-12-07)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.127.9",
"version": "1.129.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -161,6 +161,8 @@ import { SyncResponse20200115 } from '../Domain/Item/SyncResponse/SyncResponse20
import { SyncResponse } from '@standardnotes/grpc'
import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper'
import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler'
import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients/SendEventToClients'
import { RevisionsCleanupRequestedEventHandler } from '../Domain/Handler/RevisionsCleanupRequestedEventHandler'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -457,6 +459,9 @@ export class ContainerConfigLoader {
container
.bind(TYPES.Sync_REVISIONS_FREQUENCY)
.toConstantValue(env.get('REVISIONS_FREQUENCY', true) ? +env.get('REVISIONS_FREQUENCY', true) : 300)
container
.bind(TYPES.Sync_FREE_REVISIONS_FREQUENCY)
.toConstantValue(env.get('FREE_REVISIONS_FREQUENCY', true) ? +env.get('FREE_REVISIONS_FREQUENCY', true) : 86_400)
container.bind(TYPES.Sync_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container
.bind(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT)
@@ -561,6 +566,15 @@ export class ContainerConfigLoader {
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<SendEventToClients>(TYPES.Sync_SendEventToClients)
.toConstantValue(
new SendEventToClients(
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
.toConstantValue(
@@ -591,6 +605,7 @@ export class ContainerConfigLoader {
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<number>(TYPES.Sync_FREE_REVISIONS_FREQUENCY),
container.get<number>(TYPES.Sync_REVISIONS_FREQUENCY),
container.get<DetermineSharedVaultOperationOnItem>(TYPES.Sync_DetermineSharedVaultOperationOnItem),
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
@@ -607,6 +622,7 @@ export class ContainerConfigLoader {
container.get<SaveNewItem>(TYPES.Sync_SaveNewItem),
container.get<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem),
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
container.get<SendEventToClients>(TYPES.Sync_SendEventToClients),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<Logger>(TYPES.Sync_Logger),
),
@@ -965,6 +981,16 @@ export class ContainerConfigLoader {
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<RevisionsCleanupRequestedEventHandler>(TYPES.Sync_RevisionsCleanupRequestedEventHandler)
.toConstantValue(
new RevisionsCleanupRequestedEventHandler(
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<Logger>(TYPES.Sync_Logger),
),
)
// Services
container.bind<ContentDecoderInterface>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
@@ -993,6 +1019,7 @@ export class ContainerConfigLoader {
'SHARED_VAULT_REMOVED',
container.get<SharedVaultRemovedEventHandler>(TYPES.Sync_SharedVaultRemovedEventHandler),
],
['REVISIONS_CLEANUP_REQUESTED', container.get(TYPES.Sync_RevisionsCleanupRequestedEventHandler)],
])
if (!isConfiguredForHomeServer) {
container

View File

@@ -34,6 +34,7 @@ const TYPES = {
Sync_S3_BACKUP_BUCKET_NAME: Symbol.for('Sync_S3_BACKUP_BUCKET_NAME'),
Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE: Symbol.for('Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE'),
Sync_REVISIONS_FREQUENCY: Symbol.for('Sync_REVISIONS_FREQUENCY'),
Sync_FREE_REVISIONS_FREQUENCY: Symbol.for('Sync_FREE_REVISIONS_FREQUENCY'),
Sync_VERSION: Symbol.for('Sync_VERSION'),
Sync_CONTENT_SIZE_TRANSFER_LIMIT: Symbol.for('Sync_CONTENT_SIZE_TRANSFER_LIMIT'),
Sync_MAX_ITEMS_LIMIT: Symbol.for('Sync_MAX_ITEMS_LIMIT'),
@@ -77,6 +78,7 @@ const TYPES = {
Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'),
Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'),
Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
Sync_SendEventToClients: Symbol.for('Sync_SendEventToClients'),
Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),
@@ -95,6 +97,7 @@ const TYPES = {
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
Sync_SharedVaultRemovedEventHandler: Symbol.for('Sync_SharedVaultRemovedEventHandler'),
Sync_RevisionsCleanupRequestedEventHandler: Symbol.for('Sync_RevisionsCleanupRequestedEventHandler'),
// Services
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),

View File

@@ -4,6 +4,7 @@ import {
DomainEventService,
DuplicateItemSyncedEvent,
EmailRequestedEvent,
ItemDeletedEvent,
ItemDumpedEvent,
ItemRemovedFromSharedVaultEvent,
ItemRevisionCreationRequestedEvent,
@@ -316,6 +317,21 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createItemDeletedEvent(dto: { itemUuid: string; userUuid: string }): ItemDeletedEvent {
return {
type: 'ITEM_DELETED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.SyncingServer,
},
payload: dto,
}
}
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string
@@ -329,6 +345,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_REQUESTED',

View File

@@ -2,6 +2,7 @@ import {
AccountDeletionVerificationPassedEvent,
DuplicateItemSyncedEvent,
EmailRequestedEvent,
ItemDeletedEvent,
ItemDumpedEvent,
ItemRemovedFromSharedVaultEvent,
ItemRevisionCreationRequestedEvent,
@@ -74,8 +75,10 @@ export interface DomainEventFactoryInterface {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}): EmailRequestedEvent
createDuplicateItemSyncedEvent(dto: { itemUuid: string; userUuid: string }): DuplicateItemSyncedEvent
createItemDeletedEvent(dto: { itemUuid: string; userUuid: string }): ItemDeletedEvent
createItemRevisionCreationRequested(dto: { itemUuid: string; userUuid: string }): ItemRevisionCreationRequestedEvent
createItemDumpedEvent(dto: { fileDumpPath: string; userUuid: string }): ItemDumpedEvent
createRevisionsCopyRequestedEvent(

View File

@@ -81,10 +81,13 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
attachmentContentType: 'application/json',
},
],
userUuid: event.payload.userUuid,
}),
)
}
this.logger.info(`Email with backup requested for user ${event.payload.userUuid}`)
this.logger.info('Email with backup requested for user', {
userId: event.payload.userUuid,
})
}
}

View File

@@ -0,0 +1,56 @@
import {
RevisionsCleanupRequestedEvent,
DomainEventHandlerInterface,
DomainEventPublisherInterface,
} from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
export class RevisionsCleanupRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private itemRepository: ItemRepositoryInterface,
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private logger: Logger,
) {}
async handle(event: RevisionsCleanupRequestedEvent): Promise<void> {
const totalDeletedItems = await this.itemRepository.countAll({
userUuid: event.payload.userUuid,
deleted: true,
})
this.logger.info(`Found ${totalDeletedItems} deleted items`, {
userId: event.payload.userUuid,
})
const limitPerPage = 100
const numberOfPages = Math.ceil(totalDeletedItems / limitPerPage)
for (let i = 0; i < numberOfPages; i++) {
const items = await this.itemRepository.findAll({
userUuid: event.payload.userUuid,
deleted: true,
offset: i * limitPerPage,
limit: limitPerPage,
sortOrder: 'ASC',
sortBy: 'created_at_timestamp',
})
for (const item of items) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createItemDeletedEvent({
itemUuid: item.id.toString(),
userUuid: item.props.userUuid.value,
}),
)
}
}
this.logger.info(`Finished processing ${totalDeletedItems} deleted items`, {
userId: event.payload.userUuid,
})
}
}

View File

@@ -11,6 +11,8 @@ import { Item } from '../../../Item/Item'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { ItemsChangedOnServerEvent } from '@standardnotes/domain-events'
import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
describe('SaveItems', () => {
let itemSaveValidator: ItemSaveValidatorInterface
@@ -22,6 +24,7 @@ describe('SaveItems', () => {
let itemHash1: ItemHash
let savedItem: Item
let sendEventToClient: SendEventToClient
let sendEventToClients: SendEventToClients
let domainEventFactory: DomainEventFactoryInterface
const createUseCase = () =>
@@ -32,6 +35,7 @@ describe('SaveItems', () => {
saveNewItem,
updateExistingItem,
sendEventToClient,
sendEventToClients,
domainEventFactory,
logger,
)
@@ -40,6 +44,9 @@ describe('SaveItems', () => {
sendEventToClient = {} as jest.Mocked<SendEventToClient>
sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
sendEventToClients = {} as jest.Mocked<SendEventToClients>
sendEventToClients.execute = jest.fn().mockReturnValue(Result.ok())
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createItemsChangedOnServerEvent = jest
.fn()
@@ -106,6 +113,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -130,6 +138,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -154,6 +163,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -175,6 +185,7 @@ describe('SaveItems', () => {
readOnlyAccess: true,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -197,6 +208,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -215,6 +227,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -233,16 +246,65 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(updateExistingItem.execute).toHaveBeenCalledWith({
isFreeUser: false,
itemHash: itemHash1,
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(sendEventToClient.execute).toHaveBeenCalled()
expect(sendEventToClients.execute).not.toHaveBeenCalled()
})
it('should update existing shared vault items', async () => {
savedItem = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
sharedVaultAssociation: SharedVaultAssociation.create({
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
}).getValue(),
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const useCase = createUseCase()
itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
updateExistingItem.execute = jest.fn().mockResolvedValue(Result.ok(savedItem))
const result = await useCase.execute({
itemHashes: [itemHash1],
userUuid: '00000000-0000-0000-0000-000000000000',
apiVersion: '1',
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(updateExistingItem.execute).toHaveBeenCalledWith({
isFreeUser: false,
itemHash: itemHash1,
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(sendEventToClient.execute).toHaveBeenCalled()
expect(sendEventToClients.execute).toHaveBeenCalled()
})
it('should mark items as conflicts if updating existing item fails', async () => {
@@ -258,6 +320,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -282,6 +345,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -327,6 +391,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()

View File

@@ -13,6 +13,7 @@ import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
export class SaveItems implements UseCaseInterface<SaveItemsResult> {
private readonly SYNC_TOKEN_VERSION = 2
@@ -24,6 +25,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
private saveNewItem: SaveNewItem,
private updateExistingItem: UpdateExistingItem,
private sendEventToClient: SendEventToClient,
private sendEventToClients: SendEventToClients,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
@@ -82,6 +84,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
itemHash,
sessionUuid: dto.sessionUuid,
performingUserUuid: dto.userUuid,
isFreeUser: dto.isFreeUser,
})
if (udpatedItemOrError.isFailed()) {
this.logger.error(
@@ -167,7 +170,31 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`[${dto.userUuid}] Sending items changed event to client failed. Error: ${result.getError()}`)
this.logger.error(`Sending items changed event to client failed. Error: ${result.getError()}`, {
userId: dto.userUuid,
})
}
const sharedVaultUuidsMap = new Map<string, boolean>()
for (const item of savedItems) {
if (item.isAssociatedWithASharedVault()) {
sharedVaultUuidsMap.set((item.sharedVaultUuid as Uuid).value, true)
}
}
const sharedVaultUuids = Array.from(sharedVaultUuidsMap.keys())
for (const sharedVaultUuid of sharedVaultUuids) {
const result = await this.sendEventToClients.execute({
sharedVaultUuid,
event: itemsChangedEvent,
originatingUserUuid: dto.userUuid,
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Sending items changed event to clients failed. Error: ${result.getError()}`, {
userId: dto.userUuid,
sharedVaultUuid,
})
}
}
}

View File

@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
readOnlyAccess: boolean
sessionUuid: string | null
snjsVersion: string
isFreeUser: boolean
}

View File

@@ -0,0 +1,108 @@
import { Logger } from 'winston'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { SendEventToClients } from './SendEventToClients'
import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
import { DomainEventInterface } from '@standardnotes/domain-events'
describe('SendEventToClients', () => {
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let sendEventToClient: SendEventToClient
let logger: Logger
const createUseCase = () => new SendEventToClients(sharedVaultUserRepository, sendEventToClient, logger)
beforeEach(() => {
const sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create('read').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
isDesignatedSurvivor: false,
}).getValue()
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultUser])
sendEventToClient = {} as jest.Mocked<SendEventToClient>
sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should send event to all users', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000003',
})
expect(result.isFailed()).toBeFalsy()
expect(sendEventToClient.execute).toHaveBeenCalledTimes(1)
})
it('should send event to all users except the originating one', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.isFailed()).toBeFalsy()
expect(sendEventToClient.execute).toHaveBeenCalledTimes(0)
})
it('should return error if shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: 'invalid',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if originating user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
})
it('should log error if sending event to client failed', async () => {
sendEventToClient.execute = jest.fn().mockReturnValue(Result.fail('test error'))
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000003',
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,50 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { SendEventToClientsDTO } from './SendEventToClientsDTO'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
export class SendEventToClients implements UseCaseInterface<void> {
constructor(
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private sendEventToClient: SendEventToClient,
private logger: Logger,
) {}
async execute(dto: SendEventToClientsDTO): Promise<Result<void>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const originatingUserUuidOrError = Uuid.create(dto.originatingUserUuid)
if (originatingUserUuidOrError.isFailed()) {
return Result.fail(originatingUserUuidOrError.getError())
}
const originatingUserUuid = originatingUserUuidOrError.getValue()
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
for (const sharedVaultUser of sharedVaultUsers) {
if (originatingUserUuid.equals(sharedVaultUser.props.userUuid)) {
continue
}
const result = await this.sendEventToClient.execute({
event: dto.event,
userUuid: sharedVaultUser.props.userUuid.value,
})
if (result.isFailed()) {
this.logger.error(`Failed to send event to client: ${result.getError()}`, {
userId: sharedVaultUser.props.userUuid.value,
sharedVaultUuid: sharedVaultUuid.value,
})
}
}
return Result.ok()
}
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from '@standardnotes/domain-events'
export interface SendEventToClientsDTO {
sharedVaultUuid: string
event: DomainEventInterface
originatingUserUuid: string
}

View File

@@ -156,6 +156,7 @@ describe('SyncItems', () => {
apiVersion: ApiVersion.v20200115,
sessionUuid: null,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
conflicts: [],
@@ -181,6 +182,7 @@ describe('SyncItems', () => {
userUuid: '1-2-3',
apiVersion: '20200115',
snjsVersion: '1.2.3',
isFreeUser: false,
readOnlyAccess: false,
sessionUuid: null,
})
@@ -205,6 +207,7 @@ describe('SyncItems', () => {
apiVersion: ApiVersion.v20200115,
sessionUuid: null,
snjsVersion: '1.2.3',
isFreeUser: false,
})
} catch (error) {
caughtError = error
@@ -224,6 +227,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
conflicts: [],
@@ -249,6 +253,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
sharedVaultUuids: ['00000000-0000-0000-0000-000000000000'],
})
expect(result.getValue()).toEqual({
@@ -301,6 +306,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
@@ -340,6 +346,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -360,6 +367,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -380,6 +388,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -400,6 +409,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -420,6 +430,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -440,6 +451,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()

View File

@@ -47,6 +47,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
readOnlyAccess: dto.readOnlyAccess,
sessionUuid: dto.sessionUuid,
snjsVersion: dto.snjsVersion,
isFreeUser: dto.isFreeUser,
})
if (saveItemsResultOrError.isFailed()) {
return Result.fail(saveItemsResultOrError.getError())

View File

@@ -13,4 +13,5 @@ export type SyncItemsDTO = {
snjsVersion: string
readOnlyAccess: boolean
sessionUuid: string | null
isFreeUser: boolean
}

View File

@@ -38,6 +38,7 @@ describe('UpdateExistingItem', () => {
timer,
domainEventPublisher,
domainEventFactory,
86_400,
5,
determineSharedVaultOperationOnItem,
addNotificationsForUsers,
@@ -108,6 +109,7 @@ describe('UpdateExistingItem', () => {
domainEventFactory.createItemRemovedFromSharedVaultEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<DomainEventInterface>)
domainEventFactory.createItemDeletedEvent = jest.fn().mockReturnValue({} as jest.Mocked<DomainEventInterface>)
determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
determineSharedVaultOperationOnItem.execute = jest.fn().mockResolvedValue(
@@ -137,6 +139,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -151,6 +154,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: 'invalid-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -167,6 +171,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -183,6 +188,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -206,6 +212,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -224,6 +231,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -241,6 +249,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -260,6 +269,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -280,6 +290,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -300,6 +311,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -323,6 +335,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -347,6 +360,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -361,6 +375,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: 'invalid-uuid',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -379,6 +394,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
@@ -405,6 +421,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -442,6 +459,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -470,6 +488,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
@@ -490,6 +509,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -521,6 +541,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -555,6 +576,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -576,6 +598,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -595,6 +618,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.keySystemAssociation).not.toBeUndefined()
@@ -616,6 +640,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -636,6 +661,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -658,6 +684,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()

View File

@@ -31,7 +31,8 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
private timer: TimerInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private revisionFrequency: number,
private freeRevisionFrequency: number,
private premiumRevisionFrequency: number,
private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
private addNotificationForUsers: AddNotificationsForUsers,
private removeNotificationsForUser: RemoveNotificationsForUser,
@@ -158,6 +159,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
dto.existingItem.props.keySystemAssociation = undefined
}
let wasMarkedAsDeleted = false
if (dto.itemHash.props.deleted === true) {
dto.existingItem.props.deleted = true
dto.existingItem.props.content = null
@@ -165,11 +167,16 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
dto.existingItem.props.encItemKey = null
dto.existingItem.props.authHash = null
dto.existingItem.props.itemsKeyId = null
wasMarkedAsDeleted = true
}
await this.itemRepository.update(dto.existingItem)
if (secondsFromLastUpdate >= this.revisionFrequency) {
/* istanbul ignore next */
const revisionsFrequency = dto.isFreeUser ? this.freeRevisionFrequency : this.premiumRevisionFrequency
if (secondsFromLastUpdate >= revisionsFrequency) {
if (
dto.existingItem.props.contentType.value !== null &&
[ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)
@@ -192,6 +199,15 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
)
}
if (wasMarkedAsDeleted) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createItemDeletedEvent({
itemUuid: dto.existingItem.id.toString(),
userUuid: dto.existingItem.props.userUuid.value,
}),
)
}
const notificationsResult = await this.addNotificationsAndPublishEvents(userUuid, sharedVaultOperation, dto)
if (notificationsResult.isFailed()) {
return Result.fail(notificationsResult.getError())

View File

@@ -6,4 +6,5 @@ export interface UpdateExistingItemDTO {
itemHash: ItemHash
sessionUuid: string | null
performingUserUuid: string
isFreeUser: boolean
}

View File

@@ -70,6 +70,7 @@ export class BaseItemsController extends BaseHttpController {
readOnlyAccess: response.locals.readOnlyAccess,
sessionUuid: response.locals.session ? response.locals.session.uuid : null,
sharedVaultUuids,
isFreeUser: response.locals.isFreeUser,
})
if (syncResult.isFailed()) {
return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)

View File

@@ -3,6 +3,7 @@ import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston'
import { RoleName } from '@standardnotes/domain-core'
export class InversifyExpressAuthMiddleware extends BaseMiddleware {
constructor(
@@ -26,6 +27,8 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
response.locals.isFreeUser =
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
response.locals.session = decodedToken.session
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context

Some files were not shown because too many files have changed in this diff Show More